Skip to content

Commit

Permalink
Add Registration V2 API Routes to the monolith (#10022)
Browse files Browse the repository at this point in the history
* add new models

* add job to create a new registration

* make Competing lane it's own module and run rubocop

* add v2 api files

* add error codes and WIP registration_checker

* migrate Registration checker

* add new registration error and use validations for guests and comments

* move routes back to the monolith

* migrate create route

* remove dynamoid error handling

* fix registration show

* fix registration list_admin

* fix registration list

* make update work

* run rubocop

* more complex rubocop changes

* fix migration merge

* fix create after merge

* remove double render from update

* fix typo for bulk_update

* suppress logging in tests

* use allow_registration_self_delete_after_acceptance

* refactor update in competing lane

* get payment working

* add payment history entry and add has_paid boolean for frontend

* make refunds work

* rub rubocop

* implement qualifications

* run rubocop

* removed log level change

* added skip for before(:suite) block

* WIP checker specs

* WIP checker specs

* working with JWT tokens now

* rubocop

* comp qualifications factory working

* all checker create tests passing

* added update tests

* rubocop

* basic update status tests done'

* rubocop

* checker update status tests completed

* event update checks

* final updates and bulk update requests now passing

* rubocop

* minor corrections

* bulk update tests :)

* ruboocp

* list admin working

* triggering tests

* fix for competition factory

* trying to trigger test

* removed whitespace

* busy trying to figure out routes.js.erb issues

* fixing webpacker issues

* rubocop

* fixed comp factory

* fixed weird event limit reliance

* trying to fix cases where we want to add qualificatiosn later

* fix view test errors

* lol checking present without safe accessing

* oops

* please work

* move breaking changes into another PR

* wcif statuses and comments

* more comments and re-enabled random test order

* rubocop fix

* added tests against wcif statuses

* Unlocalized bulk update error

* rubocop

* refactor validate_jwt_token

* remove rescues

* add prepare task

* add TODO about @competition

* use existing method for other_series_ids

* run rubocop

* rename to payment_statuses

* Hallucinate an admin to set WCIF events

* remove unverified_wcif_events

* renamed low_event_limit trait

* remove duplicate competition factory property

* moved to admin factory

* changed mock user to competition organizer

* move all string registration statuses to constants

* move send_status_change_email check out of the method

* renamed user fields to current_user and target_user

* flip check in organizer_modifying_own_registration?

* use Qualification class for competitor_qualifies_for_event?

* missing commas

* fix guests being string

* fix constant access

* fix guests string in create_registration

* fix typo

* forgot status waiting list oops

* add :with_organizers for tests

* got request tests working

* rubocop

* fixed tests and added local check for disabled tag

* defining event ids separately from events in reg factory

* added TODO comment

* renamed with_low_event_limit

* More efficient series check

* Make events_held? more concise to read

* Make 'guest_limit_exceeded?' more readable

* switch competing_status from hardcoded strings to constants defined in registration helper

* quick refactor fixes

* simplified events_held

---------

Co-authored-by: Duncan <duncanonthejob@gmail.com>
Co-authored-by: Duncan <52967253+dunkOnIT@users.noreply.github.com>
Co-authored-by: Gregor Billing <gbilling@worldcubeassociation.org>
  • Loading branch information
4 people authored Oct 30, 2024
1 parent 7b785b0 commit 4405901
Show file tree
Hide file tree
Showing 33 changed files with 3,959 additions and 93 deletions.
25 changes: 1 addition & 24 deletions app/controllers/api/v0/api_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,8 @@ def user_qualification_data
return render json: { error: 'You cannot request qualification data for a future date.' }, status: :bad_request if date > Date.current

user = User.find(params.require(:user_id))
return render json: [] unless user.person.present?

# Compile singles
best_singles_by_cutoff = user.person.best_singles_by(date)
single_qualifications = best_singles_by_cutoff.map do |event, time|
qualification_data(event, :single, time, date)
end

# Compile averages
best_averages_by_cutoff = user.person&.best_averages_by(date)
average_qualifications = best_averages_by_cutoff.map do |event, time|
qualification_data(event, :average, time, date)
end

render json: single_qualifications + average_qualifications
render json: Registrations::Helper.user_qualification_data(user, date)
end

private def cutoff_date
Expand All @@ -65,16 +52,6 @@ def user_qualification_data
end
end

private def qualification_data(event, type, time, date)
raise ArgumentError.new("'type' may only contain the symbols `:single` or `:average`") unless [:single, :average].include?(type)
{
eventId: event,
type: type,
best: time,
on_or_before: date.iso8601,
}
end

def scramble_program
begin
rsa_key = OpenSSL::PKey::RSA.new(AppSecrets.TNOODLE_PUBLIC_KEY)
Expand Down
37 changes: 37 additions & 0 deletions app/controllers/api/v1/api_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

class Api::V1::ApiController < ActionController::API
prepend_before_action :validate_jwt_token

# Manually include new Relic because we don't derive from ActionController::Base
include NewRelic::Agent::Instrumentation::ControllerInstrumentation if Rails.env.production?

def validate_jwt_token
auth_header = request.headers['Authorization']
if auth_header.blank?
return render json: { error: Registrations::ErrorCodes::MISSING_AUTHENTICATION }, status: :unauthorized
end
token = auth_header.split[1]
begin
decode_result = JWT.decode token, AppSecrets.JWT_KEY, true, { algorithm: 'HS256' }
decoded_token = decode_result[0]
@current_user = User.find(decoded_token['user_id'].to_i)
rescue JWT::VerificationError, JWT::InvalidJtiError
render json: { error: Registrations::ErrorCodes::INVALID_TOKEN }, status: :unauthorized
rescue JWT::ExpiredSignature
render json: { error: Registrations::ErrorCodes::EXPIRED_TOKEN }, status: :unauthorized
end
end

def render_error(http_status, error, data = nil)
if data.present?
render json: { error: error, data: data }, status: http_status
else
render json: { error: error }, status: http_status
end
end

rescue_from ActionController::ParameterMissing do |e|
render json: { error: Registrations::ErrorCodes::INVALID_REQUEST_DATA }, status: :bad_request
end
end
156 changes: 156 additions & 0 deletions app/controllers/api/v1/registrations/registrations_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# frozen_string_literal: true

class Api::V1::Registrations::RegistrationsController < Api::V1::ApiController
skip_before_action :validate_jwt_token, only: [:list]
# The order of the validations is important to not leak any non public info via the API
# That's why we should always validate a request first, before taking any other before action
# before_actions are triggered in the order they are defined
before_action :validate_create_request, only: [:create]
before_action :validate_show_registration, only: [:show]
before_action :validate_list_admin, only: [:list_admin]
before_action :validate_update_request, only: [:update]
before_action :validate_bulk_update_request, only: [:bulk_update]
before_action :validate_payment_ticket_request, only: [:payment_ticket]

rescue_from ActiveRecord::RecordNotFound do
render json: {}, status: :not_found
end

rescue_from WcaExceptions::RegistrationError do |e|
# TODO: Figure out what the best way to log errors in development is
Rails.logger.debug { "Create was rejected with error #{e.error} at #{e.backtrace[0]}" }
render_error(e.status, e.error, e.data)
end

rescue_from WcaExceptions::BulkUpdateError do |e|
Rails.logger.debug { "Bulk update was rejected with error #{e.errors} at #{e.backtrace[0]}" }
render_error(e.status, e.errors)
end

def validate_show_registration
@user_id, @competition_id = show_params
@competition = Competition.find(@competition_id)
render_error(:unauthorized, ErrorCodes::USER_INSUFFICIENT_PERMISSIONS) unless @current_user.id == @user_id.to_i || @current_user.can_manage_competition?(@competition)
end

def show
registration = Registration.find_by!(user_id: @user_id, competition_id: @competition_id)
render json: registration.to_v2_json(admin: true, history: true)
end

def create
# Currently we only have one lane
if params[:competing]
competing_params = params.permit(:guests, competing: [:status, :comment, { event_ids: [] }, :admin_comment])

user_id = registration_params['user_id']
competition_id = registration_params['competition_id']

AddRegistrationJob.prepare_task(user_id, competition_id)
.perform_later("competing", competition_id, user_id, competing_params)
return render json: { status: 'accepted', message: 'Started Registration Process' }, status: :accepted
end

render json: { status: 'bad request', message: 'You need to supply at least one lane' }, status: :bad_request
end

def validate_create_request
Registrations::RegistrationChecker.create_registration_allowed!(params, @current_user)
end

def update
if params[:competing]
updated_registration = Registrations::Lanes::Competing.update!(params, @current_user.id)
return render json: { status: 'ok', registration: updated_registration.to_v2_json(admin: true, history: true) }, status: :ok
end
render json: { status: 'bad request', message: 'You need to supply at least one lane' }, status: :bad_request
end

def validate_update_request
Registrations::RegistrationChecker.update_registration_allowed!(params, @current_user)
end

def bulk_update
updated_registrations = {}
update_requests = params[:requests]
update_requests.each do |update|
updated_registrations[update['user_id']] = Registrations::Lanes::Competing.update!(update, @current_user)
end

render json: { status: 'ok', updated_registrations: updated_registrations }
end

def validate_bulk_update_request
Registrations::RegistrationChecker.bulk_update_allowed!(params, @current_user)
end

def list
competition_id = list_params
registrations = Registration.where(competition_id: competition_id)
render json: registrations.map { |r| r.to_v2_json }
end

# To list Registrations in the admin view you need to be able to administer the competition
def validate_list_admin
competition_id = list_params
# TODO: Do we set this as an instance variable here so we can use it below?
@competition = Competition.find(competition_id)
unless @current_user.can_manage_competition?(@competition)
render_error(:unauthorized, ErrorCodes::USER_INSUFFICIENT_PERMISSIONS)
end
end

def list_admin
registrations = Registration.where(competition: @competition)
render json: registrations.map { |r| r.to_v2_json(admin: true, history: true, pii: true) }
end

def validate_payment_ticket_request
competition_id = params[:competition_id]
@competition = Competition.find(competition_id)
render_error(:forbidden, ErrorCodes::PAYMENT_NOT_ENABLED) unless @competition.using_payment_integrations?

@registration = Registration.find_by(user: @current_user, competition: @competition)
render_error(:forbidden, ErrorCodes::PAYMENT_NOT_READY) if @registration.nil?
end

def payment_ticket
donation = params[:donation_iso].to_i || 0
amount_iso = @competition.base_entry_fee_lowest_denomination
currency_iso = @competition.currency_code
payment_account = @competition.payment_account_for(:stripe)
payment_intent = payment_account.prepare_intent(@registration, amount_iso + donation, currency_iso, @current_user)
render json: { client_secret: payment_intent.client_secret }
end

private

def action_type(request)
self_updating = request[:user_id] == @current_user
status = request.dig('competing', 'status')
if status == 'cancelled'
return self_updating ? 'Competitor delete' : 'Admin delete'
end
self_updating ? 'Competitor update' : 'Admin update'
end

def registration_params
params.require([:user_id, :competition_id])
params.require(:competing).require(:event_ids)
params
end

def show_params
user_id, competition_id = params.require([:user_id, :competition_id])
[user_id.to_i, competition_id]
end

def update_params
params.require([:user_id, :competition_id])
params.permit(:guests, competing: [:status, :comment, { event_ids: [] }, :admin_comment])
end

def list_params
params.require(:competition_id)
end
end
4 changes: 2 additions & 2 deletions app/controllers/registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -786,9 +786,9 @@ def create
:administrative_notes,
]
params[:registration].merge! case params[:registration][:status]
when "accepted"
when Registrations::Helper::STATUS_ACCEPTED
{ accepted_at: Time.now, accepted_by: current_user.id, deleted_at: nil }
when "deleted"
when Registrations::Helper::STATUS_DELETED
{ deleted_at: Time.now, deleted_by: current_user.id }
else
{ accepted_at: nil, deleted_at: nil }
Expand Down
6 changes: 6 additions & 0 deletions app/jobs/add_registration_job.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# frozen_string_literal: true

class AddRegistrationJob < ApplicationJob
def self.prepare_task(user_id, competition_id)
message_deduplication_id = "competing-registration-#{competition_id}-#{user_id}"
message_group_id = competition_id
self.set(message_group_id: message_group_id, message_deduplication_id: message_deduplication_id)
end

def perform(lane_name, competition_id, user_id, lane_params)
lane_model_name = lane_name.upcase_first

Expand Down
19 changes: 19 additions & 0 deletions app/models/competition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,21 @@ def main_event
Event.c_find(main_event_id)
end

def events_held?(desired_event_ids)
desired_event_ids.present? && (desired_event_ids & self.event_ids) == desired_event_ids
end

def enforces_qualifications?
uses_qualification? && !allow_registration_without_qualification
end

def guest_limit_exceeded?(guest_count)
guests_not_allowed_but_coming = !guests_enabled? && guest_count > 0
guests_exceeding_limit = guest_entry_status_restricted? && guests_per_registration_limit.present? && guest_count > guests_per_registration_limit

guests_not_allowed_but_coming || guests_exceeding_limit
end

def with_old_id
new_id = self.id
self.id = id_was
Expand Down Expand Up @@ -1877,6 +1892,10 @@ def competition_series_ids
competition_series&.competition_ids&.split(',') || []
end

def other_series_ids
series_sibling_competitions.pluck(:id)
end

def qualification_wcif
return {} unless uses_qualification?
competition_events
Expand Down
12 changes: 6 additions & 6 deletions app/models/microservice_registration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,16 @@ def ms_loaded?

def competing_status
# Treat non-competing registrations as accepted, see also `registration.rb`
return "accepted" unless self.is_competing?
return Registrations::Helper::STATUS_ACCEPTED unless self.is_competing?

self.read_ms_data :competing_status
end

alias :status :competing_status

def wcif_status
return "deleted" if self.deleted?
return "pending" if self.pending?
return Registrations::Helper::STATUS_DELETED if self.deleted?
return Registrations::Helper::STATUS_PENDING if self.pending?

self.competing_status
end
Expand Down Expand Up @@ -103,17 +103,17 @@ def administrative_notes
end

def accepted?
self.status == "accepted"
self.status == Registrations::Helper::STATUS_ACCEPTED
end

def deleted?
self.status == "cancelled" || self.status == "rejected"
self.status == Registrations::Helper::STATUS_DELETED || self.status == Registrations::Helper::STATUS_REJECTED
end

def pending?
# WCIF interprets "pending" as "not approved to compete yet"
# which is why these two statuses collapse into one.
self.status == "pending" || self.status == "waiting_list"
self.status == Registrations::Helper::STATUS_PENDING || self.status == Registrations::Helper::STATUS_WAITING_LIST
end

def to_wcif(authorized: false)
Expand Down
Loading

0 comments on commit 4405901

Please sign in to comment.