PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
patient_reconciliation_service.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
13
14#include <algorithm>
15#include <chrono>
16#include <random>
17#include <set>
18
20
21using namespace kcenon::pacs::core;
22using namespace kcenon::pacs::encoding;
23
24// =============================================================================
25// patient_reconciliation_service Implementation
26// =============================================================================
27
29 const core::dicom_dataset& dataset) {
30
32 if (uid.empty()) {
33 return false;
34 }
35
36 instances_.push_back(dataset);
37 return true;
38}
39
41 const demographics_update_request& request) {
42
45
46 if (request.target_patient_id.empty()) {
47 result.error_message = "Target patient ID is required";
48 return result;
49 }
50
51 size_t updated = 0;
52 std::set<std::string> affected_studies;
53
54 for (auto& instance : instances_) {
55 auto pid = instance.get_string(tags::patient_id);
56 if (pid == request.target_patient_id) {
58 updated++;
59
60 auto study_uid = instance.get_string(tags::study_instance_uid);
61 if (!study_uid.empty()) {
62 affected_studies.insert(study_uid);
63 }
64 }
65 }
66
67 if (updated == 0) {
68 result.error_message = "No instances found for patient ID: " +
69 request.target_patient_id;
70 return result;
71 }
72
73 result.success = true;
74 result.instances_updated = updated;
75 result.studies_affected = affected_studies.size();
76
77 // Create audit record
82 audit.operator_name = request.operator_name.value_or("SYSTEM");
83 audit.instances_updated = updated;
84 audit.timestamp = std::chrono::system_clock::now();
85 audit.success = true;
86 audit_records_.push_back(std::move(audit));
87
88 return result;
89}
90
92 const patient_merge_request& request) {
93
96
97 if (request.source_patient_id.empty()) {
98 result.error_message = "Source patient ID is required";
99 return result;
100 }
101
102 if (request.target_patient_id.empty()) {
103 result.error_message = "Target patient ID is required";
104 return result;
105 }
106
107 if (request.source_patient_id == request.target_patient_id) {
108 result.error_message = "Source and target patient IDs must be different";
109 return result;
110 }
111
112 size_t updated = 0;
113 std::set<std::string> affected_studies;
114
115 for (auto& instance : instances_) {
116 auto pid = instance.get_string(tags::patient_id);
117 if (pid == request.source_patient_id) {
118 // Reassign to target patient
119 instance.set_string(tags::patient_id, vr_type::LO,
120 request.target_patient_id);
121
122 // Apply target demographics if provided
123 if (request.target_demographics) {
124 apply_demographics(instance, request.target_demographics.value());
125 }
126
127 updated++;
128
129 auto study_uid = instance.get_string(tags::study_instance_uid);
130 if (!study_uid.empty()) {
131 affected_studies.insert(study_uid);
132 }
133 }
134 }
135
136 if (updated == 0) {
137 result.error_message = "No instances found for source patient ID: " +
138 request.source_patient_id;
139 return result;
140 }
141
142 result.success = true;
143 result.instances_updated = updated;
144 result.studies_affected = affected_studies.size();
145
146 // Create audit record
152 audit.operator_name = request.operator_name.value_or("SYSTEM");
153 audit.instances_updated = updated;
154 audit.timestamp = std::chrono::system_clock::now();
155 audit.success = true;
156 audit_records_.push_back(std::move(audit));
157
158 return result;
159}
160
161std::vector<core::dicom_dataset> patient_reconciliation_service::find_instances(
162 const std::string& patient_id) const {
163
164 std::vector<core::dicom_dataset> results;
165 for (const auto& instance : instances_) {
166 if (instance.get_string(tags::patient_id) == patient_id) {
167 results.push_back(instance);
168 }
169 }
170 return results;
171}
172
174 return instances_.size();
175}
176
177std::vector<std::string> patient_reconciliation_service::get_patient_ids() const {
178 std::set<std::string> ids;
179 for (const auto& instance : instances_) {
180 auto pid = instance.get_string(tags::patient_id);
181 if (!pid.empty()) {
182 ids.insert(pid);
183 }
184 }
185 return {ids.begin(), ids.end()};
186}
187
188const std::vector<reconciliation_audit_record>&
192
193std::vector<reconciliation_audit_record>
195 const std::string& patient_id) const {
196
197 std::vector<reconciliation_audit_record> results;
198 for (const auto& record : audit_records_) {
199 if (record.primary_patient_id == patient_id ||
200 (record.secondary_patient_id &&
201 record.secondary_patient_id.value() == patient_id)) {
202 results.push_back(record);
203 }
204 }
205 return results;
206}
207
208// =============================================================================
209// Private Methods
210// =============================================================================
211
213 core::dicom_dataset& dataset,
214 const patient_demographics& demographics) const {
215
216 if (demographics.patient_name) {
217 dataset.set_string(tags::patient_name, vr_type::PN,
218 demographics.patient_name.value());
219 }
220 if (demographics.patient_id) {
221 dataset.set_string(tags::patient_id, vr_type::LO,
222 demographics.patient_id.value());
223 }
224 if (demographics.patient_birth_date) {
225 dataset.set_string(tags::patient_birth_date, vr_type::DA,
226 demographics.patient_birth_date.value());
227 }
228 if (demographics.patient_sex) {
229 dataset.set_string(tags::patient_sex, vr_type::CS,
230 demographics.patient_sex.value());
231 }
232 if (demographics.issuer_of_patient_id) {
233 dataset.set_string(tags::issuer_of_patient_id, vr_type::LO,
234 demographics.issuer_of_patient_id.value());
235 }
236}
237
239 static std::mt19937_64 gen{std::random_device{}()};
240 static std::uniform_int_distribution<uint64_t> dist;
241
242 auto now = std::chrono::system_clock::now();
243 auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
244 now.time_since_epoch()).count();
245
246 return "PIR-" + std::to_string(timestamp) + "-" +
247 std::to_string(dist(gen) % 100000);
248}
249
250// =============================================================================
251// Free Functions
252// =============================================================================
253
255 switch (type) {
256 case reconciliation_type::demographics_update: return "demographics_update";
257 case reconciliation_type::patient_merge: return "patient_merge";
258 case reconciliation_type::patient_link: return "patient_link";
259 }
260 return "demographics_update";
261}
262
263} // namespace kcenon::pacs::services::pir
void set_string(dicom_tag tag, encoding::vr_type vr, std::string_view value)
Set a string value for the given tag.
auto get_string(dicom_tag tag, std::string_view default_value="") const -> std::string
Get the string value of an element.
std::vector< core::dicom_dataset > find_instances(const std::string &patient_id) const
Find all instances for a given patient ID.
bool add_instance(const core::dicom_dataset &dataset)
Add a DICOM instance to the managed store.
size_t instance_count() const noexcept
Get the total number of managed instances.
std::vector< std::string > get_patient_ids() const
Get distinct patient IDs in the store.
reconciliation_result merge_patients(const patient_merge_request &request)
Merge instances from source patient to target patient.
void apply_demographics(core::dicom_dataset &dataset, const patient_demographics &demographics) const
reconciliation_result update_demographics(const demographics_update_request &request)
Update patient demographics across all matching instances.
std::vector< reconciliation_audit_record > audit_trail_for_patient(const std::string &patient_id) const
Get audit records for a specific patient.
const std::vector< reconciliation_audit_record > & audit_trail() const noexcept
Get the audit trail of reconciliation operations.
Compile-time constants for commonly used DICOM tags.
constexpr dicom_tag issuer_of_patient_id
Issuer of Patient ID.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag sop_instance_uid
SOP Instance UID.
constexpr dicom_tag patient_birth_date
Patient's Birth Date.
constexpr dicom_tag patient_sex
Patient's Sex.
constexpr dicom_tag study_instance_uid
Study Instance UID.
constexpr dicom_tag patient_name
Patient's Name.
constexpr std::string_view to_string(vr_type vr) noexcept
Converts a vr_type to its two-character string representation.
Definition vr_type.h:83
reconciliation_type
Type of patient reconciliation operation.
@ patient_link
ADT^A24: link related patients.
@ demographics_update
ADT^A08: update patient demographics.
@ patient_merge
ADT^A40: merge two patients.
IHE PIR (Patient Information Reconciliation) Service.
std::optional< std::string > operator_name
Operator performing the update.
Updated patient demographics for a reconciliation operation.
std::optional< std::string > patient_name
Patient Name (0010,0010)
std::optional< std::string > patient_birth_date
Patient Birth Date (0010,0030)
std::optional< std::string > issuer_of_patient_id
Issuer of Patient ID (0010,0021)
std::optional< std::string > patient_id
Patient ID (0010,0020)
std::optional< std::string > patient_sex
Patient Sex (0010,0040)
std::string target_patient_id
Patient ID to merge into (target - will retain)
std::optional< patient_demographics > target_demographics
Optional updated demographics for the target.
std::optional< std::string > operator_name
Operator performing the merge.
std::string source_patient_id
Patient ID to merge from (source - will be removed)
std::string record_id
Unique identifier for this audit record.
reconciliation_type type
Type of operation performed.
std::string_view uid