Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle to populate with native fields and address fields from contacts #1684

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1257b41
Handle to populate with native fields and address fields from contacts
samnang Jan 6, 2025
2621c4e
Merge branch 'develop' into handle_with_native_fields_and_address_fie…
samnang Jan 21, 2025
59e9b42
Fix tests
samnang Jan 21, 2025
7e035a0
Merge branch 'develop' into handle_with_native_fields_and_address_fie…
samnang Jan 27, 2025
c92b3c6
Add rubocop
samnang Jan 27, 2025
3942413
Create a broadcast
samnang Jan 27, 2025
177bdf9
Refactor beneficiary_filter to be reused in multiple places
samnang Jan 27, 2025
3201539
Composable contract and reuse contract's rules
samnang Jan 27, 2025
af398e7
Handle invalid request examples
samnang Jan 29, 2025
50171cb
Handle updating broadcast
samnang Jan 29, 2025
2e3759d
Update state transition
samnang Jan 29, 2025
d7cd48f
Remove populations
samnang Jan 31, 2025
8d839f0
Update tests
samnang Jan 31, 2025
8fb2f42
Merge branch 'develop' into handle_with_native_fields_and_address_fie…
samnang Jan 31, 2025
70e3701
Rename beneficiary filter and add channel
samnang Feb 3, 2025
5337da7
Handle mapping between old and new states
samnang Feb 3, 2025
cf820cb
Add rule validation on beneficiary_filter
samnang Feb 3, 2025
ad3d697
Populate broadcast beneficiaries
samnang Feb 3, 2025
6e8080e
Add populate_broadcast_beneficiaries spec
samnang Feb 3, 2025
2611f53
Merge branch 'develop' into handle_with_native_fields_and_address_fie…
samnang Feb 4, 2025
80c28db
Add note
samnang Feb 6, 2025
340f8f4
Merge branch 'develop' into handle_with_native_fields_and_address_fie…
samnang Feb 6, 2025
0ec7465
Fix tests
samnang Feb 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ end

group :development do
gem "listen"
gem "rubocop"
gem "rubocop-performance"
gem "rubocop-rails-omakase", require: false
gem "rubocop-rspec", require: false
Expand Down
9 changes: 5 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ GEM
activerecord
kaminari-core (= 1.2.2)
kaminari-core (1.2.2)
language_server-protocol (3.17.0.3)
language_server-protocol (3.17.0.4)
launchy (3.0.1)
addressable (~> 2.8)
childprocess (~> 5.0)
Expand Down Expand Up @@ -478,17 +478,17 @@ GEM
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.2)
rubocop (1.70.0)
rubocop (1.71.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.36.2, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.37.0)
rubocop-ast (1.38.0)
parser (>= 3.3.1.0)
rubocop-minitest (0.36.0)
rubocop (>= 1.61, < 2.0)
Expand Down Expand Up @@ -641,6 +641,7 @@ DEPENDENCIES
responders
rspec-rails
rspec_api_documentation!
rubocop
rubocop-performance
rubocop-rails-omakase
rubocop-rspec
Expand Down
9 changes: 9 additions & 0 deletions app/controllers/api/v1/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ def validate_request_schema(with:, **options, &block)
end
end

def apply_filters(scope, with:)
validate_request_schema(
with: ApplicationFilter.build_filter_schema(with),
input_params: request.query_parameters
) do |permitted_params|
FilterScopeQuery.new(scope, permitted_params).apply
end
end

def respond_with_errors(object, **)
respond_with(object, responder: InvalidRequestSchemaResponder, **)
end
Expand Down
7 changes: 1 addition & 6 deletions app/controllers/api/v1/beneficiaries_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@ module API
module V1
class BeneficiariesController < BaseController
def index
validate_request_schema(
with: BeneficiaryFilter,
input_params: request.query_parameters
) do |permitted_params|
FilterScopeQuery.new(beneficiaries_scope, permitted_params).apply
end
apply_filters(beneficiaries_scope, with: BeneficiaryFilter)
end

def show
Expand Down
48 changes: 48 additions & 0 deletions app/controllers/api/v1/broadcasts_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
module API
module V1
class BroadcastsController < BaseController
def index
broadcasts = broadcasts_scope
respond_with_resource(broadcasts)
end

def show
broadcast = broadcasts_scope.find(params[:id])
respond_with_resource(broadcast)
end

def create
validate_request_schema(
with: ::V1::BroadcastRequestSchema,
# TODO: can remove this once after we rename the model to broadcast
location: ->(resource) { api_v1_broadcast_path(resource) }
) do |permitted_params|
broadcasts_scope.create!(permitted_params)
end
end

def update
broadcast = broadcasts_scope.find(params[:id])

validate_request_schema(
with: ::V1::UpdateBroadcastRequestSchema,
schema_options: { resource: broadcast },
) do |permitted_params|
broadcast.update!(permitted_params)

if broadcast.queued?
ExecuteWorkflowJob.perform_later(PopulateBroadcastBeneficiaries.to_s, broadcast)
end

broadcast
end
end

private

def broadcasts_scope
current_account.broadcasts
end
end
end
end
20 changes: 17 additions & 3 deletions app/filters/application_filter.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
class ApplicationFilter < ApplicationRequestSchema
def output
output_data = super
output_data.fetch(:filter, {})
class_attribute :__filter_class__

def self.build_filter_schema(filter_class)
Class.new(ApplicationFilter) do
self.__filter_class__ = filter_class

params do
optional(:filter).schema(filter_class.schema)
end

rule(:filter).validate(contract: filter_class)

def output
result = super.fetch(:filter, {})
__filter_class__.new(input_params: result).output
end
end
end
end
28 changes: 13 additions & 15 deletions app/filters/beneficiary_filter.rb
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
class BeneficiaryFilter < ApplicationFilter
params do
optional(:filter).value(:hash).schema do
optional(:status).filled(included_in?: Contact.status.values)
optional(:disability_status).maybe(included_in?: Contact.disability_status.values)
optional(:gender).filled(Types::UpcaseString, included_in?: Contact.gender.values)
optional(:date_of_birth).filled(:date)
optional(:iso_country_code).filled(Types::UpcaseString, included_in?: Contact.iso_country_code.values)
optional(:language_code).maybe(:string)
optional(:"address.iso_region_code").filled(:string)
optional(:"address.administrative_division_level_2_code").filled(:string)
optional(:"address.administrative_division_level_2_name").filled(:string)
optional(:"address.administrative_division_level_3_code").filled(:string)
optional(:"address.administrative_division_level_3_name").filled(:string)
optional(:"address.administrative_division_level_4_code").filled(:string)
optional(:"address.administrative_division_level_4_name").filled(:string)
end
optional(:status).filled(included_in?: Contact.status.values)
optional(:disability_status).maybe(included_in?: Contact.disability_status.values)
optional(:gender).filled(Types::UpcaseString, included_in?: Contact.gender.values)
optional(:date_of_birth).filled(:date)
optional(:iso_country_code).filled(Types::UpcaseString, included_in?: Contact.iso_country_code.values)
optional(:language_code).maybe(:string)
optional(:"address.iso_region_code").filled(:string)
optional(:"address.administrative_division_level_2_code").filled(:string)
optional(:"address.administrative_division_level_2_name").filled(:string)
optional(:"address.administrative_division_level_3_code").filled(:string)
optional(:"address.administrative_division_level_3_name").filled(:string)
optional(:"address.administrative_division_level_4_code").filled(:string)
optional(:"address.administrative_division_level_4_name").filled(:string)
end

def output
Expand Down
4 changes: 2 additions & 2 deletions app/models/account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ class Account < ApplicationRecord
has_many :beneficiaries, class_name: "Contact",
dependent: :restrict_with_error

has_many :callouts,
dependent: :restrict_with_error
has_many :callouts, dependent: :restrict_with_error
has_many :broadcasts, class_name: "Callout", dependent: :restrict_with_error

has_many :batch_operations,
class_name: "BatchOperation::Base",
Expand Down
4 changes: 4 additions & 0 deletions app/models/batch_operation/callout_population.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ class CalloutPopulation < Base

validates :contact_filter_params, contact_filter_params: true

def self.jsonapi_serializer_class
BroadcastPopulationSerializer
end

def run!
transaction do
create_callout_participations
Expand Down
54 changes: 45 additions & 9 deletions app/models/callout.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
class Callout < ApplicationRecord
extend Enumerize

AUDIO_CONTENT_TYPES = %w[audio/mpeg audio/mp3 audio/wav audio/x-wav].freeze
CHANNELS = %i[voice].freeze

module ActiveStorageDirty
attr_reader :audio_file_blob_was, :audio_file_will_change
Expand All @@ -26,17 +29,24 @@ def audio_file_blob_changed?
store_accessor :settings
accepts_nested_key_value_fields_for :settings

# TODO: Remove the default after we removed the old API
enumerize :channel, in: CHANNELS, default: :voice

belongs_to :account
belongs_to :created_by, class_name: "User", optional: true

has_many :callout_participations, dependent: :restrict_with_error
has_many :broadcast_beneficiaries, class_name: "CalloutParticipation", dependent: :restrict_with_error
has_many :beneficiaries, class_name: "Contact", through: :broadcast_beneficiaries, source: :contact

has_many :batch_operations,
class_name: "BatchOperation::Base",
dependent: :restrict_with_error

has_many :callout_populations,
class_name: "BatchOperation::CalloutPopulation"
has_many :populations,
class_name: "BatchOperation::CalloutPopulation"

has_many :phone_calls

Expand All @@ -48,6 +58,7 @@ def audio_file_blob_changed?

has_one_attached :audio_file

validates :channel, :status, presence: true
validates :call_flow_logic, :status, presence: true

validates :audio_file,
Expand All @@ -68,40 +79,65 @@ def audio_file_blob_changed?
after_commit :process_audio_file

aasm column: :status, whiny_transitions: false do
state :initialized, initial: true
state :pending, initial: true
state :queued
state :running
state :paused
state :stopped
state :completed

# TODO: Remove state transition from pending after we removed the old API
event :start do
transitions(
from: :initialized,
from: [ :pending, :queued ],
to: :running
)
end

event :stop do
transitions(
from: [ :running, :queued ],
to: :stopped
)
end

# TODO: Remove the pause event after we removed the old API
event :pause do
transitions(
from: :running,
to: :paused
from: [ :running, :queued ],
to: :stopped
)
end

event :resume do
transitions(
from: %i[paused stopped],
from: :stopped,
to: :running
)
end

event :stop do
event :complete do
transitions(
from: %i[running paused],
to: :stopped
from: :running,
to: :completed
)
end
end

def self.jsonapi_serializer_class
BroadcastSerializer
end

# TODO: Remove this after we removed the old API
def as_json(*)
result = super(except: [ "channel", "beneficiary_filter" ])
result["status"] = "initialized" if result["status"] == "pending"
result
end

def updatable?
status == "pending"
end

private

def set_call_flow_logic
Expand Down
1 change: 1 addition & 0 deletions app/models/phone_call.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class PhoneCall < ApplicationRecord

belongs_to :callout_participation, optional: true, counter_cache: true
belongs_to :contact, validate: true
belongs_to :beneficiary, class_name: "Contact", foreign_key: "contact_id"
belongs_to :account
belongs_to :callout, optional: true
has_many :remote_phone_call_events, dependent: :restrict_with_error
Expand Down
30 changes: 30 additions & 0 deletions app/request_schemas/application_request_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,36 @@ class ApplicationRequestSchema < Dry::Validation::Contract
key.failure(text: "is invalid") if key? && !Phony.plausible?(value)
end

register_macro(:url_format) do
next unless key?

uri = URI.parse(value)
isValid = (uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)) && uri.host.present?

key.failure(text: "is invalid") unless isValid
rescue URI::InvalidURIError
key.failure(text: "is invalid")
end

# NOTE: composable contracts
#
# params do
# required(:a).hash(OtherContract.schema)
# end
#
# rule(:a).validate(contract: OtherContract)
#
register_macro(:contract) do |macro:|
contract_instance = macro.args[0]
contract_result = contract_instance.new(input_params: value)
unless contract_result.success?
errors = contract_result.errors
errors.each do |error|
key(key.path.to_a + error.path).failure(error.text)
end
end
end

module Types
include Dry.Types()

Expand Down
7 changes: 5 additions & 2 deletions app/request_schemas/v1/beneficiary_stats_request_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ class BeneficiaryStatsRequestSchema < ApplicationRequestSchema
"address.administrative_division_level_4_name"
].freeze

params(BeneficiaryFilter.schema) do
params do
optional(:filter).schema(BeneficiaryFilter.schema)
required(:group_by).value(array[:string])
end

rule(:filter).validate(contract: BeneficiaryFilter)

rule(:group_by) do
next key.failure("is invalid") unless value.all? { |group| group.in?(GROUPS) }

Expand All @@ -37,7 +40,7 @@ class BeneficiaryStatsRequestSchema < ApplicationRequestSchema
def output
result = super

result[:filter_fields] = BeneficiaryFilter.new(input_params: result).output
result[:filter_fields] = BeneficiaryFilter.new(input_params: result[:filter]).output if result[:filter]

result[:group_by_fields] = result[:group_by].map do |group|
BeneficiaryField.find(group)
Expand Down
Loading