From 4e40d42bf943407d44dc6fc3e505a18a8d1c5be8 Mon Sep 17 00:00:00 2001 From: Sai Kumar Kotagiri Date: Thu, 1 Aug 2024 10:22:35 -0400 Subject: [PATCH] creates operational process for force syncing for crm (#4156) --- app/domain/operations/crm/family/publish.rb | 112 ++++++++++++++++++ app/domain/operations/crm/force_sync.rb | 66 +++++++++++ script/crm/trigger_force_sync.rb | 38 ++++++ .../operations/crm/family/publish_spec.rb | 49 ++++++++ spec/domain/operations/crm/force_sync_spec.rb | 99 ++++++++++++++++ 5 files changed, 364 insertions(+) create mode 100644 app/domain/operations/crm/family/publish.rb create mode 100644 app/domain/operations/crm/force_sync.rb create mode 100644 script/crm/trigger_force_sync.rb create mode 100644 spec/domain/operations/crm/family/publish_spec.rb create mode 100644 spec/domain/operations/crm/force_sync_spec.rb diff --git a/app/domain/operations/crm/family/publish.rb b/app/domain/operations/crm/family/publish.rb new file mode 100644 index 00000000000..18471233660 --- /dev/null +++ b/app/domain/operations/crm/family/publish.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +module Operations + module Crm + module Family + # This operation is responsible for publishing an event for a a primary person. + class Publish + include Dry::Monads[:do, :result] + include EventSource::Command + + # Publishes an event for a given person identified by hbx_id. + # + # @param hbx_id [String] The HBX ID of the person. + # @return [Dry::Monads::Result] The result of the publish operation. + def call(hbx_id:) + person = yield find_person(hbx_id) + family = yield find_primary_family(person) + headers = yield headers(family, person) + transformed_cv = yield construct_cv_transform(family, hbx_id) + family_entity = yield create_family_entity(transformed_cv) + es_event = yield create_es_event(family_entity, headers) + published_result = yield publish_es_event(es_event, hbx_id) + + Success(published_result) + end + + private + + # Finds a person by their HBX ID. + # + # @param hbx_id [String] The HBX ID of the person. + # @return [Dry::Monads::Result] The result containing the person or an error message. + def find_person(hbx_id) + result = ::Operations::People::Find.new.call({ person_hbx_id: hbx_id }) + + if result.success? + Success(result.value!) + else + Failure("Provide a valid person_hbx_id to fetch person. Invalid input hbx_id: #{hbx_id}") + end + end + + # Finds the primary family of a given person. + # + # @param person [Person] The person object. + # @return [Dry::Monads::Result] The result containing the family or an error message. + def find_primary_family(person) + family = person.primary_family + + if family + Success(family) + else + Failure("Primary Family does not exist with given hbx_id: #{person.hbx_id}") + end + end + + # Constructs headers for the event. + # + # @param family [Family] The family object. + # @param person [Person] The person object. + # @return [Dry::Monads::Result] The result containing the headers. + def headers(family, person) + eligible_dates = [person.created_at, person.updated_at, family.created_at, family.updated_at].compact + + Success({ after_updated_at: eligible_dates.max, before_updated_at: eligible_dates.min }) + end + + # Constructs a CV transform for the family. + # + # @param family [Family] The family object. + # @param hbx_id [String] The HBX ID of the person. + # @return [Dry::Monads::Result] The result containing the transformed CV or an error message. + def construct_cv_transform(family, hbx_id) + ::Operations::Transformers::FamilyTo::Cv3Family.new.call(family) + rescue StandardError => e + Failure("Failed to transform family with primary person_hbx_id: #{hbx_id} to CV: #{e.message}") + end + + # Creates a family entity from the transformed CV. + # + # @param transformed_cv [Hash] The transformed CV. + # @return [Dry::Monads::Result] The result containing the family entity. + def create_family_entity(transformed_cv) + ::AcaEntities::Operations::CreateFamily.new.call(transformed_cv) + end + + # Creates an event source event for the family entity. + # + # @param family_entity [FamilyEntity] The family entity object. + # @param headers [Hash] The headers for the event. + # @return [Dry::Monads::Result] The result containing the event. + def create_es_event(family_entity, headers) + event( + 'events.families.created_or_updated', + attributes: { before_save_cv_family: {}, after_save_cv_family: family_entity.to_h }, + headers: headers + ) + end + + # Publishes the event source event. + # + # @param es_event [EventSource::Event] The event source event. + # @param hbx_id [String] The HBX ID of the person. + # @return [Dry::Monads::Result] The result of the publish operation. + def publish_es_event(es_event, hbx_id) + es_event.publish + Success("Successfully published event: #{es_event.name} for family with primary person hbx_id: #{hbx_id}") + end + end + end + end +end diff --git a/app/domain/operations/crm/force_sync.rb b/app/domain/operations/crm/force_sync.rb new file mode 100644 index 00000000000..5c85683c105 --- /dev/null +++ b/app/domain/operations/crm/force_sync.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module Operations + module Crm + # This operation is responsible for initiating the force sync operation. + class ForceSync + include Dry::Monads[:do, :result] + include EventSource::Command + + # Initiates the force sync operation. + # + # @param params [Hash] the parameters for the operation + # @option params [Array] :primary_hbx_ids an array of primary HBX IDs + # @return [Dry::Monads::Result] the result of the operation + def call(params) + primary_person_hbx_ids = yield validate(params) + csv_file_name = yield publish_families(primary_person_hbx_ids) + message = yield generate_success_message(csv_file_name) + + Success(message) + end + + private + + # Validates the input parameters. + # + # @param params [Hash] the parameters to validate + # @return [Dry::Monads::Result::Success, Dry::Monads::Result::Failure] the validation result + def validate(params) + if params[:primary_hbx_ids].is_a?(Array) && params[:primary_hbx_ids].present? && params[:primary_hbx_ids].all? { |id| id.is_a?(String) } + Success(params[:primary_hbx_ids]) + else + Failure("Invalid input for primary_hbx_ids: #{params[:primary_hbx_ids]}. Provide an array of HBX IDs.") + end + end + + # Publishes each family and logs o/p into a CSV file. + # + # @param primary_person_hbx_ids [Array] an array of primary HBX IDs + # @return [Dry::Monads::Result::Success, Dry::Monads::Result::Failure] the result of the publishing operation + def publish_families(primary_person_hbx_ids) + csv_file_name = "#{Rails.root}/crm_force_sync_#{DateTime.now.strftime('%Y_%m_%d_%H_%M_%S')}.csv" + + CSV.open(csv_file_name, 'w', force_quotes: true) do |csv| + csv << ['Hbx ID', 'Result', 'Message'] + + primary_person_hbx_ids.each do |hbx_id| + result = ::Operations::Crm::Family::Publish.new.call(hbx_id: hbx_id) + + csv << [ + hbx_id, + result.success? ? 'Success' : 'Failed', + result.success? ? result.success : result.failure + ] + end + end + + Success(csv_file_name) + end + + def generate_success_message(csv_file_name) + Success("Successfully published events for all families. Review the CSV file with results: #{csv_file_name}") + end + end + end +end diff --git a/script/crm/trigger_force_sync.rb b/script/crm/trigger_force_sync.rb new file mode 100644 index 00000000000..727556e6abb --- /dev/null +++ b/script/crm/trigger_force_sync.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# This script takes a string of comma separated hbx_ids and initiates the force sync operation. + +# Command to trigger the script: +# CLIENT=me bundle exec rails runner script/crm/trigger_force_sync.rb 'hbx_id1, hbx_id2, hbx_id3' + +# Checks if the :async_publish_updated_families feature is enabled in the EnrollRegistry. +# If the feature is not enabled, it prints a message and exits the script. +# +# @return [void] +unless EnrollRegistry.feature_enabled?(:async_publish_updated_families) + puts 'The CRM Sync Refactor feature is not enabled. Please enable the feature :async_publish_updated_families to run this script. EXITING.' + exit +end + +# Creates an array of hbx_ids from the input string and removes any nil values, spaces or empty strings. +# @example +# bundle exec rails runner script/crm/trigger_force_sync.rb 'hbx_id1, hbx_id2, hbx_id3' +# @param [String] ARGV[0] a comma separated list of HBX IDs +# @return [void] +primary_person_hbx_ids = begin + # The below line splits the input string by comma, removes any leading or trailing spaces, converts the values to string, and rejects any empty strings. + ARGV[0].split(',').map { |hbx_id| hbx_id.strip.to_s }.reject(&:empty?) +rescue StandardError => e + puts "Error: #{e.message}. Invalid input for primary_hbx_ids: #{ARGV[0]}. Provide a comma separated list of HBX IDs." + exit +end + +# Initiates the force sync operation with the provided HBX IDs. +# @param [Array] primary_hbx_ids an array of HBX IDs +# @return [void] +result = ::Operations::Crm::ForceSync.new.call({ primary_hbx_ids: primary_person_hbx_ids }) +if result.success? + puts result.success +else + puts result.failure +end diff --git a/spec/domain/operations/crm/family/publish_spec.rb b/spec/domain/operations/crm/family/publish_spec.rb new file mode 100644 index 00000000000..12eeb5aa803 --- /dev/null +++ b/spec/domain/operations/crm/family/publish_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Operations::Crm::Family::Publish do + before :all do + DatabaseCleaner.clean + end + + after :all do + DatabaseCleaner.clean + end + + let(:primary) { FactoryBot.create(:person, :with_consumer_role, :with_active_consumer_role) } + let(:family) { FactoryBot.create(:family, :with_primary_family_member, person: primary) } + + describe '#call' do + context 'success' do + let(:hbx_id) { family.primary_person.hbx_id } + + it 'publishes the event successfully' do + result = subject.call(hbx_id: hbx_id) + expect(result.success).to eq( + "Successfully published event: events.families.created_or_updated for family with primary person hbx_id: #{hbx_id}" + ) + end + end + + context 'failure' do + context 'when a person does not exist with the given hbx_id' do + it 'returns failure' do + result = subject.call(hbx_id: 'primary.hbx_id') + expect(result.failure).to eq( + "Provide a valid person_hbx_id to fetch person. Invalid input hbx_id: primary.hbx_id" + ) + end + end + + context 'when person does not have primary family' do + it 'returns failure' do + result = subject.call(hbx_id: primary.hbx_id) + expect(result.failure).to eq( + "Primary Family does not exist with given hbx_id: #{primary.hbx_id}" + ) + end + end + end + end +end diff --git a/spec/domain/operations/crm/force_sync_spec.rb b/spec/domain/operations/crm/force_sync_spec.rb new file mode 100644 index 00000000000..673912a6a26 --- /dev/null +++ b/spec/domain/operations/crm/force_sync_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Operations::Crm::ForceSync do + include Dry::Monads[:result] + + before :all do + DatabaseCleaner.clean + end + + after :all do + DatabaseCleaner.clean + end + + describe '#call' do + context 'success' do + let(:primary1_hbx_id) { 'primary1' } + let(:primary2_hbx_id) { 'primary2' } + let(:primary3_hbx_id) { 'primary3' } + let(:params) { { primary_hbx_ids: [primary1_hbx_id, primary2_hbx_id, primary3_hbx_id] } } + + let(:publish_instance) { instance_double(Operations::Crm::Family::Publish) } + + let(:message1) { "Successfully published event: events.families.created_or_updated for family with primary person hbx_id: #{primary1_hbx_id}" } + let(:message2) { "Provide a valid person_hbx_id to fetch person. Invalid input hbx_id: primary2_hbx_id" } + let(:message3) { "Primary Family does not exist with given hbx_id: #{primary3_hbx_id}" } + + before do + allow(::Operations::Crm::Family::Publish).to receive(:new).and_return(publish_instance) + allow(publish_instance).to receive(:call).with(hbx_id: primary1_hbx_id).and_return( + Success(message1) + ) + + allow(publish_instance).to receive(:call).with(hbx_id: primary2_hbx_id).and_return( + Failure(message2) + ) + + allow(publish_instance).to receive(:call).with(hbx_id: primary3_hbx_id).and_return( + Failure(message3) + ) + end + + it 'returns a success monad' do + expect(subject.call(params).success?).to be_truthy + end + + it 'creates a CSV ' do + message = subject.call(params).success + csv_file_name = message.split(': ').last + expect( + File.exist?(csv_file_name) + ).to be_truthy + end + + it 'logs the results of the operation in CSV' do + message = subject.call(params).success + csv_file_name = message.split(': ').last + csv = CSV.read(csv_file_name) + expect(csv[1]).to eq([primary1_hbx_id, 'Success', message1]) + expect(csv[2]).to eq([primary2_hbx_id, 'Failed', message2]) + expect(csv[3]).to eq([primary3_hbx_id, 'Failed', message3]) + end + end + + context 'failure' do + context 'when primary_hbx_ids is not an array' do + let(:params) { { primary_hbx_ids: 'primary1' } } + + it 'returns a failure monad' do + expect(subject.call(params).failure).to eq('Invalid input for primary_hbx_ids: primary1. Provide an array of HBX IDs.') + end + end + + context 'when primary_hbx_ids is an empty array' do + let(:params) { { primary_hbx_ids: [] } } + + it 'returns a failure monad' do + expect(subject.call(params).failure).to eq('Invalid input for primary_hbx_ids: []. Provide an array of HBX IDs.') + end + end + + context 'when primary_hbx_ids array has non-string elements' do + let(:params) { { primary_hbx_ids: [100] } } + + it 'returns a failure monad' do + expect(subject.call(params).failure).to eq('Invalid input for primary_hbx_ids: [100]. Provide an array of HBX IDs.') + end + end + end + end + + after :all do + # Clean up the CSV files created during the test + Dir.glob("#{Rails.root}/crm_force_sync_*.csv").each do |file| + FileUtils.rm(file) + end + end +end