# frozen_string_literal: true module LoggableActivity module Services # This service class builds update payloads when an instance of a model is updated. class UpdatePayloadsBuilder < BasePayloadsBuilder # Builds payloads for a ::LoggableActivity::Activity. def build build_primary_update_payload build_relations_update_payloads @payloads end private # Builds the primary payload. def build_primary_update_payload previous_values, current_values = saved_changes(@record) public_attrs = updated_public_attrs(current_values.slice(*@public_attrs), previous_values.slice(*@public_attrs)) previous_values = previous_values.slice(*@loggable_attrs) current_values = current_values.slice(*@loggable_attrs) options = { related_to_activity_as: 'primary_update_payload', current_payload: true, data_owner: true } build_encrypted_update_payload( @record, current_values, previous_values, public_attrs, options ) end # Build the payloads for the relations of the record. # only relations included in the configuration, will be logged. def build_relations_update_payloads @relations.each do |relation_config| build_relation_update_payload(relation_config) end end def build_relation_update_payload(relation_config) relation_config.each_key do |key| case key when 'belongs_to' build_belongs_to_update_payload(relation_config) when 'has_one' build_has_one_update_payload(relation_config) when 'has_many' build_has_many_update_payloads(relation_config) end end end # Builds the update payload for a belongs_to relation. def build_belongs_to_update_payload(relation_config) relation_id = "#{relation_config['belongs_to']}_id" model_class_name = relation_config['model'] model_class = model_class_name.constantize data_owner = relation_config['data_owner'] || false model_ids = saved_changes(@record).map { |hash| hash[relation_id] } model_ids.each_with_index do |id, index| record = id ? model_class.find_by(id:) : nil next unless record options = { related_to_activity_as: 'belongs_to_update_payload', current_payload: index == 1, data_owner: } build_encrypted_payload(record, options) end end # Builds payloads for has_many relations. def build_has_many_update_payloads(relation_config) relation = (relation_config['has_many']).to_s records = @record.send(relation) return nil if records.empty? records.each do |record| if record.persisted? && record.changes_to_save.any? build_has_many_update_payload(relation_config, record) elsif record.persisted? && record.saved_changes.any? # "no changes to save" else build_has_many_create_payload(relation_config, record) end end end def build_has_many_update_payload(relation_config, record) record.disable_hooks = true previous_values, current_values = changes_to_save(record) loggable_attrs = relation_config['loggable_attrs'] return if previous_values == current_values public_attributes = relation_config['public_attrs'] public_attrs = updated_public_attrs( current_values.slice(*public_attributes), previous_values.slice(*public_attributes) ) previous_values = previous_values.slice(*loggable_attrs) data_owner = relation_config['data_owner'] || false options = { related_to_activity_as: 'has_many_update_payload', current_payload: true, data_owner: } build_encrypted_update_payload( record, current_values, previous_values, public_attrs, options ) end def build_has_many_create_payload(relation_config, record) record.disable_hooks = true data_owner = relation_config['data_owner'] || false options = { related_to_activity_as: 'has_many_create_payload', current_payload: true, data_owner: } build_encrypted_payload(record, options) end # Builds the update payload for a has_one relation. def build_has_one_update_payload(relation_config) relation = (relation_config['has_one']).to_s record = @record.send(relation) return nil if record.nil? previous_values, current_values = changes_to_save(record) loggable_attrs = relation_config['loggable_attrs'] return if previous_values == current_values public_attrs = updated_public_attrs( current_values.slice(*record.class.public_attrs), previous_values.slice(*record.class.public_attrs) ) previous_values = previous_values.slice(*loggable_attrs) current_values = current_values.slice(*loggable_attrs) data_owner = relation_config['data_owner'] || false options = { related_to_activity_as: 'has_one_update_payload', current_payload: true, data_owner: } build_encrypted_update_payload( record, current_values, previous_values, public_attrs, options ) end # Returns the encrypted attributes for the update payload. def encrypted_update_attrs(current_values, previous_values, encryption_key) changes = [] previous_values.each do |key, from_value| from = ::LoggableActivity::Encryption.encrypt(from_value, encryption_key) to_value = current_values[key] to = ::LoggableActivity::Encryption.encrypt(to_value, encryption_key) changes << { key => { from:, to: } } end { changes: } end def updated_public_attrs(current_values, previous_values) changes = [] previous_values.each do |key, from_value| from = from_value to_value = current_values[key] to = to_value changes << { key => { from:, to: } } end { changes: } end # Builds the encrypted update payload for a record. def build_encrypted_update_payload(record, current_values, previous_values, public_attrs, options = {}) encryption_key = ::LoggableActivity::EncryptionKey.for_record(record) encrypted_attrs = encrypted_update_attrs( current_values, previous_values, encryption_key.secret_key ) build_payload( record, encryption_key, encrypted_attrs, public_attrs, options ) end end end end