Class: Marketing::RecordEventAttendance

Inherits:
Object
  • Object
show all
Defined in:
app/services/marketing/record_event_attendance.rb

Overview

Records that a customer was at a trade show — as a TOUCH, dated when the
touch happened, without changing anyone's source.

This is the answer to the case the window rules cannot serve: a customer
reaching out a year later saying "I saw you at KBIS 2022". That is a true
statement and it must be recordable, but it is not a claim that the show
ACQUIRED them — their acquisition source was settled at creation and is
write-once. The show TOUCHED them, and touches are joined, never written.

So Source#attributable_at? stays strict about sourcing while this stays
open about attribution: attendance can be recorded at any time, for a show of
any age, and it shows up in campaign_influenced_invoices against that show
without rewriting a single source_id. Enforcement that makes people record
something false is worse than no enforcement.

The show is reached through a Campaign because the influence view keys on
campaigns. An event campaign points at the show's existing source rather
than generating one (Campaign#generate_source is skipped when source_id
is already set), so nothing new appears in the source tree.

Examples:

A customer mentions a show they attended last year

Marketing::RecordEventAttendance.call(
  source: Source.find_by(full_name: 'Trade Show > KBIS > KBIS 2022 Orlando, FL (February 8-10)'),
  customer: customer,
  at: Date.new(2022, 2, 9)
)

Defined Under Namespace

Classes: NotAnEventError

Constant Summary collapse

AUDIENCE_SUFFIX =

Suffix for the audience holding a show's attendees. One per show, created
on first use — a badge-scan import and a customer's own recollection land
in the same place.

'attendees'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source, customer, at) ⇒ RecordEventAttendance

Returns a new instance of RecordEventAttendance.

Parameters:



52
53
54
55
56
# File 'app/services/marketing/record_event_attendance.rb', line 52

def initialize(source, customer, at)
  @source = source
  @customer = customer
  @at = at
end

Class Method Details

.call(source:, customer:, at: nil) ⇒ AudienceMember

Returns the recorded touch, existing or new.

Parameters:

  • source (Source)

    a node in the Trade Show subtree

  • customer (Customer)
  • at (Time, Date) (defaults to: nil)

    when the touch happened — the show's dates for a
    badge scan, or whenever the customer says they saw us. Defaults to the
    show's start, since that is when attendance actually occurred.

Returns:

Raises:



45
46
47
# File 'app/services/marketing/record_event_attendance.rb', line 45

def self.call(source:, customer:, at: nil)
  new(source, customer, at).call
end

Instance Method Details

#callAudienceMember

Returns:

Raises:



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'app/services/marketing/record_event_attendance.rb', line 59

def call
  raise NotAnEventError, "#{@source&.full_name.inspect} is not a trade show" unless @source&.event?

  # The WHOLE resolution runs under the campaign lock — audience and member
  # both. Rails implements find_or_create_by! as `find_by || create!`, so the
  # create path runs model validations: if a concurrent request commits
  # between our SELECT and our INSERT, AudienceMember's per-audience
  # uniqueness validation fails and raises RecordInvalid *before* the database
  # constraint is reached. Rescuing RecordNotUnique alone does not cover that
  # window; serialising does.
  campaign.with_lock do
    member = audience.audience_members.find_or_create_by!(customer_id: @customer.id)
    # The touch is dated when it HAPPENED, not when it was typed in. A show
    # recalled a year later must join the influence report at the show's date,
    # or it would look like a touch that post-dates every deal it influenced.
    member.update_columns(created_at: touched_at) if member.created_at != touched_at
    member
  end
end