Skip to content

Commit

Permalink
Merge pull request #80 from hitobito/feature/hitobito_youth#58
Browse files Browse the repository at this point in the history
feat: add default ahv question for #58
  • Loading branch information
diegosteiner authored Sep 12, 2024
2 parents fac5e47 + c470310 commit 8316f2e
Show file tree
Hide file tree
Showing 12 changed files with 338 additions and 28 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Hitobito Changelog

## Unreleased

* AHV-Nummern wurde als globale Frage für alle Anlässe hinzugefügt. Es muss für jeden neuen Anlass ausgewählt werden, ob die Antwort auf diese Frage obligatorisch, optional oder versteckt sein soll. Diese Antworten dafür werden im NDS-Export des jeweiligen Anlasses berücksichtigt. (hitobito_youth#58)

## Version 1.30

* Es können neu die eigenen Kinder direkt am Anlass angemeldet werden. Kinder werden entweder auf der Person hinterlegt oder können bei der Anlass-Anmeldung direkt neu angelegt werden. (#1969)
Expand Down
29 changes: 29 additions & 0 deletions app/domain/ahv_number_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

# Copyright (c) 2012-2021, Jungwacht Blauring Schweiz. This file is part of
# hitobito and licensed under the Affero General Public License version 3
# or later. See the COPYING file at the top-level directory or at
# https://github.com/hitobito/hitobito.

class AhvNumberValidator < ActiveModel::EachValidator
require_dependency "social_security_number"
include ::SocialSecurityNumber

AHV_NUMBER_REGEX = /\A\d{3}\.\d{4}\.\d{4}\.\d{2}\z/

def validate_each(record, attribute, value)
return if value.blank?

if !AHV_NUMBER_REGEX.match?(value)
record.errors.add(attribute, :must_be_social_security_number_with_correct_format)
return
end
unless checksum_validate(value).valid?
record.errors.add(attribute, :must_be_social_security_number_with_correct_checksum)
end
end

def checksum_validate(ahv_number, country_code: "ch")
SocialSecurityNumber::Validator.new(number: ahv_number.to_s, country_code:)
end
end
4 changes: 4 additions & 0 deletions app/domain/export/tabular/people/participation_nds_row.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ def canton
entry.canton.to_s.upcase
end

def ahv_number
entry.last_known_ahv_number(participation.id)
end

def country
{
"CH" => "CH",
Expand Down
35 changes: 35 additions & 0 deletions app/models/event/question/ahv_number.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

# Copyright (c) 2012-2021, Jungwacht Blauring Schweiz. This file is part of
# hitobito and licensed under the Affero General Public License version 3
# or later. See the COPYING file at the top-level directory or at
# https://github.com/hitobito/hitobito.

# == Schema Information
#
# Table name: event_questions
#
# id :integer not null, primary key
# admin :boolean default(FALSE), not null
# choices :string(255)
# multiple_choices :boolean default(FALSE), not null
# question :text(65535)
# required :boolean default(FALSE), not null
# event_id :integer
#
# Indexes
#
# index_event_questions_on_event_id (event_id)
#

class Event::Question::AhvNumber < Event::Question
def validate_answer(answer)
validator = AhvNumberValidator.new(attributes: :answer)
validator.validate(answer)
end

def translation_class
# ensures globalize works with STI
Event::Question.globalize_translation_class
end
end
34 changes: 12 additions & 22 deletions app/models/youth/person.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@
module Youth::Person
extend ActiveSupport::Concern

require_dependency "social_security_number"
include ::SocialSecurityNumber

NATIONALITIES_J_S = %w[CH FL ANDERE].freeze

included do
Expand All @@ -27,25 +24,13 @@ module Youth::Person
has_many :manageds, through: :people_manageds

validates :nationality_j_s, inclusion: {in: NATIONALITIES_J_S, allow_blank: true}
validates :ahv_number, ahv_number: true, unless: :skip_ahv_number_validation?

validate :assert_either_only_managers_or_manageds
validate :validate_ahv_number
end

AHV_NUMBER_REGEX = /\A\d{3}\.\d{4}\.\d{4}\.\d{2}\z/

def validate_ahv_number
# Allow changing the password, even if there is an invalid AHV number in the database
return if will_save_change_to_encrypted_password? && !will_save_change_to_ahv_number?
return if ahv_number.blank?

if !AHV_NUMBER_REGEX.match?(ahv_number)
errors.add(:ahv_number, :must_be_social_security_number_with_correct_format)
return
end
unless checksum_validate(ahv_number).valid?
errors.add(:ahv_number, :must_be_social_security_number_with_correct_checksum)
end
def skip_ahv_number_validation?
will_save_change_to_encrypted_password? && !will_save_change_to_ahv_number?
end

def assert_either_only_managers_or_manageds # rubocop:disable Metrics/CyclomaticComplexity,Metrics/AbcSize,Metrics/PerceivedComplexity
Expand All @@ -67,15 +52,20 @@ def and_manageds
[self, manageds].flatten
end

def checksum_validate(ahv_number)
SocialSecurityNumber::Validator.new(number: ahv_number.to_s, country_code: "ch")
end

def valid_email?(email = self.email)
if FeatureGate.enabled?("people.people_managers")
super || Person.mailing_emails_for(self).any? { |mail| super(mail) }
else
super
end
end

def last_known_ahv_number(participation_ids = event_participation_ids)
Event::Answer.joins(:question, :participation)
.where(participation: participation_ids)
.where(event_questions: {type: Event::Question::AhvNumber.sti_name})
.where.not(answer: [nil, ""])
.order(Event::Participation.arel_table[:updated_at].desc)
.last&.answer.presence || try(:ahv_number)
end
end
20 changes: 20 additions & 0 deletions db/seeds/event_questions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# encoding: utf-8

# Copyright (c) 2012-2024, Jungwacht Blauring Schweiz. This file is part of
# hitobito_jubla and licensed under the Affero General Public License version 3
# or later. See the COPYING file at the top-level directory or at
# https://github.com/hitobito/hitobito_jubla.

Event::Question.create_with_translations([
{
disclosure: nil, # Has to be chosen for every event
event_type: nil, # Is derived for every event
type: Event::Question::AhvNumber.sti_name,
translation_attributes: [
{ locale: 'de', question: 'AHV-Nummer?' },
{ locale: 'fr', question: 'Numéro AVS ?' },
{ locale: 'it', question: 'Numero AVS?' },
{ locale: 'en', question: 'AVS number?' }
]
},
])
27 changes: 25 additions & 2 deletions spec/domain/export/tabular/people/participation_nds_row_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
it { expect(row.fetch(:first_name)).to eq 'Peter' }
it { expect(row.fetch(:birthday)).to eq '11.06.1980' }
it { expect(row.fetch(:gender)).to eq 'm' }
it { expect(row.fetch(:ahv_number)).to eq '756.1234.5678.97' }
it { expect(row.fetch(:peid)).to be_nil }
it { expect(row.fetch(:nationality_j_s)).to eq 'FL' }
it { expect(row.fetch(:first_language)).to eq 'DE' }
Expand Down Expand Up @@ -69,6 +68,31 @@
end
end

describe 'ahv_number' do
subject(:ahv_number) { row.fetch(:ahv_number) }
let(:valid_ahv_number) { '756.1234.5678.97' }

before do
expect(person).to receive(:last_known_ahv_number).and_call_original
end

context "with ahv_number fallback on person" do
it "calls #last_known_ahv_number and returns #ahv_number" do
person.ahv_number = valid_ahv_number
is_expected.to eq(person.ahv_number)
end
end

context "with ahv_number on participation" do
it "calls #last_known_ahv_number and returns participation answer" do
event = participation.event
question = Event::Question::AhvNumber.create(disclosure: :required, question: "AHV?", event: event)
answer = Event::Answer.find_by(question: question, participation: participation)
answer.update!(answer: valid_ahv_number)
is_expected.to eq(valid_ahv_number)
end
end
end

private

Expand All @@ -80,7 +104,6 @@ def nds_person
birthday: '11.06.1980',
gender: 'm',
j_s_number: '1695579',
ahv_number: '756.1234.5678.97',
street: 'Hauptstrasse',
housenumber: '33',
zip_code: '4000',
Expand Down
10 changes: 6 additions & 4 deletions spec/domain/synchronize/mailchimp/subscriber_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,23 @@
AdditionalEmail.new(label: 'vater', email: '[email protected]', mailings: true)
end

subject { described_class.mailing_list_subscribers(mailing_list) }
subject(:subscribers) { described_class.mailing_list_subscribers(mailing_list) }

context 'default strategy' do
it 'returns all people and their manager' do
manager = Fabricate(:person)
person.managers = [manager]

subscribers = subject
expect(subscribers.count).to eq(2)

managed_subscriber = subscribers.first
manager_subscriber = subscribers.last
managed_subscriber = subscribers.find { _1.person == person }
manager_subscriber = subscribers.find { _1.person == manager }

expect(managed_subscriber).to be_present
expect(managed_subscriber.email).to eq(person.email)
expect(managed_subscriber.person).to eq(person)

expect(manager_subscriber).to be_present
expect(manager_subscriber.email).to eq(manager.email)
expect(manager_subscriber.person).to eq(manager)
end
Expand Down
105 changes: 105 additions & 0 deletions spec/features/question_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# frozen_string_literal: true

# Copyright (c) 2012-2024, Jungwacht Blauring Schweiz. This file is part of
# hitobito and licensed under the Affero General Public License version 3
# or later. See the COPYING file at the top-level directory or at
# https://github.com/hitobito/hitobito.

require "spec_helper"

describe EventsController, js: true do
let(:event) do
Fabricate(:event, groups: [groups(:top_group)]).tap do |event|
event.dates.create!(start_at: 10.days.ago, finish_at: 5.days.ago)
end
end
let(:global_questions) do
{
ahv_number: Event::Question::AhvNumber.create!(question: "AHV-Number?", event_type: nil, disclosure: nil),
}
end

def click_save
all("form .btn-group").first.click_button "Speichern"
end

def click_next
all(".bottom .btn-group").first.click_button "Weiter"
end

def click_signup
all(".bottom .btn-group").first.click_button "Anmelden"
end

def find_question_field(question)
page.all(".fields").find { |question_element| question_element.text.start_with?(question.question) }
end

describe "global Event::Question::AhvNumber" do
subject(:question_fields_element) do
click_link I18n.t("event.participations.application_answers")
page.find("#application_questions_fields")
end

before do
Event::Question.delete_all
global_questions
sign_in
visit edit_group_event_path(event.group_ids.first, event.id)
end

it "includes global questions with matching event type" do
is_expected.to have_text(global_questions[:ahv_number].question)

is_expected.not_to have_text('Mögliche Antworten')
is_expected.not_to have_text('Mehrfachauswahl')
is_expected.not_to have_text('Entfernen')
end
end

describe "answers for global questions" do
let(:user) { people(:bottom_member) }
let(:event_with_questions) do
event.init_questions
event.application_questions.map { |question| question.update!(disclosure: question.disclosure || :optional) }
event.save!
event
end

subject { page }

before do
Event::Question.delete_all
global_questions
event_with_questions
sign_in(user)
visit contact_data_group_event_participations_path(event.group_ids.first, event.id, event_role: {type: Event::Role::Participant})
click_next
end

it "fails with empty required question" do
sleep 0.5 # avoid wizard race condition

within find_question_field(global_questions[:ahv_number]) do
answer_element = find('input[type="text"]')
answer_element.fill_in(with: "Not An AHV-Number")
end
click_signup
is_expected.to have_content "Antwort muss im gültigen Format sein (756.1234.5678.97)"

within find_question_field(global_questions[:ahv_number]) do
answer_element = find('input[type="text"]')
answer_element.fill_in(with: "756.1234.5678.90")
end
click_signup
is_expected.to have_content "Antwort muss eine gültige Prüfziffer haben."

within find_question_field(global_questions[:ahv_number]) do
answer_element = find('input[type="text"]')
answer_element.fill_in(with: "756.1234.5678.97")
end
click_signup
is_expected.to have_content "Teilnahme von Bottom Member in Eventus wurde erfolgreich erstellt."
end
end
end
12 changes: 12 additions & 0 deletions spec/fixtures/event/questions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,31 @@

top_ov:
event: top_course
disclosure: optional
type: Event::Question::Default

top_vegi:
event: top_course
disclosure: optional
type: Event::Question::Default

top_more:
event: top_course
disclosure: optional
type: Event::Question::Default

# global questions (not assigned to event)
ga:
event: {}
disclosure: optional
type: Event::Question::Default

vegi:
event: {}
disclosure: optional
type: Event::Question::Default

schub:
event: {}
disclosure: optional
type: Event::Question::Default
Loading

0 comments on commit 8316f2e

Please sign in to comment.