Merge pull request #551 from AyuntamientoMadrid/verification_tracking-500

Verification tracking
This commit is contained in:
Juanjo Bazán
2015-09-28 20:04:15 +02:00
27 changed files with 400 additions and 120 deletions

View File

@@ -0,0 +1,12 @@
class Admin::VerificationsController < Admin::BaseController
def index
@users = User.incomplete_verification.page(params[:page])
end
def search
@users = User.incomplete_verification.search(params[:name_or_email]).page(params[:page]).for_render
render :index
end
end

View File

@@ -12,6 +12,17 @@ module AdminHelper
options
end
def humanize_document_type(document_type)
case document_type
when "1"
t "verification.residence.new.document_type.spanish_id"
when "2"
t "verification.residence.new.document_type.passport"
when "3"
t "verification.residence.new.document_type.residence_card"
end
end
private
def namespace

View File

@@ -6,6 +6,7 @@ module Verification
scope :level_two_verified, -> { where("users.confirmed_phone IS NOT NULL AND users.residence_verified_at IS NOT NULL") }
scope :level_two_or_three_verified, -> { where("users.verified_at IS NOT NULL OR (users.confirmed_phone IS NOT NULL AND users.residence_verified_at IS NOT NULL)") }
scope :unverified, -> { where("users.verified_at IS NULL AND (users.residence_verified_at IS NULL OR users.confirmed_phone IS NULL)") }
scope :incomplete_verification, -> { where("(users.residence_verified_at IS NULL AND users.failed_census_calls_count > ?) OR (users.residence_verified_at IS NOT NULL AND (users.unconfirmed_phone IS NULL OR users.confirmed_phone IS NULL))", 0) }
end
def verification_email_sent?
@@ -44,4 +45,15 @@ module Verification
!level_two_or_three_verified?
end
def failed_residence_verification?
!residence_verified? && failed_census_calls.size > 0
end
def no_phone_available?
!verification_sms_sent?
end
def sms_code_not_confirmed?
!sms_verified?
end
end

View File

@@ -1,3 +1,3 @@
class FailedCensusCall < ActiveRecord::Base
belongs_to :user
belongs_to :user, counter_cache: true
end

View File

@@ -21,6 +21,7 @@ class User < ActiveRecord::Base
has_many :debates, -> { with_hidden }, foreign_key: :author_id
has_many :proposals, -> { with_hidden }, foreign_key: :author_id
has_many :comments, -> { with_hidden }
has_many :failed_census_calls
validates :username, presence: true, unless: :organization?
validates :username, uniqueness: true, unless: :organization?

View File

@@ -39,6 +39,13 @@
<% end %>
</li>
<li <%= "class=active" if controller_name == "verifications" %>>
<%= link_to admin_verifications_path do %>
<i class="icon-eye"></i>
<%= t("admin.menu.incomplete_verifications") %>
<% end %>
</li>
<li <%= "class=active" if controller_name == "organizations" %>>
<%= link_to admin_organizations_path do %>
<i class="icon-organizations"></i>

View File

@@ -1,15 +1,6 @@
<h2><%= t("admin.officials.index.title") %></h2>
<%= form_for(User.new, url: search_admin_officials_path, as: :user, method: :get) do |f| %>
<div class="row">
<div class="small-12 medium-6 column">
<%= text_field_tag :name_or_email, "", placeholder: t("admin.officials.index.search_placeholder") %>
</div>
<div class="form-inline small-12 medium-6 column">
<%= f.submit t("admin.officials.index.search"), class: "button radius success" %>
</div>
</div>
<% end %>
<%= render 'admin/shared/user_search', url: search_admin_officials_path %>
<h3><%= page_entries_info @officials %></h3>

View File

@@ -1,15 +1,6 @@
<h2><%= t("admin.officials.search.title") %></h2>
<%= form_for(User.new, url: search_admin_officials_path, as: :user, method: :get) do |f| %>
<div class="row">
<div class="small-12 medium-6 column">
<%= text_field_tag :name_or_email, "", placeholder: t("admin.officials.index.search_placeholder") %>
</div>
<div class="small-12 medium-6 column">
<%= f.submit t("admin.officials.search.search"), class: "button radius success" %>
</div>
</div>
<% end %>
<%= render 'admin/shared/user_search', url: search_admin_officials_path %>
<h3><%= page_entries_info @users %></h3>

View File

@@ -0,0 +1,10 @@
<%= form_for(User.new, url: url, as: :user, method: :get) do |f| %>
<div class="row">
<div class="small-12 medium-6 column">
<%= text_field_tag :name_or_email, "", placeholder: t("admin.shared.user_search.placeholder") %>
</div>
<div class="form-inline small-12 medium-6 column">
<%= f.submit t("admin.shared.user_search.button"), class: "button radius success" %>
</div>
</div>
<% end %>

View File

@@ -0,0 +1,6 @@
<div>
<%= humanize_document_type(failed_census_call.document_type) %>
<%= failed_census_call.document_number %>
<%= l(failed_census_call.date_of_birth) %>
<%= failed_census_call.postal_code %>
</div>

View File

@@ -0,0 +1,7 @@
<% if user.failed_residence_verification? %>
<%= render partial: 'failed_census_call', collection: user.failed_census_calls %>
<% elsif user.no_phone_available? %>
<p><%= t("admin.verifications.index.phone_not_given") %></p>
<% elsif user.sms_code_not_confirmed? %>
<p><%= t("admin.verifications.index.sms_code_not_confirmed") %></p>
<% end %>

View File

@@ -0,0 +1,16 @@
<h2><%= t("admin.verifications.index.title") %></h2>
<%= render 'admin/shared/user_search', url: search_admin_verifications_path %>
<h3><%= page_entries_info @users %></h3>
<ul class="admin-list">
<% @users.each do |user| %>
<li id="<%= dom_id(user) %>">
<%= link_to user.name, admin_user_path(user) %>
<%= render "pending_step", user: user %>
</li>
<% end %>
</ul>
<%= paginate @users %>

View File

@@ -16,11 +16,16 @@ en:
hidden_debates: Hidden debates
hidden_comments: Hidden comments
hidden_users: Blocked users
incomplete_verifications: Incomplete verifications
organizations: Organizations
officials: Officials
moderators: Moderators
stats: Statistics
activity: Moderation Activity
shared:
user_search:
placeholder: 'Search user by name or email'
button: 'Search'
organizations:
index:
title: Organizations
@@ -97,6 +102,11 @@ en:
email: "Email:"
registered_at: "Registered at:"
hidden_at: "Hidden at:"
verifications:
index:
title: Incomplete verifications
phone_not_given: 'Phone not given'
sms_code_not_confirmed: 'Has not confirmed the sms code'
officials:
level_0: Not an official
level_1: Level 1
@@ -106,13 +116,10 @@ en:
level_5: Level 5
index:
title: Officials
search_placeholder: 'Search user by name or email'
search: Search
search:
title: 'Officials: Search users'
edit_official: Edit official
make_official: Make this user an official
search: Search
edit:
title: 'Officials: edit user'
destroy: "Remove 'Official' condition"

View File

@@ -16,11 +16,16 @@ es:
hidden_debates: Debates ocultos
hidden_comments: Comentarios ocultos
hidden_users: Usuarios bloqueados
incomplete_verifications: Verificaciones incompletas
organizations: Organizaciones
officials: Cargos públicos
moderators: Moderadores
stats: Estadísticas
activity: Actividad de moderadores
shared:
user_search:
placeholder: 'Buscar usuario por nombre o email'
button: 'Buscar'
organizations:
index:
title: Organizaciones
@@ -97,6 +102,11 @@ es:
email: "Email:"
registered_at: "Fecha de alta:"
hidden_at: "Bloqueado:"
verifications:
index:
title: Verificaciones incompletas
phone_not_given: 'No ha dado su teléfono'
sms_code_not_confirmed: 'No ha introducido su código de seguridad'
officials:
level_0: "No es cargo público"
level_1: Nivel 1
@@ -106,13 +116,10 @@ es:
level_5: Nivel 5
index:
title: Cargos Públicos
search_placeholder: 'Buscar usuario por nombre o email'
search: Buscar
search:
title: 'Cargos Públicos: Búsqueda de usuarios'
edit_official: Editar cargo público
make_official: Convertir en cargo público
search: Buscar
edit:
title: 'Cargos Públicos: Editar usuario'
destroy: "Eliminar condición de 'Cargo Público'"

View File

@@ -23,7 +23,7 @@ en:
form_errors: "prevented your residence verification"
error_verifying_census: "The census of the city of Madrid could not verify your information. Pero revise de information and try again or get in touch with us."
error_not_allowed_age: "You need yo be at least 16 years old"
error_not_allowed_postal_code: "Please, to verify your account you need to be in the census of the Madrid town."
error_not_allowed_postal_code: "To verify your account you need to be in the census of the Madrid town."
create:
flash:
success: "Residence verified"

View File

@@ -23,7 +23,7 @@ es:
form_errors: "evitaron verificar tu residencia"
error_verifying_census: "El Padrón de Madrid no pudo verificar tu información. Por favor, confirma que tus datos de empadronamiento sean correctos llamando al 010 o ponte en contacto con nosotros."
error_not_allowed_age: "Hay que tener al menos 16 años"
error_not_allowed_postal_code: "Por favor, para verificarte debes estar empadronado en el municipio de Madrid."
error_not_allowed_postal_code: "Para verificarte debes estar empadronado en el municipio de Madrid."
create:
flash:
success: "Residencia verificada"

View File

@@ -113,6 +113,10 @@ Rails.application.routes.draw do
collection { get :search }
end
resources :verifications, controller: :verifications, only: :index do
collection { get :search}
end
resource :activity, controller: :activity, only: :show
resource :stats, only: :show

View File

@@ -0,0 +1,5 @@
class AddFailedCensusCallsCounterCacheToUsers < ActiveRecord::Migration
def change
add_column :users, :failed_census_calls_count, :integer, default: 0
end
end

View File

@@ -257,30 +257,30 @@ ActiveRecord::Schema.define(version: 20150926115929) do
add_index "tags", ["proposals_count"], name: "index_tags_on_proposals_count", using: :btree
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "confirmation_token"
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
t.string "unconfirmed_email"
t.boolean "email_on_comment", default: false
t.boolean "email_on_comment_reply", default: false
t.string "phone_number", limit: 30
t.boolean "email_on_comment", default: false
t.boolean "email_on_comment_reply", default: false
t.string "phone_number", limit: 30
t.string "official_position"
t.integer "official_level", default: 0
t.integer "official_level", default: 0
t.datetime "hidden_at"
t.string "sms_confirmation_code"
t.string "username", limit: 60
t.string "username", limit: 60
t.string "document_number"
t.string "document_type"
t.datetime "residence_verified_at"
@@ -292,6 +292,7 @@ ActiveRecord::Schema.define(version: 20150926115929) do
t.datetime "letter_requested_at"
t.datetime "confirmed_hide_at"
t.string "letter_verification_code"
t.integer "failed_census_calls_count", default: 0
end
add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree

8
lib/tasks/users.rake Normal file
View File

@@ -0,0 +1,8 @@
namespace :users do
desc "Recalculates all the failed census calls counters for users"
task count_failed_census_calls: :environment do
User.find_each{ |user| User.reset_counters(user.id, :failed_census_calls)}
end
end

View File

@@ -0,0 +1,156 @@
require 'rails_helper'
shared_examples_for "verifiable" do
let(:model) { described_class }
describe "#scopes" do
describe "#level_three_verified" do
it "returns level three verified users" do
user1 = create(:user, verified_at: Time.now)
user2 = create(:user, verified_at: nil)
expect(model.level_three_verified).to include(user1)
expect(model.level_three_verified).to_not include(user2)
end
end
describe "#level_two_verified" do
it "returns level two verified users" do
user1 = create(:user, confirmed_phone: "123456789", residence_verified_at: Time.now)
user2 = create(:user, confirmed_phone: "123456789", residence_verified_at: nil)
user3 = create(:user, confirmed_phone: nil, residence_verified_at: Time.now)
expect(model.level_two_verified).to include(user1)
expect(model.level_two_verified).to_not include(user2)
expect(model.level_two_verified).to_not include(user3)
end
end
describe "#level_two_or_three_verified" do
it "returns level two or three verified users" do
user1 = create(:user, confirmed_phone: "123456789", residence_verified_at: Time.now)
user2 = create(:user, verified_at: Time.now)
user3 = create(:user, confirmed_phone: "123456789", residence_verified_at: nil)
user4 = create(:user, confirmed_phone: nil, residence_verified_at: Time.now)
expect(model.level_two_or_three_verified).to include(user1)
expect(model.level_two_or_three_verified).to include(user2)
expect(model.level_two_or_three_verified).to_not include(user3)
expect(model.level_two_or_three_verified).to_not include(user4)
end
end
describe "#unverified" do
it "returns unverified users" do
user1 = create(:user, verified_at: nil, confirmed_phone: nil)
user2 = create(:user, verified_at: nil, residence_verified_at: nil, confirmed_phone: "123456789")
user3 = create(:user, verified_at: nil, residence_verified_at: Time.now, confirmed_phone: nil)
user4 = create(:user, verified_at: Time.now, residence_verified_at: Time.now, confirmed_phone: "123456789")
expect(model.unverified).to include(user1)
expect(model.unverified).to include(user2)
expect(model.unverified).to include(user3)
expect(model.unverified).to_not include(user4)
end
end
describe "#incomplete_verification" do
it "returns users with incomplete verifications" do
user1 = create(:user, verified_at: nil, confirmed_phone: nil)
create(:failed_census_call, user: user1)
user2 = create(:user, verified_at: nil, residence_verified_at: Time.now, unconfirmed_phone: nil)
user3 = create(:user, verified_at: nil, confirmed_phone: nil)
user4 = create(:user, verified_at: Time.now, residence_verified_at: Time.now, unconfirmed_phone: "123456789", confirmed_phone: "123456789")
expect(model.incomplete_verification).to include(user1)
expect(model.incomplete_verification).to include(user2)
expect(model.incomplete_verification).to_not include(user3)
expect(model.incomplete_verification).to_not include(user4)
end
end
end
describe "#methods" do
it "residence_verified? is true only if residence_verified_at" do
user = create(:user, residence_verified_at: Time.now)
expect(user.residence_verified?).to eq(true)
user = create(:user, residence_verified_at: nil)
expect(user.residence_verified?).to eq(false)
end
it "sms_verified? is true only if confirmed_phone" do
user = create(:user, confirmed_phone: "123456789")
expect(user.sms_verified?).to eq(true)
user = create(:user, confirmed_phone: nil)
expect(user.sms_verified?).to eq(false)
end
it "level_two_verified? is true only if residence_verified_at and confirmed_phone" do
user = create(:user, confirmed_phone: "123456789", residence_verified_at: Time.now)
expect(user.level_two_verified?).to eq(true)
user = create(:user, confirmed_phone: nil, residence_verified_at: Time.now)
expect(user.level_two_verified?).to eq(false)
user = create(:user, confirmed_phone: "123456789", residence_verified_at: nil)
expect(user.level_two_verified?).to eq(false)
end
it "level_three_verified? is true only if verified_at" do
user = create(:user, verified_at: Time.now)
expect(user.level_three_verified?).to eq(true)
user = create(:user, verified_at: nil)
expect(user.level_three_verified?).to eq(false)
end
it "unverified? is true only if not level_three_verified and not level_two_verified" do
user = create(:user, verified_at: nil, confirmed_phone: nil)
expect(user.unverified?).to eq(true)
user = create(:user, verified_at: Time.now, confirmed_phone: "123456789", residence_verified_at: Time.now)
expect(user.unverified?).to eq(false)
end
it "verification_email_sent? is true only if user has email_verification_token" do
user = create(:user, email_verification_token: "xxxxxxx")
expect(user.verification_email_sent?).to eq(true)
user = create(:user, email_verification_token: nil)
expect(user.verification_email_sent?).to eq(false)
end
it "verification_sms_sent? is true only if user has unconfirmed_phone and sms_confirmation_code" do
user = create(:user, unconfirmed_phone: "666666666", sms_confirmation_code: "666")
expect(user.verification_sms_sent?).to eq(true)
user = create(:user, unconfirmed_phone: nil, sms_confirmation_code: "666")
expect(user.verification_sms_sent?).to eq(false)
user = create(:user, unconfirmed_phone: "666666666", sms_confirmation_code: nil)
expect(user.verification_sms_sent?).to eq(false)
user = create(:user, unconfirmed_phone: nil, sms_confirmation_code: nil)
expect(user.verification_sms_sent?).to eq(false)
end
it "verification_letter_sent? is true only if user has letter_requested_at and letter_verification_code" do
user = create(:user, letter_requested_at: Time.now, letter_verification_code: "666")
expect(user.verification_letter_sent?).to eq(true)
user = create(:user, letter_requested_at: nil, letter_verification_code: "666")
expect(user.verification_letter_sent?).to eq(false)
user = create(:user, letter_requested_at: Time.now, letter_verification_code: nil)
expect(user.verification_letter_sent?).to eq(false)
user = create(:user, letter_requested_at: nil, letter_verification_code: nil)
expect(user.verification_letter_sent?).to eq(false)
end
end
end

View File

@@ -7,9 +7,17 @@ FactoryGirl.define do
terms_of_service '1'
confirmed_at { Time.now }
trait :incomplete_verification do
after :create do |user|
create(:failed_census_call, user: user)
end
end
trait :level_two do
residence_verified_at Time.now
unconfirmed_phone "611111111"
confirmed_phone "611111111"
sms_confirmation_code "1234"
document_number "12345678Z"
end
@@ -52,6 +60,14 @@ FactoryGirl.define do
end
end
factory :failed_census_call do
user
document_number '11111111A'
document_type 1
date_of_birth Date.new(1900, 1, 1)
postal_code '28000'
end
factory :verification_sms, class: Verification::Sms do
phone "699999999"
end

View File

@@ -0,0 +1,76 @@
require 'rails_helper'
feature 'Incomplete verifications' do
background do
admin = create(:administrator)
login_as(admin.user)
end
scenario 'Index' do
incompletely_verified_user1 = create(:user, :incomplete_verification)
incompletely_verified_user2 = create(:user, :incomplete_verification)
never_tried_to_verify_user = create(:user)
verified_user = create(:user, :level_two)
visit admin_verifications_path
expect(page).to have_content(incompletely_verified_user1.username)
expect(page).to have_content(incompletely_verified_user2.username)
expect(page).to_not have_content(never_tried_to_verify_user.username)
expect(page).to_not have_content(verified_user.username)
end
scenario 'Search' do
verified_user = create(:user, :level_two, username: "Juan Carlos")
unverified_user = create(:user, :incomplete_verification, username: "Juan_anonymous")
unverified_user = create(:user, :incomplete_verification, username: "Isabel_anonymous")
visit admin_verifications_path
fill_in "name_or_email", with: "juan"
click_button "Search"
expect(page).to have_content("Juan_anonymous")
expect(page).to_not have_content("Juan Carlos")
expect(page).to_not have_content("Isabel_anonymous")
end
scenario "Residence unverified" do
incompletely_verified_user = create(:user, :incomplete_verification)
visit admin_verifications_path
within "#user_#{incompletely_verified_user.id}" do
expect(page).to have_content "Spanish ID"
expect(page).to have_content "11111111A"
expect(page).to have_content Date.new(1900, 1, 1)
expect(page).to have_content "28000"
end
end
scenario "Phone not given" do
incompletely_verified_user = create(:user, residence_verified_at: Time.now, unconfirmed_phone: nil)
visit admin_verifications_path
within "#user_#{incompletely_verified_user.id}" do
expect(page).to have_content 'Phone not given'
end
end
scenario "SMS code not confirmed" do
incompletely_verified_user = create(:user, residence_verified_at: Time.now,
unconfirmed_phone: "611111111",
sms_confirmation_code: "1234",
confirmed_phone: nil)
visit admin_verifications_path
within "#user_#{incompletely_verified_user.id}" do
expect(page).to have_content 'Has not confirmed the sms code'
end
end
end

View File

@@ -49,7 +49,7 @@ feature 'Residence' do
click_button 'Verify residence'
expect(page).to have_content 'Please, to verify your account you need to be in the census of the Madrid town.'
expect(page).to have_content 'To verify your account you need to be in the census of the Madrid town.'
end
scenario 'Error on Madrid census' do

View File

@@ -0,0 +1,13 @@
require 'rails_helper'
describe AdminHelper do
describe '#humanize_document_type' do
it "should return a humanized document type" do
expect(humanize_document_type("1")).to eq "Spanish ID"
expect(humanize_document_type("2")).to eq "Passport"
expect(humanize_document_type("3")).to eq "Residence card"
end
end
end

View File

@@ -48,7 +48,7 @@ describe Verification::Residence do
residence.postal_code = "13280"
residence.valid?
expect(residence.errors[:postal_code].size).to eq(1)
expect(residence.errors[:postal_code]).to include("Please, to verify your account you need to be in the census of the Madrid town.")
expect(residence.errors[:postal_code]).to include("To verify your account you need to be in the census of the Madrid town.")
end
end

View File

@@ -262,85 +262,8 @@ describe User do
end
end
describe "verification levels" do
it "residence_verified? is true only if residence_verified_at" do
user = create(:user, residence_verified_at: Time.now)
expect(user.residence_verified?).to eq(true)
user = create(:user, residence_verified_at: nil)
expect(user.residence_verified?).to eq(false)
end
it "sms_verified? is true only if confirmed_phone" do
user = create(:user, confirmed_phone: "123456789")
expect(user.sms_verified?).to eq(true)
user = create(:user, confirmed_phone: nil)
expect(user.sms_verified?).to eq(false)
end
it "level_two_verified? is true only if residence_verified_at and confirmed_phone" do
user = create(:user, confirmed_phone: "123456789", residence_verified_at: Time.now)
expect(user.level_two_verified?).to eq(true)
user = create(:user, confirmed_phone: nil, residence_verified_at: Time.now)
expect(user.level_two_verified?).to eq(false)
user = create(:user, confirmed_phone: "123456789", residence_verified_at: nil)
expect(user.level_two_verified?).to eq(false)
end
it "level_three_verified? is true only if verified_at" do
user = create(:user, verified_at: Time.now)
expect(user.level_three_verified?).to eq(true)
user = create(:user, verified_at: nil)
expect(user.level_three_verified?).to eq(false)
end
it "unverified? is true only if not level_three_verified and not level_two_verified" do
user = create(:user, verified_at: nil, confirmed_phone: nil)
expect(user.unverified?).to eq(true)
user = create(:user, verified_at: Time.now, confirmed_phone: "123456789", residence_verified_at: Time.now)
expect(user.unverified?).to eq(false)
end
it "verification_email_sent? is true only if user has email_verification_token" do
user = create(:user, email_verification_token: "xxxxxxx")
expect(user.verification_email_sent?).to eq(true)
user = create(:user, email_verification_token: nil)
expect(user.verification_email_sent?).to eq(false)
end
it "verification_sms_sent? is true only if user has unconfirmed_phone and sms_confirmation_code" do
user = create(:user, unconfirmed_phone: "666666666", sms_confirmation_code: "666")
expect(user.verification_sms_sent?).to eq(true)
user = create(:user, unconfirmed_phone: nil, sms_confirmation_code: "666")
expect(user.verification_sms_sent?).to eq(false)
user = create(:user, unconfirmed_phone: "666666666", sms_confirmation_code: nil)
expect(user.verification_sms_sent?).to eq(false)
user = create(:user, unconfirmed_phone: nil, sms_confirmation_code: nil)
expect(user.verification_sms_sent?).to eq(false)
end
it "verification_letter_sent? is true only if user has letter_requested_at and letter_verification_code" do
user = create(:user, letter_requested_at: Time.now, letter_verification_code: "666")
expect(user.verification_letter_sent?).to eq(true)
user = create(:user, letter_requested_at: nil, letter_verification_code: "666")
expect(user.verification_letter_sent?).to eq(false)
user = create(:user, letter_requested_at: Time.now, letter_verification_code: nil)
expect(user.verification_letter_sent?).to eq(false)
user = create(:user, letter_requested_at: nil, letter_verification_code: nil)
expect(user.verification_letter_sent?).to eq(false)
end
describe "verification" do
it_behaves_like "verifiable"
end
describe "cache" do