Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
ruby 3.2.2
ruby 3.3.8
nodejs 20.7.0
8 changes: 4 additions & 4 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ gem "administrate", "0.20.1"
# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", "1.18.3", require: false

# Rack middleware for blocking & throttling
gem 'rack-attack'
# Rack middleware for blocking & throttling
gem "rack-attack"

# Use Sass to process CSS
# gem "sassc-rails"
Expand Down Expand Up @@ -109,6 +109,8 @@ group :development, :test do
end

group :development do
gem "brakeman", "6.1.2"
gem "bullet"
# To ensure code consistency [https://docs.rubocop.org]
gem "rubocop", "1.56.2"
gem "rubocop-factory_bot", "!= 2.26.0", require: false
Expand All @@ -118,8 +120,6 @@ group :development do
gem "rubocop-rspec_rails", "!= 2.29.0", require: false
# Use console on exceptions pages [https://github.com/rails/web-console]
gem "web-console", "4.2.1"
gem "bullet"
gem "brakeman", "6.1.2"

# Preview mail in the browser instead of sending.
gem "letter_opener", "1.10.0"
Expand Down
44 changes: 22 additions & 22 deletions app/controllers/api/v1/event_procedures_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,28 @@ class EventProceduresController < ApiController
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index

def index
authorized_scope = policy_scope(EventProcedure)

listed_event_procedures = EventProcedures::List.result(
scope: authorized_scope,
params: event_procedure_permitted_query_params
)

event_procedures = listed_event_procedures.event_procedures
event_procedures_unpaginated = listed_event_procedures.event_procedures_unpaginated

total_amount_cents = EventProcedures::TotalAmountCents.call(
event_procedures: event_procedures_unpaginated
)

render json: {
total: total_amount_cents.total,
total_paid: total_amount_cents.paid,
total_unpaid: total_amount_cents.unpaid,
event_procedures: serialized_event_procedures(event_procedures)
}, status: :ok
end
def index
authorized_scope = policy_scope(EventProcedure)

listed_event_procedures = EventProcedures::List.result(
scope: authorized_scope,
params: event_procedure_permitted_query_params
)

event_procedures = listed_event_procedures.event_procedures
event_procedures_unpaginated = listed_event_procedures.event_procedures_unpaginated

total_amount_cents = EventProcedures::TotalAmountCents.call(
event_procedures: event_procedures_unpaginated
)

render json: {
total: total_amount_cents.total,
total_paid: total_amount_cents.paid,
total_unpaid: total_amount_cents.unpaid,
event_procedures: serialized_event_procedures(event_procedures)
}, status: :ok
end

def create
authorize(EventProcedure)
Expand Down
77 changes: 77 additions & 0 deletions app/controllers/api/v1/medical_shift_recurrences_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# frozen_string_literal: true

module Api
module V1
class MedicalShiftRecurrencesController < ApiController
before_action :set_recurrence, only: %i[destroy]
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index

def index
authorized_scope = policy_scope(MedicalShiftRecurrence)
recurrences = authorized_scope
.where(deleted_at: nil)
.order(created_at: :desc)

render json: recurrences, each_serializer: MedicalShiftRecurrenceSerializer
end

def create
authorize(MedicalShiftRecurrence)
result = MedicalShiftRecurrences::Create.result(
attributes: recurrence_params,
user_id: current_user.id
)

if result.success?
render json: {
medical_shift_recurrence: result.medical_shift_recurrence,
shifts_generated: result.shifts_created.count
}, status: :created
else
render json: { errors: result.error }, status: :unprocessable_content
end
end

def destroy
authorize(@recurrence)
result = MedicalShiftRecurrences::Cancel.call(
medical_shift_recurrence: @recurrence
)

if result.success?
render json: {
message: "Recurrence cancelled successfully",
shifts_cancelled: result.shifts_cancelled
}, status: :ok
else
render json: { errors: result.error }, status: :unprocessable_entity
end
end

private

def set_recurrence
@recurrence = current_user.medical_shift_recurrences
.where(deleted_at: nil)
.find(params[:id])
rescue ActiveRecord::RecordNotFound
render json: { error: "Recurrence not found" }, status: :not_found
end

def recurrence_params
params.require(:medical_shift_recurrence).permit(
:frequency,
:day_of_week,
:day_of_month,
:start_date,
:end_date,
:workload,
:start_hour,
:hospital_name,
:amount_cents
).to_h
end
end
end
end
5 changes: 5 additions & 0 deletions app/models/medical_shift.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class MedicalShift < ApplicationRecord
monetize :amount

belongs_to :user
belongs_to :medical_shift_recurrence, optional: true

scope :by_hospital, MedicalShifts::ByHospitalQuery
scope :by_month, MedicalShifts::ByMonthQuery
Expand All @@ -33,4 +34,8 @@ def shift
def title
"#{hospital_name} | #{workload_humanize} | #{shift}"
end

def recurring?
medical_shift_recurrence_id.present?
end
end
63 changes: 63 additions & 0 deletions app/models/medical_shift_recurrence.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# frozen_string_literal: true

class MedicalShiftRecurrence < ApplicationRecord
acts_as_paranoid

FREQUENCIES = %w[weekly biweekly monthly_fixed_day].freeze

has_enumeration_for :workload, with: MedicalShifts::Workloads, create_helpers: true

monetize :amount

belongs_to :user

has_many :medical_shifts, dependent: :nullify

validates :frequency, presence: true, inclusion: { in: FREQUENCIES }
validates :start_date, presence: true
validates :workload, presence: true
validates :start_hour, presence: true
validates :hospital_name, presence: true
validates :amount_cents, presence: true, numericality: { greater_than_or_equal_to: 0 }

validates :day_of_week, presence: true, if: lambda {
frequency.in?(%w[weekly biweekly])
}, numericality: { only_integer: true, in: 0..6 }
validates :day_of_month, presence: true, if: lambda {
frequency == "monthly_fixed_day"
}, numericality: { only_integer: true, in: 1..31 }

validate :day_of_month_blank_for_weekly
validate :day_of_week_blank_for_monthly
validate :end_date_after_start_date
validate :start_date_not_in_past

scope :active, -> { where(deleted_at: nil) }
scope :needs_generation, MedicalShiftRecurrences::NeedsGenerationQuery

private

def day_of_month_blank_for_weekly
return unless frequency.in?(%w[weekly biweekly]) && day_of_month.present?

errors.add(:day_of_month, "It must be empty for weekly/biweekly recurrence.")
end

def day_of_week_blank_for_monthly
return unless frequency == "monthly_fixed_day" && day_of_week.present?

errors.add(:day_of_week, "It must be empty for monthly_fixed_day recurrence.")
end

def end_date_after_start_date
return unless end_date.present? && start_date.present? && end_date < start_date

errors.add(:end_date, "End date must be after start date.")
end

def start_date_not_in_past
return unless start_date.present? && start_date < Time.zone.today

errors.add(:start_date, "Start date cannot be in the past.")
end
end
1 change: 1 addition & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class User < ApplicationRecord

has_many :event_procedures, dependent: :destroy
has_many :medical_shifts, dependent: :destroy
has_many :medical_shift_recurrences, dependent: :destroy
has_many :patients, dependent: :destroy
has_many :procedures, dependent: :destroy
has_many :health_insurances, dependent: :destroy
Expand Down
30 changes: 30 additions & 0 deletions app/operations/medical_shift_recurrences/cancel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

module MedicalShiftRecurrences
class Cancel < Actor
input :medical_shift_recurrence, type: MedicalShiftRecurrence

output :shifts_cancelled, type: Integer, default: 0

def call
fail!(error: "Recurrence already cancelled") if medical_shift_recurrence.deleted_at.present?

ActiveRecord::Base.transaction do
cancel_future_shifts
medical_shift_recurrence.destroy
end
end

private

def cancel_future_shifts
future_shifts = medical_shift_recurrence
.medical_shifts
.where(deleted_at: nil)
.where("start_date >= ?", Date.current)

self.shifts_cancelled = future_shifts.count
future_shifts.destroy_all
end
end
end
63 changes: 63 additions & 0 deletions app/operations/medical_shift_recurrences/create.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# frozen_string_literal: true

module MedicalShiftRecurrences
class Create < Actor
input :attributes, type: Hash
input :user_id, type: Integer

output :medical_shift_recurrence, type: MedicalShiftRecurrence
output :shifts_created, type: Array, default: -> { [] }

GENERATION_HORIZON_MONTHS = 2

def call
create_recurrence
initialize_shifts_array
generate_shifts
end

private

def create_recurrence
self.medical_shift_recurrence = MedicalShiftRecurrence.new(
attributes.reverse_merge(user_id: user_id)
)

fail!(error: medical_shift_recurrence.errors.full_messages) unless medical_shift_recurrence.save
end

def initialize_shifts_array
self.shifts_created = []
end

def generate_shifts
target_date = GENERATION_HORIZON_MONTHS.months.from_now.to_date
dates = MedicalShiftRecurrences::RecurrenceDateCalculatorService.new(
medical_shift_recurrence
).dates_until(target_date)

dates.each do |date|
result = MedicalShifts::Create.result(
attributes: shift_attributes(date),
user_id: user_id
)

shifts_created << result.medical_shift if result.success?
end

medical_shift_recurrence.update!(last_generated_until: target_date) if shifts_created.any?
end

def shift_attributes(date)
{
start_date: date,
start_hour: medical_shift_recurrence.start_hour,
workload: medical_shift_recurrence.workload,
hospital_name: medical_shift_recurrence.hospital_name,
amount_cents: medical_shift_recurrence.amount_cents,
medical_shift_recurrence_id: medical_shift_recurrence.id,
paid: false
}
end
end
end
Loading