pvdgm_services 0.1.0
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.
- checksums.yaml +7 -0
- data/README.md +29 -0
- data/Rakefile +40 -0
- data/app/contexts/services/accept_hl7_message_context.rb +32 -0
- data/app/contexts/services/application_mds_context.rb +21 -0
- data/app/contexts/services/complete_sltc_registration_process_context.rb +23 -0
- data/app/contexts/services/create_account_mapping_context.rb +23 -0
- data/app/contexts/services/create_configured_account_context.rb +26 -0
- data/app/contexts/services/create_configured_facility_context.rb +26 -0
- data/app/contexts/services/create_credential_context.rb +23 -0
- data/app/contexts/services/create_facility_mapping_context.rb +23 -0
- data/app/contexts/services/create_public_key_context.rb +22 -0
- data/app/contexts/services/create_service_context.rb +22 -0
- data/app/contexts/services/create_service_definition_context.rb +26 -0
- data/app/contexts/services/create_sltc_registration_context.rb +24 -0
- data/app/contexts/services/create_third_party_context.rb +22 -0
- data/app/contexts/services/hl7_message_trimming_context.rb +19 -0
- data/app/contexts/services/invoke_service_context.rb +21 -0
- data/app/contexts/services/list_sltc_providers_context.rb +22 -0
- data/app/contexts/services/mds_file_processing_context.rb +27 -0
- data/app/contexts/services/mds_file_upload_context.rb +25 -0
- data/app/contexts/services/mds_pull_context.rb +21 -0
- data/app/contexts/services/monthly_service_table_cleanup_context.rb +19 -0
- data/app/contexts/services/notify_new_registration_context.rb +22 -0
- data/app/contexts/services/notify_sltc_provider_change_context.rb +19 -0
- data/app/contexts/services/request_sltc_baseline_context.rb +23 -0
- data/app/contexts/services/update_account_mapping_context.rb +23 -0
- data/app/contexts/services/update_configured_account_context.rb +26 -0
- data/app/contexts/services/update_configured_facility_context.rb +26 -0
- data/app/contexts/services/update_credential_context.rb +23 -0
- data/app/contexts/services/update_facility_mapping_context.rb +23 -0
- data/app/contexts/services/update_public_key_context.rb +23 -0
- data/app/contexts/services/update_service_context.rb +23 -0
- data/app/contexts/services/update_service_definition_context.rb +26 -0
- data/app/contexts/services/update_third_party_context.rb +23 -0
- data/app/controllers/services/account_mappings_controller.rb +54 -0
- data/app/controllers/services/adts_controller.rb +35 -0
- data/app/controllers/services/application_controller.rb +17 -0
- data/app/controllers/services/assessment_requests_controller.rb +48 -0
- data/app/controllers/services/available_files_controller.rb +19 -0
- data/app/controllers/services/configured_account_base_controller.rb +17 -0
- data/app/controllers/services/configured_accounts_controller.rb +59 -0
- data/app/controllers/services/configured_facilities_controller.rb +65 -0
- data/app/controllers/services/facility_mappings_controller.rb +58 -0
- data/app/controllers/services/mds_files_controller.rb +75 -0
- data/app/controllers/services/mds_pull_accounts_controller.rb +57 -0
- data/app/controllers/services/public_keys_controller.rb +50 -0
- data/app/controllers/services/service_base_controller.rb +18 -0
- data/app/controllers/services/service_definition_base_controller.rb +31 -0
- data/app/controllers/services/service_definitions_controller.rb +59 -0
- data/app/controllers/services/services_controller.rb +56 -0
- data/app/controllers/services/sltc_providers_controller.rb +15 -0
- data/app/controllers/services/sltc_registrations_controller.rb +74 -0
- data/app/controllers/services/status_masking.rb +17 -0
- data/app/controllers/services/third_parties_controller.rb +47 -0
- data/app/controllers/services/third_party_base_controller.rb +17 -0
- data/app/controllers/services/validation_controller.rb +17 -0
- data/app/helpers/services/application_helper.rb +8 -0
- data/app/helpers/services/assessment_request_helper.rb +19 -0
- data/app/helpers/services/available_files_helper.rb +27 -0
- data/app/helpers/services/mds_pull_accounts_helper.rb +27 -0
- data/app/helpers/services/sltc_registrations_helper.rb +15 -0
- data/app/mailers/services_mailer.rb +57 -0
- data/app/models/services/abaqis_mds_push.rb +51 -0
- data/app/models/services/account_mapping.rb +10 -0
- data/app/models/services/application_api.rb +19 -0
- data/app/models/services/assessment_request.rb +18 -0
- data/app/models/services/available_file.rb +73 -0
- data/app/models/services/configured_account.rb +26 -0
- data/app/models/services/configured_facility.rb +16 -0
- data/app/models/services/credential.rb +9 -0
- data/app/models/services/facility_mapping.rb +12 -0
- data/app/models/services/ftp_server.rb +69 -0
- data/app/models/services/hl7_inbound_service.rb +47 -0
- data/app/models/services/hl7_message.rb +27 -0
- data/app/models/services/isc_code.rb +15 -0
- data/app/models/services/isc_code_lookup.rb +75 -0
- data/app/models/services/mds_assessment.rb +371 -0
- data/app/models/services/mds_content.rb +55 -0
- data/app/models/services/mds_pull.rb +41 -0
- data/app/models/services/mds_pull_account.rb +192 -0
- data/app/models/services/mds_push.rb +24 -0
- data/app/models/services/mds_upload.rb +64 -0
- data/app/models/services/mds_upload_content.rb +148 -0
- data/app/models/services/mds_ws_response.rb +21 -0
- data/app/models/services/mds_ws_response_handler.rb +31 -0
- data/app/models/services/pcc_mds_pull.rb +77 -0
- data/app/models/services/provider_change.rb +26 -0
- data/app/models/services/public_key.rb +11 -0
- data/app/models/services/service.rb +20 -0
- data/app/models/services/service_definition.rb +37 -0
- data/app/models/services/service_implementation.rb +29 -0
- data/app/models/services/sltc_api.rb +179 -0
- data/app/models/services/sltc_api_exception.rb +54 -0
- data/app/models/services/sltc_mds_pull.rb +230 -0
- data/app/models/services/sltc_registration.rb +23 -0
- data/app/models/services/third_party.rb +18 -0
- data/app/roles/services/account_mapping_creator.rb +13 -0
- data/app/roles/services/account_mapping_updator.rb +12 -0
- data/app/roles/services/configured_account_creator.rb +13 -0
- data/app/roles/services/configured_account_updater.rb +12 -0
- data/app/roles/services/configured_facility_creator.rb +13 -0
- data/app/roles/services/configured_facility_updater.rb +12 -0
- data/app/roles/services/credential_creator.rb +11 -0
- data/app/roles/services/credential_updater.rb +20 -0
- data/app/roles/services/facility_mapping_creator.rb +13 -0
- data/app/roles/services/facility_mapping_updater.rb +11 -0
- data/app/roles/services/hl7_adt_message_saver.rb +15 -0
- data/app/roles/services/hl7_message_trimmer.rb +14 -0
- data/app/roles/services/mds_assessment_categorizer.rb +71 -0
- data/app/roles/services/mds_file_processor.rb +86 -0
- data/app/roles/services/mds_notifications.rb +44 -0
- data/app/roles/services/monthly_service_table_cleaner.rb +19 -0
- data/app/roles/services/new_registration_notifier.rb +11 -0
- data/app/roles/services/public_key_creator.rb +14 -0
- data/app/roles/services/public_key_updater.rb +12 -0
- data/app/roles/services/service_creator.rb +13 -0
- data/app/roles/services/service_definition_creator.rb +13 -0
- data/app/roles/services/service_definition_updater.rb +12 -0
- data/app/roles/services/service_invoker.rb +25 -0
- data/app/roles/services/service_updater.rb +11 -0
- data/app/roles/services/sltc_baseline_requestor.rb +19 -0
- data/app/roles/services/sltc_provider_change_notifier.rb +25 -0
- data/app/roles/services/sltc_provider_lister.rb +13 -0
- data/app/roles/services/sltc_registration_completer.rb +81 -0
- data/app/roles/services/sltc_registration_saver.rb +13 -0
- data/app/roles/services/submit_mds_file_for_processing.rb +28 -0
- data/app/roles/services/third_party_creator.rb +13 -0
- data/app/roles/services/third_party_updater.rb +12 -0
- data/app/utils/services/file_upload_handler.rb +33 -0
- data/app/utils/services/mds_upload_filters.rb +26 -0
- data/app/utils/services/mds_xml_file_parser.rb +104 -0
- data/app/utils/services/upload_file.rb +13 -0
- data/app/validators/isc_code_validator.rb +42 -0
- data/app/validators/mds_birthdate_validator.rb +37 -0
- data/app/validators/mds_date_validator.rb +8 -0
- data/app/validators/mds_integer_validator.rb +15 -0
- data/app/validators/mds_version_validator.rb +16 -0
- data/app/validators/state_code_validator.rb +9 -0
- data/app/views/layouts/services/application.html.erb +14 -0
- data/app/views/services/account_mappings/index.json.jbuilder +9 -0
- data/app/views/services/account_mappings/show.json.jbuilder +7 -0
- data/app/views/services/assessment_requests/index.json.jbuilder +11 -0
- data/app/views/services/available_files/index.json.jbuilder +13 -0
- data/app/views/services/configured_accounts/index.json.jbuilder +13 -0
- data/app/views/services/configured_accounts/show.json.jbuilder +11 -0
- data/app/views/services/configured_facilities/index.json.jbuilder +13 -0
- data/app/views/services/configured_facilities/show.json.jbuilder +11 -0
- data/app/views/services/facility_mappings/index.json.jbuilder +10 -0
- data/app/views/services/facility_mappings/show.json.jbuilder +8 -0
- data/app/views/services/mds_files/create.builder +10 -0
- data/app/views/services/mds_pull_accounts/index.json.jbuilder +11 -0
- data/app/views/services/public_keys/index.json.jbuilder +8 -0
- data/app/views/services/public_keys/show.json.jbuilder +6 -0
- data/app/views/services/service_definitions/index.json.jbuilder +17 -0
- data/app/views/services/service_definitions/show.json.jbuilder +16 -0
- data/app/views/services/services/index.json.jbuilder +6 -0
- data/app/views/services/services/show.json.jbuilder +4 -0
- data/app/views/services/sltc_providers/index.json.jbuilder +10 -0
- data/app/views/services/sltc_registrations/index.json.jbuilder +10 -0
- data/app/views/services/third_parties/index.json.jbuilder +7 -0
- data/app/views/services/third_parties/show.json.jbuilder +5 -0
- data/app/views/services_mailer/burying_job.html.erb +20 -0
- data/app/views/services_mailer/delaying_job.html.erb +20 -0
- data/app/views/services_mailer/notify_sltc_client_registration.html.erb +39 -0
- data/app/views/services_mailer/notify_sltc_client_registration_invalid_request_type.html.erb +8 -0
- data/app/views/services_mailer/notify_sltc_client_registration_request_invalid.html.erb +10 -0
- data/app/views/services_mailer/notify_sltc_client_registration_validation_errors.html.erb +13 -0
- data/app/views/services_mailer/notify_sltc_provider_changes.html.erb +26 -0
- data/app/views/services_mailer/notify_support_about_no_mds_pull.html.erb +6 -0
- data/config/routes.rb +48 -0
- data/db/migrate/20140102000000_create_services_engine_tables.rb +131 -0
- data/db/migrate/20140517184450_new_services_columns.rb +24 -0
- data/db/migrate/20140525142842_new_configured_provider_table.rb +18 -0
- data/db/migrate/20140714172442_add_error_column_to_hl7_messages.rb +5 -0
- data/db/migrate/20140730164152_mds_upload_tables.rb +78 -0
- data/db/sql_data/service_data_setup.sql +24 -0
- data/lib/services/engine.rb +40 -0
- data/lib/services/version.rb +3 -0
- data/lib/services.rb +4 -0
- data/lib/tasks/services_tasks.rake +40 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +15 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config/application.rb +60 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +29 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +35 -0
- data/spec/dummy/config/environments/production.rb +68 -0
- data/spec/dummy/config/environments/test.rb +32 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/email.rb +1 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +4 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/schema.rb +237 -0
- data/spec/dummy/db/sql_data/services_isc_codes.sql +878 -0
- data/spec/dummy/log/test.log +3498 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/models/abaqis_mds_push_spec.rb +120 -0
- data/spec/models/available_file_spec.rb +234 -0
- data/spec/models/configured_account_spec.rb +39 -0
- data/spec/models/ftp_server_spec.rb +221 -0
- data/spec/models/isc_code_lookup_spec.rb +125 -0
- data/spec/models/isc_code_spec.rb +5 -0
- data/spec/models/mds_assessment_spec.rb +1070 -0
- data/spec/models/mds_pull_account_spec.rb +468 -0
- data/spec/models/mds_pull_spec.rb +48 -0
- data/spec/models/mds_push_spec.rb +43 -0
- data/spec/models/mds_ws_response_spec.rb +54 -0
- data/spec/models/pcc_mds_pull_spec.rb +273 -0
- data/spec/models/service_implementation_spec.rb +88 -0
- data/spec/models/sltc_api_exception_spec.rb +136 -0
- data/spec/models/sltc_api_spec.rb +192 -0
- data/spec/models/sltc_mds_pull_spec.rb +776 -0
- data/spec/roles/account_mapping_creator_spec.rb +40 -0
- data/spec/roles/account_mapping_updator_spec.rb +16 -0
- data/spec/roles/configured_account_creator_spec.rb +40 -0
- data/spec/roles/configured_account_updater_spec.rb +16 -0
- data/spec/roles/configured_facility_creator_spec.rb +40 -0
- data/spec/roles/configured_facility_updater_spec.rb +16 -0
- data/spec/roles/credential_creator_spec.rb +23 -0
- data/spec/roles/credential_updater_spec.rb +38 -0
- data/spec/roles/facility_mapping_creator_spec.rb +40 -0
- data/spec/roles/facility_mapping_updater_spec.rb +16 -0
- data/spec/roles/hl7_adt_message_saver_spec.rb +35 -0
- data/spec/roles/hl7_message_trimmer_spec.rb +31 -0
- data/spec/roles/monthly_service_table_cleaner_spec.rb +27 -0
- data/spec/roles/new_registration_notifier_spec.rb +18 -0
- data/spec/roles/service_ceator_spec.rb +34 -0
- data/spec/roles/service_definition_creator_spec.rb +40 -0
- data/spec/roles/service_definition_updater_spec.rb +16 -0
- data/spec/roles/service_invoker_spec.rb +22 -0
- data/spec/roles/service_updater_spec.rb +17 -0
- data/spec/roles/sltc_baseline_requestor_spec.rb +30 -0
- data/spec/roles/sltc_provider_lister_spec.rb +27 -0
- data/spec/roles/sltc_registration_completer_spec.rb +187 -0
- data/spec/roles/sltc_registration_saver_spec.rb +34 -0
- data/spec/roles/third_party_creator_spec.rb +34 -0
- data/spec/roles/third_party_updater_spec.rb +17 -0
- data/spec/spec_helper.rb +72 -0
- metadata +581 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
module Services
|
|
2
|
+
|
|
3
|
+
class MdsUpload < ActiveRecord::Base
|
|
4
|
+
include MdsUploadContent
|
|
5
|
+
include MdsNotifications
|
|
6
|
+
|
|
7
|
+
NEW = 0
|
|
8
|
+
ASSESSMENTS_LOADED = 1
|
|
9
|
+
COMPLETE = 2
|
|
10
|
+
ERROR = 3
|
|
11
|
+
|
|
12
|
+
TIME_TO_RUN = 36000
|
|
13
|
+
PRIORITY = 500
|
|
14
|
+
ASYNC_SEND_OPTIONS = { :priority => PRIORITY, :time_to_run => TIME_TO_RUN }
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
belongs_to :account
|
|
18
|
+
belongs_to :upload_user, class_name: "User"
|
|
19
|
+
has_many :mds_assessments
|
|
20
|
+
has_many :facilities, through: :mds_assessments
|
|
21
|
+
has_many :residents, through: :mds_assessments
|
|
22
|
+
has_one :mds_content, as: :uploadable
|
|
23
|
+
|
|
24
|
+
serialize :file_errors, JSON
|
|
25
|
+
|
|
26
|
+
scope :by_upload_user, ->(user_id) { where("upload_user_id = ?", user_id ) }
|
|
27
|
+
scope :completed, -> { where(status: COMPLETE) }
|
|
28
|
+
|
|
29
|
+
mds_upload_content :mds_file, max_size: { size: 75.megabytes,
|
|
30
|
+
message: "MDS Files need to be less than 75 MB in size." },
|
|
31
|
+
presence: { message: "Please select an MDS file for upload." },
|
|
32
|
+
name_match: { regex: /\.zip$/i,
|
|
33
|
+
message: "Uploaded file must have a .zip extension." },
|
|
34
|
+
file_type: { regex: /zip/i,
|
|
35
|
+
message: "Uploaded file must be a ZIP file" }
|
|
36
|
+
|
|
37
|
+
before_create { self.file_errors ||= {} }
|
|
38
|
+
after_save :handle_mds_notifications, :if => :ready_for_notification?
|
|
39
|
+
|
|
40
|
+
def add_file_errors(file_name, error)
|
|
41
|
+
self.file_errors ||= {}
|
|
42
|
+
error_array = error.is_a?(Array) ? error : [ error ]
|
|
43
|
+
if self.file_errors[file_name].present?
|
|
44
|
+
self.file_errors[file_name].concat(error_array)
|
|
45
|
+
else
|
|
46
|
+
self.file_errors[file_name] = error_array
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def has_errors?
|
|
51
|
+
status == COMPLETE && (fatal_error.present? || file_errors.size > num_duplicated)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def successful?
|
|
55
|
+
status == COMPLETE
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def ready_for_notification?
|
|
59
|
+
status == COMPLETE
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
module Services
|
|
2
|
+
|
|
3
|
+
module MdsUploadContent
|
|
4
|
+
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.extend ClassMethods
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
|
|
11
|
+
#
|
|
12
|
+
# Specify that there is a file attachment associated with this model.
|
|
13
|
+
#
|
|
14
|
+
# attr_name - the name of the parameter in the request that contains the file upload.
|
|
15
|
+
# args - a hash of options to control operations. Valid options include:
|
|
16
|
+
#
|
|
17
|
+
# :max_size => { :size => size-in-bytes,
|
|
18
|
+
# :message => 'Error message if size exceeded' }
|
|
19
|
+
# If :size is not specified, a default of 5.megabytes is used
|
|
20
|
+
# If :message is not specified, a default message of "uploaded file exceedes maximum of #{args[:max_size][:size] bytes" is used
|
|
21
|
+
#
|
|
22
|
+
# :presence => { :message => 'Error message if upload not provided.' }
|
|
23
|
+
# If :message is not specified, a default message of "no file upload provided"
|
|
24
|
+
#
|
|
25
|
+
# :name_match => { :regex => regular expression for file name to match,
|
|
26
|
+
# :message => 'Error message if upload not provided.' }
|
|
27
|
+
# If :regex is not specified, all names will match.
|
|
28
|
+
# If :message is not specified, a default message of "uploaded file name does not match regular expression".
|
|
29
|
+
#
|
|
30
|
+
# :file_type => { :message => 'Error message if no match on file type',
|
|
31
|
+
# :regex => regular expression for the results of 'file -b --mime' to match }
|
|
32
|
+
# If :message is not specified, a default message of "unexpected upload file type"
|
|
33
|
+
# If :regex is not specified, then there will be no check of the file type.
|
|
34
|
+
#
|
|
35
|
+
def mds_upload_content(attr_name, args={})
|
|
36
|
+
|
|
37
|
+
# Define the setter method for the upload file.
|
|
38
|
+
define_method("#{attr_name}=".to_sym) do | upload_file |
|
|
39
|
+
return if upload_file.nil?
|
|
40
|
+
self.original_file_name = File.basename(upload_file.original_filename)
|
|
41
|
+
self.content_type = upload_file.content_type
|
|
42
|
+
self.size = upload_file.size
|
|
43
|
+
@upload_file = upload_file
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
define_method("clear_#{attr_name}".to_sym) do
|
|
47
|
+
@upload_file = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
define_max_size_validation(attr_name, args[:max_size]) if args.has_key?(:max_size)
|
|
51
|
+
|
|
52
|
+
define_presence_validation(attr_name, args[:presence]) if args.has_key?(:presence)
|
|
53
|
+
|
|
54
|
+
define_name_match_validation(attr_name, args[:name_match]) if args.has_key?(:name_match)
|
|
55
|
+
|
|
56
|
+
define_file_type_validation(attr_name, args[:file_type]) if args.has_key?(:file_type)
|
|
57
|
+
|
|
58
|
+
before_create :construct_file_contents
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def define_max_size_validation(attr_name, max_size_args)
|
|
64
|
+
max_size_args ||= {}
|
|
65
|
+
max_size_args[:size] ||= 5.megabytes
|
|
66
|
+
max_size_args[:message] ||= "uploaded file exceedes maximum of #{max_size_args[:size]} bytes"
|
|
67
|
+
|
|
68
|
+
# set up the validator
|
|
69
|
+
validate :attachment_size, :on => :create
|
|
70
|
+
|
|
71
|
+
define_method(:attachment_size) do
|
|
72
|
+
return if self.size.nil? # If the size isn't set, what's the point?
|
|
73
|
+
errors.add(attr_name, max_size_args[:message]) if self.size > max_size_args[:size]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def define_presence_validation(attr_name, presence_args)
|
|
78
|
+
presence_args ||= {}
|
|
79
|
+
presence_args[:message] ||= "no file upload provided"
|
|
80
|
+
|
|
81
|
+
# Set up the validator
|
|
82
|
+
validate :attachment_presence, :on => :create
|
|
83
|
+
|
|
84
|
+
define_method(:attachment_presence) do
|
|
85
|
+
errors.add(attr_name, presence_args[:message]) if @upload_file.nil?
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def define_name_match_validation(attr_name, name_match_args)
|
|
90
|
+
name_match_args ||= {}
|
|
91
|
+
name_match_args[:regex] ||= nil
|
|
92
|
+
name_match_args[:message] ||= "uploaded file name does not match regular expression"
|
|
93
|
+
|
|
94
|
+
# Set up the validator
|
|
95
|
+
validate :attachment_name_match, :on => :create
|
|
96
|
+
|
|
97
|
+
define_method(:attachment_name_match) do
|
|
98
|
+
return if self.original_file_name.blank?
|
|
99
|
+
errors.add(attr_name, name_match_args[:message]) if name_match_args[:regex].present? && !(self.original_file_name =~ name_match_args[:regex])
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def define_file_type_validation(attr_name, file_type_args)
|
|
104
|
+
file_type_args ||= {}
|
|
105
|
+
file_type_args[:regex] ||= nil
|
|
106
|
+
file_type_args[:message] ||= "unexpected upload file type"
|
|
107
|
+
|
|
108
|
+
# Set up the validator
|
|
109
|
+
validate :file_type_match, :on => :create
|
|
110
|
+
|
|
111
|
+
define_method(:file_type_match) do
|
|
112
|
+
return if @upload_file.nil? || file_type_args[:regex].nil?
|
|
113
|
+
mime_type = mime_file_type(@upload_file.path)
|
|
114
|
+
errors.add(attr_name, file_type_args[:message]) if mime_type.present? && !(mime_type =~ file_type_args[:regex])
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
end # End of ClassMethods
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
#
|
|
123
|
+
# This is the implementation of a before_create method. It's responsibility is to
|
|
124
|
+
# set the content attribute of the model to the contents of the uploaded file.
|
|
125
|
+
# This method has effect only in the case of saving a new record.
|
|
126
|
+
#
|
|
127
|
+
def construct_file_contents
|
|
128
|
+
return if @upload_file.nil?
|
|
129
|
+
|
|
130
|
+
# Construct and populate the content record
|
|
131
|
+
content_record = content_class.new
|
|
132
|
+
content_record.content = @upload_file.read
|
|
133
|
+
|
|
134
|
+
# Make the association
|
|
135
|
+
self.send(content_assn_assignment, content_record)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def content_class() MdsContent end
|
|
139
|
+
def content_assn_assignment() "mds_content=".to_sym end
|
|
140
|
+
|
|
141
|
+
def mime_file_type(path)
|
|
142
|
+
raise "Unable to find upload file at: #{path}" if !File.exists?(path)
|
|
143
|
+
`file -b --mime #{path}`
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Services
|
|
2
|
+
|
|
3
|
+
class MdsWsResponse
|
|
4
|
+
|
|
5
|
+
STATUS_ERROR = "ERROR"
|
|
6
|
+
STATUS_SUCCESS = "SUCCESS"
|
|
7
|
+
|
|
8
|
+
attr_accessor :status, :error_message, :filename, :acknowledgement_id, :submission_datetime
|
|
9
|
+
|
|
10
|
+
def self.parse_xml_response(response_xml)
|
|
11
|
+
handler = MdsWsResponseHandler.new
|
|
12
|
+
parser = LibXML::XML::SaxParser.string(response_xml)
|
|
13
|
+
parser.callbacks = handler
|
|
14
|
+
parser.parse
|
|
15
|
+
|
|
16
|
+
handler.response
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require 'libxml'
|
|
2
|
+
|
|
3
|
+
module Services
|
|
4
|
+
|
|
5
|
+
class MdsWsResponseHandler
|
|
6
|
+
include LibXML::XML::SaxParser::Callbacks
|
|
7
|
+
|
|
8
|
+
COMPOSITE_FILE_RESPONSE = 'composite_file_response'
|
|
9
|
+
RESPONSE_NAME = 'Response'
|
|
10
|
+
|
|
11
|
+
attr_reader :response
|
|
12
|
+
|
|
13
|
+
def on_start_element(element_name, attributes)
|
|
14
|
+
@current_reader = element_name.to_sym
|
|
15
|
+
@current_writer = "#{element_name}=".to_sym
|
|
16
|
+
|
|
17
|
+
if element_name == COMPOSITE_FILE_RESPONSE
|
|
18
|
+
@response = MdsWsResponse.new
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def on_characters(text)
|
|
23
|
+
return unless @response.respond_to?(@current_reader)
|
|
24
|
+
|
|
25
|
+
current_value = @response.send(@current_reader)
|
|
26
|
+
@response.send(@current_writer, current_value ? current_value + text.strip : text.strip)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module Services
|
|
2
|
+
|
|
3
|
+
class PccMdsPull < MdsPull
|
|
4
|
+
|
|
5
|
+
#
|
|
6
|
+
# Factory method to return one and only one instance of the ftp server
|
|
7
|
+
# class for an instance of the PccMdsPull class.
|
|
8
|
+
#
|
|
9
|
+
def ftp_server
|
|
10
|
+
# Create a new FtpServer instance only if necessary
|
|
11
|
+
@ftp_server ||= FtpServer.new(service_definition.hostname,
|
|
12
|
+
service_definition.username,
|
|
13
|
+
server_credentials.password,
|
|
14
|
+
service_definition.base_uri)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
#
|
|
18
|
+
# Overridden from MdsPull.
|
|
19
|
+
#
|
|
20
|
+
def list_available_files(configured_account)
|
|
21
|
+
mapped_account = account_map(configured_account.account_id)
|
|
22
|
+
|
|
23
|
+
files = []
|
|
24
|
+
ftp_server.connect do | server |
|
|
25
|
+
files = server.file_list mapped_account.account_code, '*.zip'
|
|
26
|
+
end
|
|
27
|
+
files.map { | fn | [ fn, fn ] }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
#
|
|
31
|
+
# Overridden from MdsPull.
|
|
32
|
+
#
|
|
33
|
+
def perform_file_downloads(configured_account, files_to_download)
|
|
34
|
+
mapped_account = account_map(configured_account.account_id)
|
|
35
|
+
|
|
36
|
+
downloaded_files = []
|
|
37
|
+
ftp_server.connect do | server |
|
|
38
|
+
files_to_download.each do | available_file |
|
|
39
|
+
downloaded_files << download_file(server, available_file, mapped_account.account_code)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
downloaded_files.compact # The compact is here because there could be nil's in the list
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
#
|
|
46
|
+
# Overridden from MdsPull
|
|
47
|
+
#
|
|
48
|
+
def perform_cleanup_on_remote_server(configured_account)
|
|
49
|
+
mapped_account = account_map(configured_account.account_id)
|
|
50
|
+
|
|
51
|
+
cleared_files = []
|
|
52
|
+
ftp_server.connect do | server |
|
|
53
|
+
configured_account.available_files.deleted.each do | available_file |
|
|
54
|
+
cleared_files << remove_file(server, available_file, mapped_account.account_code)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
cleared_files.compact
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def download_file(server, available_file, account_directory)
|
|
61
|
+
server.download_file account_directory, available_file.request_id, available_file.download_path
|
|
62
|
+
available_file.id
|
|
63
|
+
rescue Exception => ex
|
|
64
|
+
Rails.logger.error "Error downloading file: #{available_file.to_rrepr}: #{ex.message}\n#{ex.backtrace.join("\n")}"
|
|
65
|
+
nil # Return nil; we failed to download the specified file and 'nil' is the correct response
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def remove_file(server, available_file, account_directory)
|
|
69
|
+
server.remove_file account_directory, available_file.request_id
|
|
70
|
+
available_file.id
|
|
71
|
+
rescue Exception => ex
|
|
72
|
+
Rails.logger.error "Error removing server file: #{available_file.to_rrepr}: #{ex.message}\n#{ex.backtrace.join("\n")}"
|
|
73
|
+
nil # Return nil; we failed to remove the specified file and 'nil' is the correct response
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Services
|
|
2
|
+
|
|
3
|
+
class ProviderChange < ActiveRecord::Base
|
|
4
|
+
belongs_to :configured_account
|
|
5
|
+
|
|
6
|
+
VERB_ADD = 1
|
|
7
|
+
VERB_DELETE = 2
|
|
8
|
+
|
|
9
|
+
STATUS_NEW = 0
|
|
10
|
+
STATUS_REPORTED = 1
|
|
11
|
+
|
|
12
|
+
validates :configured_account_id, presence: true
|
|
13
|
+
validates :facility_code, presence: true, length: { maximum: 255 }
|
|
14
|
+
validates :name, presence: true, length: { maximum: 255 }
|
|
15
|
+
validates :ccn, presence: true, length: { maximum: 255 }
|
|
16
|
+
validates :npi, length: { maximum: 255 }
|
|
17
|
+
validates :fac_id, length: { maximum: 16 }
|
|
18
|
+
validates :state, length: { maximum: 15 }
|
|
19
|
+
validates :verb, presence: true
|
|
20
|
+
|
|
21
|
+
scope :added_providers, ->(ca_id, facility_codes) { where(configured_account_id: ca_id, facility_code: facility_codes, verb: VERB_ADD).order("facility_code ASC")}
|
|
22
|
+
scope :deleted_providers, ->(ca_id, facility_codes) { where(configured_account_id: ca_id, facility_code: facility_codes, verb: VERB_DELETE).order("facility_code ASC")}
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Services
|
|
2
|
+
|
|
3
|
+
class PublicKey < ActiveRecord::Base
|
|
4
|
+
|
|
5
|
+
scope :active, -> { where("valid_until IS NULL OR valid_until >= NOW()") }
|
|
6
|
+
|
|
7
|
+
validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
|
|
8
|
+
validates :key, presence: true
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Services
|
|
2
|
+
|
|
3
|
+
class Service < ActiveRecord::Base
|
|
4
|
+
has_many :service_definitions, dependent: :destroy
|
|
5
|
+
|
|
6
|
+
#
|
|
7
|
+
# The MDS Pull service is specified by the DB ID. This is the only one
|
|
8
|
+
# we can predict this way. In production, the IDs of the other records
|
|
9
|
+
# will be unpredictable
|
|
10
|
+
#
|
|
11
|
+
MDS_PULL = 'mds_pull'
|
|
12
|
+
MDS_PUSH = "mds_push"
|
|
13
|
+
HL7_INBOUND = "hl7_inbound"
|
|
14
|
+
|
|
15
|
+
validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
|
|
16
|
+
validates :key, presence: true, uniqueness: true, length: { maximum: 255 }
|
|
17
|
+
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Services
|
|
2
|
+
|
|
3
|
+
class ServiceDefinition < ActiveRecord::Base
|
|
4
|
+
belongs_to :third_party
|
|
5
|
+
belongs_to :service
|
|
6
|
+
|
|
7
|
+
has_many :configured_accounts, dependent: :destroy
|
|
8
|
+
has_many :configured_facilities, dependent: :destroy
|
|
9
|
+
has_many :credentials, as: :credentialled, dependent: :destroy
|
|
10
|
+
|
|
11
|
+
validates :hostname, length: { minimum: 0, maximum: 255 }, allow_nil: true, allow_blank: true
|
|
12
|
+
validates :base_uri, length: { minimum: 0, maximum: 255 }, allow_nil: true, allow_blank: true
|
|
13
|
+
validates :username, length: { minimum: 0, maximum: 255 }, allow_nil: true, allow_blank: true
|
|
14
|
+
validates :service_class, presence: true, length: { maximum: 255 }
|
|
15
|
+
|
|
16
|
+
def invoke
|
|
17
|
+
service_implementation.invoke
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def service_implementation
|
|
21
|
+
clazz = eval(service_class)
|
|
22
|
+
clazz.new(self)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def hostname_with_port
|
|
26
|
+
hwp = hostname
|
|
27
|
+
hwp << ":#{port}" if self.port.present?
|
|
28
|
+
hwp
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def server_base_url
|
|
32
|
+
ERB.new(base_uri).result(binding)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Services
|
|
2
|
+
|
|
3
|
+
class ServiceImplementation
|
|
4
|
+
|
|
5
|
+
attr_reader :service_definition
|
|
6
|
+
|
|
7
|
+
def initialize(srv_def)
|
|
8
|
+
@service_definition = srv_def
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
#
|
|
12
|
+
# Returns the credentials assocated with this service definition's server
|
|
13
|
+
#
|
|
14
|
+
def server_credentials
|
|
15
|
+
(@credentials ||= service_definition.credentials.first).tap do | cred |
|
|
16
|
+
raise "No credentials configured for server: (#{service_definition.to_rrepr})" if cred.blank?
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def account_map(account_id)
|
|
21
|
+
AccountMapping.where(account_id: account_id,
|
|
22
|
+
third_party_id: service_definition.third_party_id).first.tap do |am|
|
|
23
|
+
raise "No account mapping associated with #{service_definition.to_rrepr} and account_id = #{account_id}" if am.blank?
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
module Services
|
|
2
|
+
|
|
3
|
+
class SltcApi
|
|
4
|
+
|
|
5
|
+
BATCH_SIZE = 100
|
|
6
|
+
|
|
7
|
+
attr_reader :base_uri, :api_key, :account, :username, :password
|
|
8
|
+
|
|
9
|
+
def self.for_configured_account(configured_account)
|
|
10
|
+
service_definition = configured_account.service_definition
|
|
11
|
+
mapped_account = configured_account.account_mapping
|
|
12
|
+
credentials = configured_account.credentials.last
|
|
13
|
+
|
|
14
|
+
SltcApi.new(service_definition.server_base_url,
|
|
15
|
+
service_definition.credentials.last.token,
|
|
16
|
+
mapped_account.account_code,
|
|
17
|
+
configured_account.username,
|
|
18
|
+
credentials.password)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(base_uri, api_key, account, username, password)
|
|
22
|
+
@base_uri = base_uri
|
|
23
|
+
@api_key = api_key
|
|
24
|
+
@account = account
|
|
25
|
+
@username = username
|
|
26
|
+
@password = password
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def list_providers
|
|
30
|
+
get_response do
|
|
31
|
+
RestClient::Request.execute(method: :get,
|
|
32
|
+
url: build_url(:Providers),
|
|
33
|
+
user: "#{username}@#{account}",
|
|
34
|
+
password: password,
|
|
35
|
+
headers: authentication_headers.merge({ 'Accept' => 'application/json' }))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def list_batches(provider, pull_from, pull_to)
|
|
40
|
+
batches = []
|
|
41
|
+
page = 1
|
|
42
|
+
while true
|
|
43
|
+
response = get_response do
|
|
44
|
+
RestClient::Request.execute(method: :get,
|
|
45
|
+
url: build_url(:MdsBatches, { provider_id: provider,
|
|
46
|
+
page: page,
|
|
47
|
+
uploaded_after: pull_from,
|
|
48
|
+
uploaded_before: pull_to }),
|
|
49
|
+
user: "#{username}@#{account}",
|
|
50
|
+
password: password,
|
|
51
|
+
headers: authentication_headers.merge({ 'Accept' => 'application/json' }))
|
|
52
|
+
end
|
|
53
|
+
batches += response
|
|
54
|
+
if response.size < BATCH_SIZE
|
|
55
|
+
break
|
|
56
|
+
else
|
|
57
|
+
page += 1
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
batches
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def search_assessments(provider, pull_from, pull_to, status='Accepted')
|
|
64
|
+
get_response do
|
|
65
|
+
RestClient::Request.execute(method: :get,
|
|
66
|
+
url: build_url(:MdsAssessments, { provider_id: provider,
|
|
67
|
+
status_updated_after: pull_from,
|
|
68
|
+
status_updated_before: pull_to,
|
|
69
|
+
status: status }),
|
|
70
|
+
user: "#{username}@#{account}",
|
|
71
|
+
password: password,
|
|
72
|
+
headers: authentication_headers.merge({ 'Accept' => 'application/json' }))
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def upload_batch(provider_id, path)
|
|
77
|
+
file = nil
|
|
78
|
+
File.open(path, "rb") { |f| file = f.read }
|
|
79
|
+
get_response do
|
|
80
|
+
RestClient::Request.execute(method: :post,
|
|
81
|
+
url: build_url(:MdsBatches, { filename: path }),
|
|
82
|
+
payload: file,
|
|
83
|
+
user: "#{username}@#{account}",
|
|
84
|
+
password: password,
|
|
85
|
+
headers: authentication_headers.merge( { :content_type => 'application/octet-stream',
|
|
86
|
+
'Accept' => 'application/json' } ))
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def download_batch(batch_id)
|
|
91
|
+
get_response(true) do
|
|
92
|
+
RestClient::Request.execute(method: :get,
|
|
93
|
+
url: build_url(:MdsBatches, batch_id),
|
|
94
|
+
user: "#{username}@#{account}",
|
|
95
|
+
password: password,
|
|
96
|
+
headers: authentication_headers.merge( { 'Accept' => 'application/octet-stream' } ))
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def download_assessment_request(request_id)
|
|
101
|
+
get_response(true) do
|
|
102
|
+
RestClient::Request.execute(method: :get,
|
|
103
|
+
url: build_url(:MdsAssessmentsAsync, request_id),
|
|
104
|
+
user: "#{username}@#{account}",
|
|
105
|
+
password: password,
|
|
106
|
+
headers: authentication_headers.merge({ 'Accept' => 'application/octet-stream' }))
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def assessment_request_status(request_id)
|
|
111
|
+
get_response do
|
|
112
|
+
RestClient::Request.execute(method: :get,
|
|
113
|
+
url: build_url(:MdsAssessmentsAsync, request_id),
|
|
114
|
+
user: "#{username}@#{account}",
|
|
115
|
+
password: password,
|
|
116
|
+
headers: authentication_headers.merge( { 'Accept' => 'application/json' }))
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def assessment_request(provider_id, uploaded_after, uploaded_before, status)
|
|
121
|
+
parameters = {
|
|
122
|
+
'provider_id' => provider_id,
|
|
123
|
+
'status_updated_after' => uploaded_after.strftime("%Y-%m-%dT%H:%MZ"),
|
|
124
|
+
'status_updated_before' => uploaded_before.strftime("%Y-%m-%dT%H:%MZ"),
|
|
125
|
+
'status' => status
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
get_response do
|
|
129
|
+
RestClient::Request.execute(method: :post,
|
|
130
|
+
url: build_url(:MdsAssessmentsAsync),
|
|
131
|
+
payload: parameters.to_json,
|
|
132
|
+
user: "#{username}@#{account}",
|
|
133
|
+
password: password,
|
|
134
|
+
headers: authentication_headers.merge({ :content_type => :json,
|
|
135
|
+
'Accept' => 'application/json' }))
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def get_response(binary_response=false)
|
|
142
|
+
response = nil
|
|
143
|
+
begin
|
|
144
|
+
response = yield
|
|
145
|
+
rescue RestClient::Exception => ex
|
|
146
|
+
raise SltcApiException.new(ex)
|
|
147
|
+
end
|
|
148
|
+
binary_response ? response : JSON.parse(response)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def authentication_headers
|
|
152
|
+
{
|
|
153
|
+
'SLTC-Api-Key' => api_key,
|
|
154
|
+
'Host' => URI(base_uri).host
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def build_url(api_name, parameters=nil)
|
|
159
|
+
url = "#{base_uri}/#{api_name.to_s}"
|
|
160
|
+
if parameters.present?
|
|
161
|
+
if parameters.is_a?(Fixnum)
|
|
162
|
+
url << '/' << parameters.to_s
|
|
163
|
+
elsif parameters.is_a?(String)
|
|
164
|
+
url << '/' << parameters
|
|
165
|
+
elsif parameters.is_a?(Hash)
|
|
166
|
+
url << "?"
|
|
167
|
+
url << parameters.keys.each.reduce([]) do | a, key |
|
|
168
|
+
raw_value = parameters[key]
|
|
169
|
+
value = raw_value.respond_to?(:strftime) ? raw_value.strftime("%Y-%m-%dT%H:%MZ") : raw_value
|
|
170
|
+
a << "#{key}=#{value}"
|
|
171
|
+
end.join("&")
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
url
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
end
|