his_emr_api_lab 1.1.19 → 1.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. metadata +8 -86
  3. data/MIT-LICENSE +0 -20
  4. data/README.md +0 -71
  5. data/Rakefile +0 -32
  6. data/app/controllers/lab/application_controller.rb +0 -6
  7. data/app/controllers/lab/labels_controller.rb +0 -17
  8. data/app/controllers/lab/orders_controller.rb +0 -38
  9. data/app/controllers/lab/reasons_for_test_controller.rb +0 -9
  10. data/app/controllers/lab/results_controller.rb +0 -19
  11. data/app/controllers/lab/specimen_types_controller.rb +0 -15
  12. data/app/controllers/lab/test_result_indicators_controller.rb +0 -9
  13. data/app/controllers/lab/test_types_controller.rb +0 -15
  14. data/app/controllers/lab/tests_controller.rb +0 -26
  15. data/app/jobs/lab/application_job.rb +0 -4
  16. data/app/jobs/lab/push_order_job.rb +0 -12
  17. data/app/jobs/lab/update_patient_orders_job.rb +0 -32
  18. data/app/jobs/lab/void_order_job.rb +0 -17
  19. data/app/mailers/lab/application_mailer.rb +0 -6
  20. data/app/models/lab/application_record.rb +0 -5
  21. data/app/models/lab/lab_accession_number_counter.rb +0 -13
  22. data/app/models/lab/lab_encounter.rb +0 -7
  23. data/app/models/lab/lab_order.rb +0 -54
  24. data/app/models/lab/lab_result.rb +0 -31
  25. data/app/models/lab/lab_test.rb +0 -19
  26. data/app/models/lab/lims_failed_import.rb +0 -4
  27. data/app/models/lab/lims_order_mapping.rb +0 -10
  28. data/app/serializers/lab/lab_order_serializer.rb +0 -55
  29. data/app/serializers/lab/result_serializer.rb +0 -36
  30. data/app/serializers/lab/test_serializer.rb +0 -29
  31. data/app/services/lab/accession_number_service.rb +0 -77
  32. data/app/services/lab/concepts_service.rb +0 -82
  33. data/app/services/lab/labelling_service/order_label.rb +0 -106
  34. data/app/services/lab/lims/api/blackhole_api.rb +0 -21
  35. data/app/services/lab/lims/api/couchdb_api.rb +0 -53
  36. data/app/services/lab/lims/api/mysql_api.rb +0 -316
  37. data/app/services/lab/lims/api/rest_api.rb +0 -413
  38. data/app/services/lab/lims/api/ws_api.rb +0 -121
  39. data/app/services/lab/lims/api_factory.rb +0 -19
  40. data/app/services/lab/lims/config.rb +0 -100
  41. data/app/services/lab/lims/exceptions.rb +0 -11
  42. data/app/services/lab/lims/migrator.rb +0 -216
  43. data/app/services/lab/lims/order_dto.rb +0 -105
  44. data/app/services/lab/lims/order_serializer.rb +0 -216
  45. data/app/services/lab/lims/pull_worker.rb +0 -289
  46. data/app/services/lab/lims/push_worker.rb +0 -144
  47. data/app/services/lab/lims/utils.rb +0 -91
  48. data/app/services/lab/lims/worker.rb +0 -86
  49. data/app/services/lab/metadata.rb +0 -24
  50. data/app/services/lab/orders_search_service.rb +0 -66
  51. data/app/services/lab/orders_service.rb +0 -212
  52. data/app/services/lab/results_service.rb +0 -122
  53. data/app/services/lab/tests_service.rb +0 -93
  54. data/config/routes.rb +0 -17
  55. data/db/migrate/20210126092910_create_lab_lab_accession_number_counters.rb +0 -12
  56. data/db/migrate/20210310115457_create_lab_lims_order_mappings.rb +0 -15
  57. data/db/migrate/20210323080140_change_lims_id_to_string_in_lims_order_mapping.rb +0 -15
  58. data/db/migrate/20210326195504_add_order_revision_to_lims_order_mapping.rb +0 -5
  59. data/db/migrate/20210407071728_create_lab_lims_failed_imports.rb +0 -19
  60. data/db/migrate/20210610095024_fix_numeric_results_value_type.rb +0 -20
  61. data/db/migrate/20210807111531_add_default_to_lims_order_mapping.rb +0 -7
  62. data/lib/auto12epl.rb +0 -201
  63. data/lib/couch_bum/couch_bum.rb +0 -92
  64. data/lib/generators/lab/install/USAGE +0 -9
  65. data/lib/generators/lab/install/install_generator.rb +0 -19
  66. data/lib/generators/lab/install/templates/rswag-ui-lab.rb +0 -5
  67. data/lib/generators/lab/install/templates/start_worker.rb +0 -32
  68. data/lib/generators/lab/install/templates/swagger.yaml +0 -714
  69. data/lib/his_emr_api_lab.rb +0 -5
  70. data/lib/lab/engine.rb +0 -15
  71. data/lib/lab/version.rb +0 -5
  72. data/lib/logger_multiplexor.rb +0 -38
  73. data/lib/tasks/lab_tasks.rake +0 -25
  74. data/lib/tasks/loaders/data/reasons-for-test.csv +0 -7
  75. data/lib/tasks/loaders/data/test-measures.csv +0 -225
  76. data/lib/tasks/loaders/data/tests.csv +0 -161
  77. data/lib/tasks/loaders/loader_mixin.rb +0 -53
  78. data/lib/tasks/loaders/metadata_loader.rb +0 -26
  79. data/lib/tasks/loaders/reasons_for_test_loader.rb +0 -23
  80. data/lib/tasks/loaders/specimens_loader.rb +0 -65
  81. data/lib/tasks/loaders/test_result_indicators_loader.rb +0 -54
@@ -1,216 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'csv'
4
- require 'parallel'
5
-
6
- require 'couch_bum/couch_bum'
7
- require 'logger_multiplexor'
8
-
9
- require 'concept'
10
- require 'concept_name'
11
- require 'drug_order'
12
- require 'encounter'
13
- require 'encounter_type'
14
- require 'observation'
15
- require 'order'
16
- require 'order_type'
17
- require 'patient'
18
- require 'patient_identifier'
19
- require 'patient_identifier_type'
20
- require 'person'
21
- require 'person_name'
22
- require 'program'
23
- require 'user'
24
-
25
- require 'lab/lab_encounter'
26
- require 'lab/lab_order'
27
- require 'lab/lab_result'
28
- require 'lab/lab_test'
29
- require 'lab/lims_order_mapping'
30
- require 'lab/lims_failed_import'
31
-
32
- require_relative './api/couchdb_api'
33
- require_relative './config'
34
- require_relative './pull_worker'
35
- require_relative './utils'
36
-
37
- require_relative '../orders_service'
38
- require_relative '../results_service'
39
- require_relative '../tests_service'
40
- require_relative '../../../serializers/lab/lab_order_serializer'
41
- require_relative '../../../serializers/lab/result_serializer'
42
- require_relative '../../../serializers/lab/test_serializer'
43
-
44
- require_relative 'order_dto'
45
- require_relative 'utils'
46
-
47
- module Lab
48
- module Lims
49
- ##
50
- # Tools for performing a bulk import of data from LIMS' databases to local OpenMRS database.
51
- #
52
- # Migration sources supported:
53
- # - MySQL
54
- # - CouchDB
55
- #
56
- # The sources above can be changed by setting the environment various MIGRATION_SOURCE to
57
- # either mysql or couchdb.
58
- module Migrator
59
- MAX_THREADS = ENV.fetch('MIGRATION_WORKERS', 6).to_i
60
-
61
- ##
62
- # A Lab::Lims::Api object that supports crawling of a LIMS CouchDB instance.
63
- class CouchDbMigratorApi < Lab::Lims::Api::CouchDbApi
64
- def initialize(*args, processes: 1, on_merge_processes: nil, **kwargs)
65
- super(*args, **kwargs)
66
-
67
- @processes = processes
68
- @on_merge_processes = on_merge_processes
69
- end
70
-
71
- def consume_orders(from: nil, **_kwargs)
72
- limit = 25_000
73
-
74
- loop do
75
- on_merge_processes = ->(_item, index, _result) { @on_merge_processes&.call(from + index) }
76
- processes = @processes > 1 ? @processes : 0
77
-
78
- orders = read_orders(from, limit)
79
- break if orders.empty?
80
-
81
- Parallel.each(orders, in_processes: processes, finish: on_merge_processes) do |row|
82
- next unless row['doc']['type']&.casecmp?('Order')
83
-
84
- User.current = Utils.lab_user
85
- yield OrderDTO.new(row['doc']), OpenStruct.new(last_seq: (from || 0) + limit, current_seq: from)
86
- end
87
-
88
- from += orders.size
89
- end
90
- end
91
-
92
- private
93
-
94
- def read_orders(from, batch_size)
95
- start_key_param = from ? "&skip=#{from}" : ''
96
- url = "_all_docs?include_docs=true&limit=#{batch_size}#{start_key_param}"
97
-
98
- Rails.logger.debug("#{CouchDbMigratorApi}: Pulling orders from LIMS CouchDB: #{url}")
99
- response = bum.couch_rest :get, url
100
-
101
- response['rows']
102
- end
103
- end
104
-
105
- ##
106
- # Extends the PullWorker to provide pause/resume capabilities.
107
- #
108
- # Migrations can be take a long time to complete, in cases where something
109
- # went wrong you wouldn't to start all over. This worker thus saves
110
- # progress and allows for the process to continue from whether it stopped.
111
- class MigrationWorker < PullWorker
112
- LOG_FILE_PATH = Utils::LIMS_LOG_PATH.join('migration-last-id.dat')
113
-
114
- attr_reader :rejections
115
-
116
- def initialize(api_class)
117
- api = api_class.new(processes: MAX_THREADS, on_merge_processes: method(:save_seq))
118
- super(api)
119
- end
120
-
121
- def last_seq
122
- return 0 unless File.exist?(LOG_FILE_PATH)
123
-
124
- File.open(LOG_FILE_PATH, File::RDONLY) do |file|
125
- last_seq = file.read&.strip
126
- return last_seq.blank? ? nil : last_seq&.to_i
127
- end
128
- end
129
-
130
- private
131
-
132
- def save_seq(last_seq)
133
- File.open(LOG_FILE_PATH, File::WRONLY | File::CREAT, 0o644) do |file|
134
- Rails.logger.debug("Process ##{Parallel.worker_number}: Saving last seq: #{last_seq}")
135
- file.flock(File::LOCK_EX)
136
- file.write(last_seq.to_s)
137
- file.flush
138
- end
139
- end
140
-
141
- def order_rejected(order_dto, reason)
142
- @rejections ||= []
143
-
144
- @rejections << OpenStruct.new(order: order_dto, reason: reason)
145
- end
146
- end
147
-
148
- def self.save_csv(filename, rows:, headers: nil)
149
- CSV.open(filename, File::WRONLY | File::CREAT) do |csv|
150
- csv << headers if headers
151
- rows.each { |row| csv << row }
152
- end
153
- end
154
-
155
- # NOTE: LIMS_LOG_PATH below is defined in worker.rb
156
- MIGRATION_REJECTIONS_CSV_PATH = Utils::LIMS_LOG_PATH.join('migration-rejections.csv')
157
-
158
- def self.export_rejections(rejections)
159
- headers = ['doc_id', 'Accession number', 'NHID', 'First name', 'Last name', 'Reason']
160
- rows = (rejections || []).map do |rejection|
161
- [
162
- rejection.order[:_id],
163
- rejection.order[:tracking_number],
164
- rejection.order[:patient][:id],
165
- rejection.order[:patient][:first_name],
166
- rejection.order[:patient][:last_name],
167
- rejection.reason
168
- ]
169
- end
170
-
171
- save_csv(MIGRATION_REJECTIONS_CSV_PATH, headers: headers, rows: rows)
172
- end
173
-
174
- MIGRATION_FAILURES_CSV_PATH = Utils::LIMS_LOG_PATH.join('migration-failures.csv')
175
-
176
- def self.export_failures
177
- headers = ['doc_id', 'Accession number', 'NHID', 'Reason', 'Difference']
178
- rows = Lab::LimsFailedImport.all.map do |failure|
179
- [
180
- failure.lims_id,
181
- failure.tracking_number,
182
- failure.patient_nhid,
183
- failure.reason,
184
- failure.diff
185
- ]
186
- end
187
-
188
- save_csv(MIGRATION_FAILURES_CSV_PATH, headers: headers, rows: rows)
189
- end
190
-
191
- MIGRATION_LOG_PATH = Utils::LIMS_LOG_PATH.join('migration.log')
192
-
193
- def self.start_migration
194
- Dir.mkdir(Utils::LIMS_LOG_PATH) unless File.exist?(Utils::LIMS_LOG_PATH)
195
-
196
- logger = LoggerMultiplexor.new(Logger.new($stdout), MIGRATION_LOG_PATH)
197
- logger.level = :debug
198
- Rails.logger = logger
199
- ActiveRecord::Base.logger = logger
200
- # CouchBum.logger = logger
201
-
202
- api_class = case ENV.fetch('MIGRATION_SOURCE', 'couchdb').downcase
203
- when 'couchdb' then CouchDbMigratorApi
204
- when 'mysql' then Api::MysqlApi
205
- else raise "Invalid MIGRATION_SOURCE: #{ENV['MIGRATION_SOURCE']}"
206
- end
207
-
208
- worker = MigrationWorker.new(api_class)
209
- worker.pull_orders(batch_size: 10_000)
210
- ensure
211
- worker && export_rejections(worker.rejections)
212
- export_failures
213
- end
214
- end
215
- end
216
- end
@@ -1,105 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative './exceptions'
4
-
5
- module Lab
6
- module Lims
7
- ##
8
- # LIMS' Data Transfer Object for orders
9
- class OrderDTO < ActiveSupport::HashWithIndifferentAccess
10
- include Utils
11
-
12
- ##
13
- # Unpacks a LIMS order into an object that OrdersService can handle
14
- def to_order_service_params(patient_id:)
15
- ActiveSupport::HashWithIndifferentAccess.new(
16
- program_id: lab_program.program_id,
17
- accession_number: self['tracking_number'],
18
- patient_id: patient_id,
19
- specimen: { concept_id: specimen_type_id },
20
- tests: self['tests']&.map { |test| { concept_id: test_type_id(test) } },
21
- requesting_clinician: requesting_clinician,
22
- date: start_date,
23
- target_lab: facility_name(self['receiving_facility']),
24
- order_location: facility_name(self['sending_facility']),
25
- reason_for_test_id: reason_for_test
26
- )
27
- end
28
-
29
- private
30
-
31
- # Translates a LIMS specimen name to an OpenMRS concept_id
32
- def specimen_type_id
33
- lims_specimen_name = self['sample_type']&.strip&.downcase
34
-
35
- if lims_specimen_name.nil? || %w[specimen_not_collected not_assigned not_specified].include?(lims_specimen_name)
36
- return ConceptName.select(:concept_id).find_by_name!('Unknown').concept_id
37
- end
38
-
39
- concept = Utils.find_concept_by_name(lims_specimen_name)
40
- return concept.concept_id if concept
41
-
42
- raise UnknownSpecimenType, "Unknown specimen name: #{lims_specimen_name}"
43
- end
44
-
45
- # Translates a LIMS test type name to an OpenMRS concept_id
46
- def test_type_id(lims_test_name)
47
- lims_test_name = Utils.translate_test_name(lims_test_name)
48
- concept = Utils.find_concept_by_name(lims_test_name)
49
- return concept.concept_id if concept
50
-
51
- raise UnknownTestType, "Unknown test type: #{lims_test_name}"
52
- end
53
-
54
- # Extract requesting clinician name from LIMS
55
- def requesting_clinician
56
- return 'Unknown' unless self['who_order_test']
57
-
58
- # TODO: Extend requesting clinician to an obs tree having extra parameters
59
- # like phone number and ID to closely match the lims user.
60
- first_name = self['who_order_test']['first_name'] || ''
61
- last_name = self['who_order_test']['last_name'] || ''
62
-
63
- if first_name.blank? && last_name.blank?
64
- logger.warn('Missing requesting clinician name')
65
- return ''
66
- end
67
-
68
- "#{first_name} #{last_name}"
69
- end
70
-
71
- def start_date
72
- raise LimsException, 'Order missing created date' if self['date_created'].blank?
73
-
74
- Utils.parse_date(self['date_created'])
75
- end
76
-
77
- # Parses a LIMS facility name
78
- def facility_name(lims_target_lab)
79
- return 'Unknown' if lims_target_lab == 'not_assigned'
80
-
81
- lims_target_lab
82
- end
83
-
84
- # Translates a LIMS sample priority to a concept_id
85
- def reason_for_test
86
- return nil unless self['priority']
87
-
88
- name = case self['priority']
89
- when %r{Reapet / Missing}i then 'Repeat / Missing'
90
- else self['priority']
91
- end
92
-
93
- ConceptName.find_by_name!(name).concept_id
94
- end
95
-
96
- def lab_program
97
- Program.find_by_name!(Lab::Metadata::LAB_PROGRAM_NAME)
98
- end
99
-
100
- def unknown_concept
101
- ConceptName.find_by_name!('Unknown')
102
- end
103
- end
104
- end
105
- end
@@ -1,216 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative './config'
4
- require_relative './order_dto'
5
- require_relative './utils'
6
-
7
- module Lab
8
- module Lims
9
- ##
10
- # Serializes a LabOrder into a LIMS OrderDTO.
11
- module OrderSerializer
12
- class << self
13
- include Utils
14
-
15
- def serialize_order(order)
16
- serialized_order = Lims::Utils.structify(Lab::LabOrderSerializer.serialize_order(order))
17
-
18
- Lims::OrderDTO.new(
19
- _id: Lab::LimsOrderMapping.find_by(order: order)&.lims_id || serialized_order.accession_number,
20
- tracking_number: serialized_order.accession_number,
21
- sending_facility: current_facility_name,
22
- receiving_facility: serialized_order.target_lab,
23
- tests: serialized_order.tests.map { |test| format_test_name(test.name) },
24
- patient: format_patient(serialized_order.patient_id),
25
- order_location: format_order_location(serialized_order.encounter_id),
26
- sample_type: format_sample_type(serialized_order.specimen.name),
27
- sample_status: format_sample_status(serialized_order.specimen.name),
28
- sample_statuses: format_sample_status_trail(order),
29
- test_statuses: format_test_status_trail(order),
30
- who_order_test: format_orderer(order),
31
- districy: current_district, # yes districy [sic]...
32
- priority: format_sample_priority(serialized_order.reason_for_test.name),
33
- date_created: serialized_order.order_date,
34
- test_results: format_test_results(serialized_order),
35
- type: 'Order'
36
- )
37
- end
38
-
39
- private
40
-
41
- def format_order_location(encounter_id)
42
- location_id = Encounter.select(:location_id).where(encounter_id: encounter_id)
43
- location = Location.select(:name)
44
- .where(location_id: location_id)
45
- .first
46
-
47
- location&.name
48
- end
49
-
50
- # Format patient into a structure that LIMS expects
51
- def format_patient(patient_id)
52
- person = Person.find(patient_id)
53
- name = PersonName.find_by_person_id(patient_id)
54
- national_id = PatientIdentifier.joins(:type)
55
- .merge(PatientIdentifierType.where(name: 'National ID'))
56
- .where(patient_id: patient_id)
57
- .first
58
- phone_number = PersonAttribute.joins(:type)
59
- .merge(PersonAttributeType.where(name: 'Cell phone Number'))
60
- .where(person_id: patient_id)
61
- .first
62
-
63
- {
64
- first_name: name&.given_name,
65
- last_name: name&.family_name,
66
- id: national_id&.identifier,
67
- phone_number: phone_number&.value || 'Unknown',
68
- gender: person.gender,
69
- email: nil
70
- }
71
- end
72
-
73
- def format_sample_type(name)
74
- return 'not_specified' if name.casecmp?('Unknown')
75
-
76
- return 'CSF' if name.casecmp?('Cerebrospinal Fluid')
77
-
78
- name.titleize
79
- end
80
-
81
- def format_sample_status(name)
82
- name.casecmp?('Unknown') ? 'specimen_not_collected' : 'specimen_collected'
83
- end
84
-
85
- def format_sample_status_trail(order)
86
- return [] if order.concept_id == ConceptName.find_by_name!('Unknown').concept_id
87
-
88
- user = User.find(order.discontinued_by || order.creator)
89
- drawn_by = PersonName.find_by_person_id(user.user_id)
90
- drawn_date = order.discontinued_date || order.start_date
91
-
92
- [
93
- drawn_date.strftime('%Y%m%d%H%M%S') => {
94
- 'status' => 'Drawn',
95
- 'updated_by' => {
96
- 'first_name' => drawn_by&.given_name || user.username,
97
- 'last_name' => drawn_by&.family_name,
98
- 'phone_number' => nil,
99
- 'id' => user.username
100
- }
101
- }
102
- ]
103
- end
104
-
105
- def format_test_status_trail(order)
106
- tests = order.voided.zero? ? order.tests : Lab::LabOrderSerializer.voided_tests(order)
107
-
108
- tests.each_with_object({}) do |test, trail|
109
- test_name = format_test_name(ConceptName.find_by_concept_id!(test.value_coded).name)
110
-
111
- current_test_trail = trail[test_name] = {}
112
-
113
- current_test_trail[test.obs_datetime.strftime('%Y%m%d%H%M%S')] = {
114
- status: 'Drawn',
115
- updated_by: find_user(test.creator)
116
- }
117
-
118
- unless test.voided.zero?
119
- current_test_trail[test.date_voided.strftime('%Y%m%d%H%M%S')] = {
120
- status: 'Voided',
121
- updated_by: find_user(test.voided_by)
122
- }
123
- end
124
-
125
- next unless test.result
126
-
127
- current_test_trail[test.obs_datetime.strftime('%Y%m%d%H%M%S')] = {
128
- status: 'Verified',
129
- updated_by: find_user(test.result.creator)
130
- }
131
- end
132
- end
133
-
134
- def format_orderer(order)
135
- find_user(order.creator)
136
- end
137
-
138
- def format_test_results(order)
139
- order.tests&.each_with_object({}) do |test, results|
140
- next if test.result.nil? || test.result.empty?
141
-
142
- test_creator = User.find(Observation.find(test.result.first.id).creator)
143
- test_creator_name = PersonName.find_by_person_id(test_creator.person_id)
144
-
145
- results[format_test_name(test.name)] = {
146
- results: test.result.each_with_object({}) do |measure, measures|
147
- measures[format_test_name(measure.indicator.name)] = {
148
- result_value: "#{measure.value_modifier}#{measure.value}"
149
- }
150
- end,
151
- result_date: test.result.first&.date,
152
- result_entered_by: {
153
- first_name: test_creator_name&.given_name,
154
- last_name: test_creator_name&.family_name,
155
- id: test_creator.username
156
- }
157
- }
158
- end
159
- end
160
-
161
- def format_test_name(test_name)
162
- return 'Viral Load' if test_name.casecmp?('HIV Viral load')
163
-
164
- return 'TB' if test_name.casecmp?('TB Program')
165
-
166
- test_name.titleize
167
- end
168
-
169
- def format_sample_priority(priority)
170
- return 'Routine' if priority&.casecmp?('Medical examination, routine')
171
-
172
- priority&.titleize
173
- end
174
-
175
- def current_health_center
176
- health_center = Location.current_health_center
177
- raise 'Current health center not set' unless health_center
178
-
179
- health_center
180
- end
181
-
182
- def current_district
183
- district = current_health_center.city_village\
184
- || current_health_center.parent&.name\
185
- || GlobalProperty.find_by_property('current_health_center_district')&.property_value
186
-
187
- return district if district
188
-
189
- GlobalProperty.create(property: 'current_health_center_district',
190
- property_value: Lims::Config.application['district'],
191
- uuid: SecureRandom.uuid)
192
-
193
- Config.application['district']
194
- end
195
-
196
- def current_facility_name
197
- current_health_center.name
198
- end
199
-
200
- def find_user(user_id)
201
- user = User.find(user_id)
202
- person_name = PersonName.find_by(person_id: user.person_id)
203
- phone_number = PersonAttribute.find_by(type: PersonAttributeType.where(name: 'Cell phone number'),
204
- person_id: user.person_id)
205
-
206
- {
207
- first_name: person_name&.given_name,
208
- last_name: person_name&.family_name,
209
- phone_number: phone_number&.value,
210
- id: user.username
211
- }
212
- end
213
- end
214
- end
215
- end
216
- end