Class: Certification

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable, Models::LiquidMethods
Defined in:
app/models/certification.rb

Overview

== Schema Information

Table name: certifications
Database name: primary

id :bigint not null, primary key
attachment_name :string
attachment_uid :string
certified_by :integer
certified_on :datetime
expiration_reminder_10_days_sent :boolean default(FALSE), not null
expiration_reminder_10_days_sent_on :datetime
expiration_reminder_30_days_sent :boolean default(FALSE), not null
expiration_reminder_30_days_sent_on :datetime
expired_reminder_sent :boolean default(FALSE), not null
expired_reminder_sent_on :datetime
expires_at :datetime
reference :string
state :string
title :string
welcome_meeting :boolean default(FALSE), not null
welcome_meeting_date :datetime
workshop_attended :boolean
workshop_attended_date :datetime
created_at :datetime not null
updated_at :datetime not null
course_enrollment_id :integer
party_id :integer

Indexes

idx_party_id (party_id)

An installer's certification credential, issued after passing a course exam.

Lifecycle: pending → requested → active → expired (or cancelled from
pending/requested when staff reject the request). Activation is gated by
#can_be_activated?: active liability insurance, a welcome meeting, workshop
attendance, and a signed installer agreement. On activation the certification
receives a CER#### reference number, a one-year expiry, and the customer a
congratulations email; expiry reminders are handled by
CertificationCheckWorker.

Defined Under Namespace

Classes: InsuranceEscalation

Constant Summary

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Models::Schedulable

Models::Schedulable::SIMPLE_FORM_OPTIONS

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has one collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Class Method Details

.activeActiveRecord::Relation<Certification>

A relation of Certifications that are active. Active Record Scope

Returns:

See Also:



73
# File 'app/models/certification.rb', line 73

scope :active, -> { where(state: 'active') }

.expiredActiveRecord::Relation<Certification>

A relation of Certifications that are expired. Active Record Scope

Returns:

See Also:



74
# File 'app/models/certification.rb', line 74

scope :expired, -> { where(state: 'expired') }

.requestedActiveRecord::Relation<Certification>

A relation of Certifications that are requested. Active Record Scope

Returns:

See Also:



75
# File 'app/models/certification.rb', line 75

scope :requested, -> { where(state: 'requested') }

.titleString?

Class-level title fallback used by legacy call sites.

Returns:

  • (String, nil)


280
281
282
# File 'app/models/certification.rb', line 280

def self.title
  title || course_enrollment.course&.name
end

Instance Method Details

#activation_blockersArray<String>

Human-readable list of unmet activation requirements, for surfacing to
staff when an activation attempt is refused.

Returns:

  • (Array<String>)

    empty when the certification can be activated



146
147
148
149
150
151
152
153
154
155
156
157
# File 'app/models/certification.rb', line 146

def activation_blockers
  blockers = []
  # There needs to be at least 1 liability insurance active at this moment
  blockers << 'active proof of liability insurance' unless liability_ok?
  # An online welcome meeting needs to happen before being activated
  blockers << 'welcome meeting' unless welcome_meeting_ok?
  # Pro needs to attend at least one workshop
  blockers << 'workshop attendance' unless workshop_attended_ok?
  # The agreement needs to be signed and uploaded
  blockers << 'signed installer agreement' unless agreement_ok?
  blockers
end

#agreementAgreement

The installer agreement that must be signed before activation.

Returns:

See Also:



69
# File 'app/models/certification.rb', line 69

has_one :agreement, as: :resource

#agreement_ok?Boolean

Whether the installer agreement exists and has been signed.

Returns:

  • (Boolean)


183
184
185
# File 'app/models/certification.rb', line 183

def agreement_ok?
  agreement.present? && agreement.signed?
end

#can_be_activated?Boolean

Whether every activation requirement is met (state-machine guard for
the activate event).

Returns:

  • (Boolean)


138
139
140
# File 'app/models/certification.rb', line 138

def can_be_activated?
  activation_blockers.empty?
end

Customer-portal URL of this certification (exposed to Liquid templates).

Returns:

  • (String)


287
288
289
# File 'app/models/certification.rb', line 287

def certificate_link
  "#{WEB_URL}/my_account/certifications/#{id}"
end

#contactParty

Same-FK alias of #party used by contact-centric call sites.

Deliberately declared without inverse_of:contact is a same-FK alias
of customer/party (all three resolve party_id to the STI-typed Party).
The base Party class has no :certifications association — only the
subclasses (Contact/Customer/Employee) do — so inverse_of: :certifications
raised "Could not find the inverse association for contact (:certifications
in Party)" whenever #contact was touched (AppSignal #6034). The sibling
:customer/:party work fine without it.

Returns:

See Also:



66
# File 'app/models/certification.rb', line 66

belongs_to :contact, class_name: 'Party', foreign_key: 'party_id', optional: true

#course_enrollmentCourseEnrollment

The enrollment this certification was earned through.



52
# File 'app/models/certification.rb', line 52

belongs_to :course_enrollment, optional: true

#create_agreementvoid

This method returns an undefined value.

Builds the installer agreement and sends it out for e-signature
(after_create callback).



212
213
214
215
216
217
218
# File 'app/models/certification.rb', line 212

def create_agreement
  agreement = build_agreement(sign_method: 'esignatures', template: 'installer_agreement')
  agreement.agreement_participants.new(
    party_id: party_id
  )
  agreement.send_to_esignatures if agreement.save
end

#create_welcome_meeting_activityActivity

Creates the welcome-meeting task for staff (fired on request).

Returns:



319
320
321
322
323
324
325
# File 'app/models/certification.rb', line 319

def create_welcome_meeting_activity
  Activity.create(lock_target_datetime: true,
                  activity_type_id: ActivityType.find_by(task_type: 'INSTALL_CERTIFICATION_PM')&.id,
                  party: party,
                  resource: self,
                  notes: "Welcome meeting for new certification request. Please arrange meeting.")
end

#create_workshop_activityActivity

Creates the workshop-scheduling task for staff (fired on request).

Returns:



308
309
310
311
312
313
314
# File 'app/models/certification.rb', line 308

def create_workshop_activity
  Activity.create(lock_target_datetime: true,
                  activity_type_id: ActivityType.find_by(task_type: 'INSTALL_CERTIFICATION_PWS')&.id,
                  party: party,
                  resource: self,
                  notes: "New certification requires workshop to attend. Please schedule and verify this customer attends.")
end

#customerParty

Same-FK alias of #party used by customer-centric call sites.

Returns:

See Also:



56
# File 'app/models/certification.rb', line 56

belongs_to :customer, class_name: 'Party', foreign_key: 'party_id', optional: true

#liability_ok?Boolean

Whether the installer has liability insurance effective right now.

Returns:

  • (Boolean)


162
163
164
# File 'app/models/certification.rb', line 162

def liability_ok?
  party.customer.liability_insurances.active.any? && customer.liability_insurances.effective_on(Time.current).present?
end

#partyParty

The certified installer (STI Party: Customer/Contact/Employee).

Returns:

See Also:



54
# File 'app/models/certification.rb', line 54

belongs_to :party, optional: true

#possible_eventsArray<Symbol>

State-machine events currently available from this record's state.

Returns:

  • (Array<Symbol>)


294
295
296
# File 'app/models/certification.rb', line 294

def possible_events
  state_transitions.map(&:event).sort
end

#possible_events_for_selectArray<Array(String, Symbol)>

#possible_events shaped for a select input.

Returns:

  • (Array<Array(String, Symbol)>)


301
302
303
# File 'app/models/certification.rb', line 301

def possible_events_for_select
  possible_events.map { |evt| [evt.to_s.titleize, evt] }
end

#send_admin_alertvoid

This method returns an undefined value.

Alerts staff that a new certification was requested (fired on the
request transition).



273
274
275
# File 'app/models/certification.rb', line 273

def send_admin_alert
  TrainingMailer.new_certification_request(self).deliver_later!
end

#send_congratulations_emailCommunication?

Emails the installer their NEW_INSTALLER congratulations (fired on activation).

Returns:



223
224
225
226
227
228
229
# File 'app/models/certification.rb', line 223

def send_congratulations_email
  customer = party
  CommunicationBuilder.new(resource: self,
                                  recipient_party: customer,
                                  use_best_email: true,
                                  template_system_code: 'NEW_INSTALLER').create
end

#send_expiration_reminderBoolean

Notifies staff and the installer that the certification expired, and
stamps the sent flag (fired on the active → expired transition).

Returns:

  • (Boolean)

    result of the flag update



259
260
261
262
263
264
265
266
267
# File 'app/models/certification.rb', line 259

def send_expiration_reminder
  TrainingMailer.certification_expired(self).deliver_later!
  customer = party
  CommunicationBuilder.new(resource: self,
                                  recipient_party: customer,
                                  use_best_email: true,
                                  template_system_code: 'CERT_EXPIRED').create
  update(expired_reminder_sent: true, expired_reminder_sent_on: Time.current)
end

#send_expiration_reminder_10Boolean

Emails the 10-days-to-expiry reminder and stamps the sent flag.

Returns:

  • (Boolean)

    result of the flag update



246
247
248
249
250
251
252
253
# File 'app/models/certification.rb', line 246

def send_expiration_reminder_10
  customer = party
  CommunicationBuilder.new(resource: self,
                                  recipient_party: customer,
                                  use_best_email: true,
                                  template_system_code: 'CERT_EXPIRES_10').create
  update(expiration_reminder_10_days_sent: true, expiration_reminder_10_days_sent_on: Time.current)
end

#send_expiration_reminder_30Boolean

Emails the 30-days-to-expiry reminder and stamps the sent flag.

Returns:

  • (Boolean)

    result of the flag update



234
235
236
237
238
239
240
241
# File 'app/models/certification.rb', line 234

def send_expiration_reminder_30
  customer = party
  CommunicationBuilder.new(resource: self,
                                  recipient_party: customer,
                                  use_best_email: true,
                                  template_system_code: 'CERT_EXPIRES_30').create
  update(expiration_reminder_30_days_sent: true, expiration_reminder_30_days_sent_on: Time.current)
end

#set_partyvoid

This method returns an undefined value.

Derives the certified party from the enrollment (before_create callback).



204
205
206
# File 'app/models/certification.rb', line 204

def set_party
  self.party_id = course_enrollment.party.id if course_enrollment.party.present? && party_id.nil?
end

#set_reference_numberBoolean

Assigns the public CER#### certificate number (fired on activation).

Returns:

  • (Boolean)

    result of the update



197
198
199
# File 'app/models/certification.rb', line 197

def set_reference_number
  update(reference: "CER#{id.to_s.rjust(4, '0')}")
end

#set_titlevoid

This method returns an undefined value.

Defaults the title to the course name (before_create callback).



190
191
192
# File 'app/models/certification.rb', line 190

def set_title
  self.title = course_enrollment&.course&.name if title.blank?
end

#welcome_meeting_ok?Boolean

Whether the onboarding welcome meeting has taken place.

Returns:

  • (Boolean)


169
170
171
# File 'app/models/certification.rb', line 169

def welcome_meeting_ok?
  welcome_meeting.to_b
end

#workshop_attended_ok?Boolean

Whether the installer attended at least one workshop.

Returns:

  • (Boolean)


176
177
178
# File 'app/models/certification.rb', line 176

def workshop_attended_ok?
  workshop_attended.to_b
end