Class: Tracking::ConsentPreferences

Inherits:
Object
  • Object
show all
Defined in:
app/services/tracking/consent_preferences.rb

Overview

Handles consent preferences at the Party level
Uses jsonb_accessor for typed access to consent_preferences JSONB column

Constant Summary collapse

'opt_in'
'opt_out'
'implied'
GDPR_COUNTRIES =

EU/EEA countries requiring GDPR consent

%w[AT BE BG HR CY CZ DK EE FI FR DE GR HU IE IT LV LT LU MT NL PL PT RO SK SI ES SE GB IS LI NO CH].freeze
MODE_STRICTNESS =

Consent mode strictness ranking (higher = stricter)

{
  CONSENT_MODE_OPT_OUT => 1,   # Least strict - implied consent, opt-out
  CONSENT_MODE_IMPLIED => 2,   # Medium - implied consent
  CONSENT_MODE_OPT_IN => 3     # Most strict - requires explicit opt-in
}.freeze

Class Method Summary collapse

Class Method Details

.build_defaults(mode, country, region) ⇒ Hash

Build default consent preferences based on mode (without persisting)

Parameters:

  • mode (String)

    'opt_in', 'opt_out', or 'implied'

  • country (String)

    2-letter country code

  • region (String)

    Region/state code

Returns:

  • (Hash)

    Default preferences



109
110
111
112
113
114
115
116
117
118
119
120
# File 'app/services/tracking/consent_preferences.rb', line 109

def build_defaults(mode, country, region)
  is_permissive = mode != CONSENT_MODE_OPT_IN
  {
    'consent_mode' => mode,
    'consent_country' => country,
    'consent_region' => region,
    'consent_analytics' => is_permissive,
    'consent_marketing' => is_permissive
    # NOTE: consent_updated_at intentionally NOT set on auto-defaults
    # Only set when user explicitly interacts with consent banner
  }
end

.determine_mode(country, region) ⇒ String

Determine consent mode based on visitor location

Parameters:

  • country (String)

    2-letter country code

  • region (String)

    Region/state code

Returns:

  • (String)

    'opt_in', 'opt_out', or 'implied'



27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'app/services/tracking/consent_preferences.rb', line 27

def determine_mode(country, region)
  country = country&.upcase
  region = region&.upcase

  if gdpr_country?(country) || quebec?(country, region)
    CONSENT_MODE_OPT_IN
  elsif country == 'US'
    CONSENT_MODE_OPT_OUT
  elsif country == 'CA'
    CONSENT_MODE_IMPLIED
  else
    CONSENT_MODE_OPT_OUT # Default for unknown regions
  end
end

.initialize_defaults(party, country:, region:) ⇒ Hash

Initialize default consent preferences for a Party based on geo
Uses typed jsonb_accessor fields for clean access

Parameters:

  • party (Party)

    The party to update

  • country (String)

    2-letter country code

  • region (String)

    Region/state code

Returns:

  • (Hash)

    The consent preferences hash



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'app/services/tracking/consent_preferences.rb', line 82

def initialize_defaults(party, country:, region:)
  return party.consent_preferences if party.consent_preferences.present?

  mode = determine_mode(country, region)
  is_permissive = mode != CONSENT_MODE_OPT_IN

  # Use typed accessors from jsonb_accessor gem
  # NEVER set consent_updated_at on auto-defaults - only set when user explicitly
  # interacts with consent banner (via update_consent method)
  party.update_columns(
    consent_preferences: {
      'consent_mode' => mode,
      'consent_country' => country,
      'consent_region' => region,
      'consent_analytics' => is_permissive,
      'consent_marketing' => is_permissive
    }
  )

  party.consent_preferences
end

.requires_reconsent?(party, current_country:, current_region:) ⇒ Boolean

Check if user needs to re-consent due to moving to a stricter region

Parameters:

  • party (Party)

    The party to check

  • current_country (String)

    Current visitor country

  • current_region (String)

    Current visitor region

Returns:

  • (Boolean)

    true if re-consent is required



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'app/services/tracking/consent_preferences.rb', line 57

def requires_reconsent?(party, current_country:, current_region:)
  return false if party.blank? || party.consent_preferences.blank?

  stored_mode = party.consent_mode
  current_mode = determine_mode(current_country, current_region)

  # If moving to stricter region, check if they've consented at that level
  return false unless stricter_mode?(current_mode, stored_mode)

  # They're in a stricter region - did they explicitly consent there?
  # Check if stored consent was from an opt-in action (not implicit)
  stored_country = party.consent_country
  stored_region = party.consent_region

  # If their stored consent was from a permissive region, they need re-consent
  stored_consent_mode = determine_mode(stored_country, stored_region)
  stored_consent_mode != CONSENT_MODE_OPT_IN
end

.stricter_mode?(new_mode, stored_mode) ⇒ Boolean

Check if a consent mode is stricter than another

Parameters:

  • new_mode (String)

    The new consent mode

  • stored_mode (String)

    The stored consent mode

Returns:

  • (Boolean)

    true if new_mode is stricter



46
47
48
49
50
# File 'app/services/tracking/consent_preferences.rb', line 46

def stricter_mode?(new_mode, stored_mode)
  return false if new_mode.blank? || stored_mode.blank?

  MODE_STRICTNESS.fetch(new_mode, 0) > MODE_STRICTNESS.fetch(stored_mode, 0)
end

Update consent preferences when user changes them
Uses typed accessors for clean updates

Parameters:

  • party (Party)

    The party to update

  • analytics (Boolean)

    Analytics consent

  • marketing (Boolean)

    Marketing consent

  • mode (String) (defaults to: nil)

    Consent mode

  • country (String) (defaults to: nil)

    Country code

  • region (String) (defaults to: nil)

    Region code

Returns:

  • (Hash)

    Updated preferences



131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'app/services/tracking/consent_preferences.rb', line 131

def update_consent(party, analytics:, marketing:, mode: nil, country: nil, region: nil)
  updates = {
    consent_analytics: analytics,
    consent_marketing: marketing,
    consent_updated_at: Time.current
  }
  updates[:consent_mode] = mode if mode.present?
  updates[:consent_country] = country if country.present?
  updates[:consent_region] = region if region.present?

  # Update using typed accessors
  party.update!(updates)
  party.consent_preferences
end