solidus_meta 1.1.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a184b34b3cc96543d01321991267b34e00f9e4f70e9525842dd656fc5aa243e4
4
- data.tar.gz: 1be9681add46e69481edd0868f79fa77f4bb7c2e5bb585406536f305b71fd270
3
+ metadata.gz: 14bb27e1abde5b941ef688e94da4a4b8649d96eca6fc89a5c85425ee6641b6b8
4
+ data.tar.gz: 76c4b9d5798e65d063ef2047837f629647c3865fb513e23f986794e0c755daf2
5
5
  SHA512:
6
- metadata.gz: a62b7241a870ba49481770ac8185f72dfb132073ec6de3ebcb56ee448383704646398a17d74e15512058670ef5ab4158488cb5bbb0b9cd7d16e8b264bd47713e
7
- data.tar.gz: 23cf1fff325b7cd284afbdf408895954212979b8ded7d7053b4f1eb0a9d952ec3b03161a545a8bf240ef1dd3f8cce9fe7162552a05eacf9eaf014ff2e7d7aad5
6
+ metadata.gz: 6ba7f2f85a1fedb47d3fcb66da1b3190f6d210315c922888b50147418c8490292ef990ffcc814719e000f13f6907c8c9f56710dd29383a730839c60f8b638fe4
7
+ data.tar.gz: 896b524d0088eeae20c0364743b8f3afda163f465129abbefbbed7a823e768cd09aaf6a113ff3926cc4047a692ce385d35597d1f49399241855eb711480c4024
data/README.md CHANGED
@@ -1,6 +1,17 @@
1
1
  # SolidusMeta
2
2
 
3
- Envia eventos da [Conversions API da Meta](https://developers.facebook.com/docs/marketing-api/conversions-api) a partir de pedidos do Solidus: `AddToCart`, `InitiateCheckout`, `AddPaymentInfo` e `Purchase`.
3
+ Envia eventos para a Meta a partir de uma loja Solidus, pela [Conversions API](https://developers.facebook.com/docs/marketing-api/conversions-api) (servidor) e pelo Pixel (navegador): `ViewContent`, `AddToCart`, `InitiateCheckout`, `AddPaymentInfo`, `Purchase`, `CompleteRegistration` e `Search`.
4
+
5
+ ## Por que enviar pelo navegador e pelo servidor
6
+
7
+ Cada lado tem informações que o outro não tem, e a Meta atribui melhor as conversões quando recebe os dois:
8
+
9
+ - **Navegador (Pixel):** roda no navegador do cliente e fala direto com a Meta, que reconhece a pessoa pela sessão do Facebook/Instagram, principalmente no navegador interno desses apps, de onde vem boa parte do tráfego de anúncio no celular. Também identifica visitantes anônimos, que ainda não informaram email ou telefone.
10
+ - **Servidor (Conversions API):** tem os dados do pedido (email, telefone, endereço), não é afetado por bloqueador de anúncio nem pelos limites de cookie do Safari, e envia mesmo quando o cliente já saiu da página, como num PIX confirmado depois por um job.
11
+
12
+ Os dois são enviados com o mesmo `event_id`, então a Meta junta os dois numa conversão só, sem contar duas vezes. Isso só funciona dentro de 48 horas entre um envio e outro, por isso o `Purchase` no navegador só dispara para pedidos completados no último dia.
13
+
14
+ No Gerenciador de Eventos, a "cobertura de eventos" mede a parte dos eventos que chega pelos dois lados, e a cobertura de `event_id` mede quantos deles podem ser desduplicados. A Meta recomenda ao menos 75%.
4
15
 
5
16
  ## Instalação
6
17
 
@@ -18,14 +29,30 @@ SolidusMeta.configure do |config|
18
29
  config.pixel_id = ENV["META_PIXEL_ID"]
19
30
  config.access_token = ENV["META_ACCESS_TOKEN"]
20
31
  config.store_url = "https://minhaloja.com.br"
32
+ config.external_id = ->(order) { order.user_id&.to_s || order.guest_token }
21
33
  end
22
34
  ```
23
35
 
36
+ `external_id` identifica o cliente entre eventos. O padrão é o id do usuário ou o `guest_token` do pedido. Use um valor que o navegador também conheça, para o pixel enviar o mesmo id. Valores que já são um SHA-256 (64 caracteres hexadecimais) são enviados como estão, sem hash de novo.
37
+
24
38
  `store_url` é usado para montar o `event_source_url` de cada evento. Se não for configurado, usa a url da loja do pedido (`order.store.url`). Quando a url vem sem protocolo, a gem adiciona `https://`.
25
39
 
26
40
  ## Pixel
27
41
 
28
- A gem envia os eventos pelo servidor, mas o pixel da Meta também precisa estar instalado no `<head>` do `app/views/layouts/application.html.erb`. É ele que cria os cookies `_fbp` e `_fbc`, que a gem envia junto de cada evento para a Meta conseguir atribuir a conversão ao clique no anúncio.
42
+ O pixel da Meta precisa estar no `<head>` do layout. Ele cria os cookies `_fbp` e `_fbc`, que a gem envia junto de cada evento. Passe os dados do cliente no `init` (correspondência avançada; o pixel faz o hash):
43
+
44
+ ```erb
45
+ fbq('init', '<%= j ENV["META_PIXEL_ID"] %>', <%= meta_pixel_user_data(order: current_order, user: current_user).to_json.html_safe %>);
46
+ fbq('track', 'PageView');
47
+ ```
48
+
49
+ E, dentro do `<body>`, o tag que dispara no navegador os eventos enviados pelo servidor, com o mesmo `eventID` para a Meta desduplicar:
50
+
51
+ ```erb
52
+ <%= meta_pixel_events_tag %>
53
+ ```
54
+
55
+ Ele precisa ficar no `<body>`: com Turbo, scripts do `<head>` só rodam no carregamento completo da página.
29
56
 
30
57
  Obs.: O id do pixel precisa estar dessa forma: `<%= j ENV["NOME_DA_ENV"] %>`. Escrever `ENV["NOME_DA_ENV"]` direto no `<script>` não funciona, porque o navegador não tem acesso ao `ENV`.
31
58
 
@@ -39,28 +66,42 @@ class ApplicationController < ActionController::Base
39
66
  end
40
67
  ```
41
68
 
42
- E dispare o `EventJob` nos pontos do fluxo da loja. O `meta_tracking_data` lê os cookies `_fbp` e `_fbc` e, se o app usa [Ahoy](https://github.com/ankane/ahoy), a visita atual, para enviar IP e user agent do cliente:
69
+ O helper também grava o cookie `_fbc` pelo servidor quando a url chega com `fbclid`.
70
+
71
+ Nos pontos do fluxo da loja, chame `track_meta_event`. Ele enfileira o envio pela Conversions API (com cookies `_fbp`/`_fbc` e IP e user agent da visita do Ahoy ou, sem ela, da requisição) e agenda o mesmo evento, com o mesmo `event_id`, para o pixel disparar na próxima página renderizada:
43
72
 
44
73
  ```ruby
45
- SolidusMeta::EventJob.perform_later("AddToCart", line_item_id: line_item.id, quantity:, **meta_tracking_data)
46
- SolidusMeta::EventJob.perform_later("InitiateCheckout", order_id: order.id, **meta_tracking_data)
47
- SolidusMeta::EventJob.perform_later("AddPaymentInfo", payment_id: payment.id, **meta_tracking_data)
48
- SolidusMeta::EventJob.perform_later("Purchase", order_id: order.id, **meta_tracking_data)
74
+ track_meta_event("ViewContent", variant_ids: variants.map(&:id), order_id: current_order&.id)
75
+ track_meta_event("AddToCart", line_item_id: line_item.id, quantity:)
76
+ track_meta_event("InitiateCheckout", order_id: order.id)
77
+ track_meta_event("AddPaymentInfo", payment_id: payment.id)
78
+ track_meta_event("Purchase", order_id: order.id)
79
+ track_meta_event("CompleteRegistration", user:, order_id: current_order&.id)
80
+ track_meta_event("Search", search_string: params[:keywords], content_ids: skus, order_id: current_order&.id)
49
81
  ```
50
82
 
83
+ Requisições de prefetch do Turbo (`X-Sec-Purpose: prefetch`) não disparam evento.
84
+
85
+ Na página do pedido, chame `track_meta_purchase_in_browser(order)`. Ele dispara o `Purchase` no navegador para pedidos completados no último dia que ainda não foram disparados nessa sessão, o que cobre pedidos confirmados por job (PIX pago depois).
86
+
51
87
  ### Eventos suportados
52
88
 
53
89
  | Evento | Quando chamar | Parâmetros |
54
90
  |---|---|---|
91
+ | `ViewContent` | Página de produto | `variant_ids:` (variantes que podem ser compradas; a primeira define `value`) e, opcionalmente, `order_id:` |
55
92
  | `AddToCart` | Item adicionado ao carrinho | `line_item_id:`, `quantity:` |
56
93
  | `InitiateCheckout` | Entrou no checkout | `order_id:` |
57
94
  | `AddPaymentInfo` | Pagamento criado com sucesso (PIX gerado, cartão aprovado ou em análise) | `payment_id:` |
58
95
  | `Purchase` | Pedido completado (`order.complete` retornou `true`) | `order_id:` |
96
+ | `Search` | Busca feita pelo cliente | `search_string:`, `content_ids:` (SKUs dos primeiros resultados) e, opcionalmente, `order_id:` |
97
+ | `CompleteRegistration` | Conta criada. Conta uma vez por usuário (`event_id` fixo) | `user:` e, opcionalmente, `order_id:` (o carrinho atual, para enviar endereço, telefone e `external_id`) |
98
+
99
+ Fora de um controller (por exemplo, num job que confirma um pagamento PIX), chame `SolidusMeta::EventJob.perform_later` passando `cookies_meta:` e `ahoy_visit_id:` direto, com os valores salvos durante o checkout.
59
100
 
60
- Fora de um controller (por exemplo, num job que confirma um pagamento PIX), passe `cookies_meta:` e `ahoy_visit_id:` direto, com os valores salvos durante o checkout.
101
+ O `value` é o valor dos produtos com descontos, sem frete. No `AddToCart`, é o preço vezes a quantidade adicionada.
61
102
 
62
103
  O `event_time` enviado à Meta é o momento em que o job foi enfileirado.
63
104
 
64
105
  ### Adicionando um evento
65
106
 
66
- Inclua o nome em `SolidusMeta::Event::EVENTS` e crie um método privado em `SolidusMeta::Event` com o nome em snake_case (`ViewContent` → `view_content`). Ele recebe as opções passadas ao job, define `@order` e retorna `path` e, opcionalmente, `event_id`, `fallback` e `custom_data` (que sobrescreve os campos padrão). Sem `event_id`, o id é `<número do pedido>-<event_time>`, então cada disparo conta como um evento separado. Defina um `event_id` fixo quando o evento só pode contar uma vez por pedido, como o `Purchase`.
107
+ Inclua o nome em `SolidusMeta::Event::EVENTS` e crie um método privado em `SolidusMeta::Event` com o nome em snake_case (`ViewContent` → `view_content`). Ele recebe as opções passadas ao job, define `@order` e retorna `path` e, opcionalmente, `event_id` e `custom_data` (que sobrescreve os campos padrão). Sem `event_id`, o id é `<número do pedido>-<event_time>`, então cada disparo conta como um evento separado. Defina um `event_id` fixo quando o evento só pode contar uma vez por pedido, como o `Purchase`.
@@ -2,14 +2,64 @@ module SolidusMeta
2
2
  module ControllerHelpers
3
3
  extend ActiveSupport::Concern
4
4
 
5
+ BROWSER_EVENTS_KEY = :solidus_meta_browser_events
6
+ TRACKED_PURCHASES_KEY = :solidus_meta_tracked_purchases
7
+
8
+ included do
9
+ before_action :store_meta_click_id
10
+ helper SolidusMeta::PixelHelper
11
+ end
12
+
5
13
  private
6
14
 
15
+ def track_meta_event(event_name, **options)
16
+ return if request.headers["X-Sec-Purpose"] == "prefetch"
17
+
18
+ browser_event = SolidusMeta::Event.new.browser_data(event_name, **options)
19
+ event_id = browser_event[:event_id] || SecureRandom.uuid
20
+
21
+ SolidusMeta::EventJob.perform_later(event_name, event_id:, **options, **meta_tracking_data)
22
+ queue_meta_browser_event(event_name, event_id, browser_event[:custom_data])
23
+ mark_meta_purchase_tracked(event_id) if event_name == "Purchase"
24
+ end
25
+
26
+ def track_meta_purchase_in_browser(order)
27
+ return unless order.complete? && order.completed_at > 1.day.ago
28
+ return if Array(session[TRACKED_PURCHASES_KEY]).include?(order.number)
29
+
30
+ browser_event = SolidusMeta::Event.new.browser_data("Purchase", order_id: order.id)
31
+ queue_meta_browser_event("Purchase", browser_event[:event_id], browser_event[:custom_data])
32
+ mark_meta_purchase_tracked(order.number)
33
+ end
34
+
35
+ def queue_meta_browser_event(event_name, event_id, custom_data)
36
+ session[BROWSER_EVENTS_KEY] = Array(session[BROWSER_EVENTS_KEY]).last(4) + [{"name" => event_name, "event_id" => event_id, "custom_data" => custom_data.as_json}]
37
+ end
38
+
39
+ # O cookie de sessão tem limite de 4KB, então só guarda os pedidos mais recentes.
40
+ def mark_meta_purchase_tracked(order_number)
41
+ session[TRACKED_PURCHASES_KEY] = Array(session[TRACKED_PURCHASES_KEY]).last(9) + [order_number]
42
+ end
43
+
44
+ # O pixel grava o _fbc via JavaScript e o Safari apaga esses cookies em 7 dias; gravado pelo servidor ele dura os 90 dias da janela de atribuição.
45
+ def store_meta_click_id
46
+ fbclid = params[:fbclid].presence
47
+ return if fbclid.nil? || cookies[:_fbc].to_s.end_with?(".#{fbclid}")
48
+
49
+ cookies[:_fbc] = {value: "fb.1.#{(Time.current.to_f * 1000).to_i}.#{fbclid}", expires: 90.days, domain: :all}
50
+ end
51
+
7
52
  def cookies_meta
8
53
  {fbp: cookies[:_fbp], fbc: cookies[:_fbc]}
9
54
  end
10
55
 
11
56
  def meta_tracking_data
12
- {cookies_meta:, ahoy_visit_id: (current_visit&.id if respond_to?(:current_visit, true))}
57
+ {
58
+ cookies_meta:,
59
+ ahoy_visit_id: (current_visit&.id if respond_to?(:current_visit, true)),
60
+ client_ip_address: request.remote_ip,
61
+ client_user_agent: request.user_agent
62
+ }
13
63
  end
14
64
  end
15
65
  end
@@ -0,0 +1,20 @@
1
+ module SolidusMeta
2
+ module PixelHelper
3
+ def meta_pixel_user_data(order: nil, user: nil, external_id: nil)
4
+ SolidusMeta::UserData.new(order:, user:, external_id:).to_h
5
+ end
6
+
7
+ def meta_pixel_events_tag
8
+ # O Turbo pré-carrega links no hover; sem isso a fila seria consumida por uma página que talvez nunca apareça.
9
+ return if request.headers["X-Sec-Purpose"] == "prefetch"
10
+
11
+ events = session.delete(SolidusMeta::ControllerHelpers::BROWSER_EVENTS_KEY)
12
+ return if events.blank?
13
+
14
+ calls = events.map do |event|
15
+ "fbq('track', #{event["name"].to_json}, #{event["custom_data"].to_json}, {eventID: #{event["event_id"].to_json}});"
16
+ end
17
+ javascript_tag("if (window.fbq) { #{calls.join(" ")} }", nonce: true)
18
+ end
19
+ end
20
+ end
@@ -1,5 +1,5 @@
1
1
  module SolidusMeta
2
2
  class BaseJob < ActiveJob::Base
3
- retry_on Ac::TooManyRequestsError, wait: :polynomially_longer, attempts: 5
3
+ retry_on Ac::TooManyRequestsError, Ac::ServerError, SolidusMeta::ConnectionError, wait: :polynomially_longer, attempts: 8
4
4
  end
5
5
  end
@@ -24,6 +24,10 @@ module SolidusMeta
24
24
  ]
25
25
  }
26
26
  post("#{@pixel_id}/events", body:, params:)
27
+ rescue Ac::AcError => e
28
+ # Timeout e falha de conexão chegam como AcError genérico com código 0, misturados com erros 4xx que não adianta reenviar.
29
+ raise ConnectionError, e.message if e.response.response.code.zero?
30
+ raise
27
31
  end
28
32
  end
29
33
  end
@@ -1,6 +1,6 @@
1
1
  module SolidusMeta
2
2
  class Event
3
- EVENTS = %w[AddToCart InitiateCheckout AddPaymentInfo Purchase].freeze
3
+ EVENTS = %w[ViewContent AddToCart InitiateCheckout AddPaymentInfo Purchase CompleteRegistration Search].freeze
4
4
 
5
5
  attr_reader :order
6
6
 
@@ -8,26 +8,44 @@ module SolidusMeta
8
8
  @client = client
9
9
  end
10
10
 
11
- def track(event_name, cookies_meta:, ahoy_visit_id: nil, event_time: Time.current, **options)
11
+ def build(event_name, **options)
12
12
  raise Error, "evento não suportado: #{event_name}" unless EVENTS.include?(event_name)
13
13
  event = send(event_name.underscore, **options)
14
- event_id = event[:event_id] || "#{order.number}-#{event_time.to_time.to_i}"
15
14
 
16
- @client.send_event(
17
- event_name,
18
- event_time:,
19
- event_id:,
20
- event_source_url: File.join(SolidusMeta.config.store_url(order.store), event[:path]),
21
- user_data: user_data(cookies_meta, ahoy_visit_id, fallback: event[:fallback]),
15
+ {
16
+ event_id: event[:event_id],
17
+ event_source_url: File.join(SolidusMeta.config.store_url(order&.store), event[:path]),
18
+ user: event[:user],
22
19
  custom_data: {
23
- currency: order.currency,
24
- value: order.total.to_f,
25
- order_id: order.number,
26
- content_type: "product",
27
- contents: content_line_items,
28
- **address_data,
20
+ **(event.fetch(:order_data, true) ? order_custom_data : {}),
29
21
  **event.fetch(:custom_data, {})
30
22
  }.compact
23
+ }
24
+ end
25
+
26
+ def browser_data(event_name, **options)
27
+ event = build(event_name, **options)
28
+ {event_id: event[:event_id], custom_data: event[:custom_data].except(:contents)}
29
+ end
30
+
31
+ def track(event_name, cookies_meta:, event_id: nil, ahoy_visit_id: nil, client_ip_address: nil, client_user_agent: nil, event_time: Time.current, **options)
32
+ event = build(event_name, **options)
33
+ visit = Ahoy::Visit.find_by(id: ahoy_visit_id) if ahoy_visit_id && defined?(Ahoy::Visit)
34
+ cookies_meta = cookies_meta.to_h.with_indifferent_access
35
+
36
+ @client.send_event(
37
+ event_name,
38
+ event_time:,
39
+ event_id: event_id || event[:event_id] || "#{order.number}-#{event_time.to_time.to_i}",
40
+ event_source_url: event[:event_source_url],
41
+ user_data: {
42
+ **UserData.new(order:, user: event[:user]).hashed,
43
+ client_ip_address: visit&.ip || client_ip_address,
44
+ client_user_agent: visit&.user_agent || client_user_agent,
45
+ fbp: cookies_meta[:fbp],
46
+ fbc: cookies_meta[:fbc]
47
+ }.compact,
48
+ custom_data: event[:custom_data]
31
49
  )
32
50
  end
33
51
 
@@ -40,10 +58,10 @@ module SolidusMeta
40
58
  {
41
59
  event_id: "#{line_item.id}-#{line_item.updated_at.to_i}",
42
60
  path: "carrinho",
43
- fallback: "sem usuário",
44
61
  custom_data: {
45
62
  currency: line_item.currency,
46
- value: line_item.total.to_f,
63
+ value: (line_item.price * quantity.to_i).to_f,
64
+ content_ids: [line_item.sku],
47
65
  contents: [content_line_item(line_item, quantity)]
48
66
  }
49
67
  }
@@ -55,7 +73,7 @@ module SolidusMeta
55
73
  {
56
74
  path: "checkout/address/edit",
57
75
  custom_data: {
58
- num_items: order.line_items.count
76
+ num_items: order.line_items.sum(&:quantity)
59
77
  }
60
78
  }
61
79
  end
@@ -67,9 +85,6 @@ module SolidusMeta
67
85
  {
68
86
  path: "checkout/payment",
69
87
  custom_data: {
70
- currency: payment.currency,
71
- value: payment.amount.to_f,
72
- content_id: payment.number,
73
88
  payment_type: payment.payment_method.name
74
89
  }
75
90
  }
@@ -80,77 +95,86 @@ module SolidusMeta
80
95
 
81
96
  {
82
97
  event_id: order.number,
83
- path: "pedidos"
98
+ path: "pedidos/#{order.number}",
99
+ custom_data: {
100
+ num_items: order.line_items.sum(&:quantity)
101
+ }
84
102
  }
85
103
  end
86
104
 
87
- def user_data(cookies_meta, ahoy_visit_id, fallback: nil)
88
- cookies_meta = cookies_meta.to_h.with_indifferent_access
89
- visit = Ahoy::Visit.find_by(id: ahoy_visit_id) if ahoy_visit_id && defined?(Ahoy::Visit)
90
-
91
- {
92
- em: sha256(email || fallback),
93
- fn: sha256(name || fallback),
94
- ph: sha256(phone),
95
- ct: sha256(address&.city&.downcase),
96
- st: sha256(address&.state&.name&.downcase),
97
- zp: sha256(address&.zipcode),
98
- country: sha256(country),
99
- external_id: address&.id&.to_s,
100
- client_user_agent: visit&.user_agent,
101
- client_ip_address: visit&.ip,
102
- fbp: cookies_meta[:fbp],
103
- fbc: cookies_meta[:fbc]
104
- }.compact
105
- end
105
+ def complete_registration(user:, order_id: nil)
106
+ @order = Spree::Order.find_by(id: order_id) if order_id
106
107
 
107
- def address_data
108
108
  {
109
- postal_code: address&.zipcode,
110
- region: state,
111
- country:
109
+ event_id: "registration-#{user.id}",
110
+ path: "cadastro",
111
+ user:,
112
+ order_data: false,
113
+ custom_data: {
114
+ status: true
115
+ }
112
116
  }
113
117
  end
114
118
 
115
- def content_line_items
116
- order.line_items.map { |line_item| content_line_item(line_item, line_item.quantity) }
117
- end
119
+ def view_content(variant_ids:, order_id: nil)
120
+ @order = Spree::Order.find_by(id: order_id) if order_id
121
+ variants = Spree::Variant.where(id: variant_ids).includes(:product).index_by(&:id).values_at(*variant_ids).compact
122
+ main_variant = variants.first
118
123
 
119
- def content_line_item(line_item, quantity)
120
124
  {
121
- id: line_item.sku,
122
- quantity:,
123
- item_price: line_item.price.to_f
125
+ path: "produtos/#{main_variant.product.slug}",
126
+ order_data: false,
127
+ custom_data: {
128
+ currency: main_variant.default_price&.currency || Spree::Config.currency,
129
+ value: main_variant.price.to_f,
130
+ content_type: "product",
131
+ content_ids: variants.map(&:sku),
132
+ contents: variants.map { |variant| {id: variant.sku, quantity: 1, item_price: variant.price.to_f} }
133
+ }
124
134
  }
125
135
  end
126
136
 
127
- def address
128
- order.ship_address || order.user&.address
129
- end
137
+ def search(search_string:, content_ids: [], order_id: nil)
138
+ @order = Spree::Order.find_by(id: order_id) if order_id
130
139
 
131
- def state
132
- address&.state&.name&.downcase
140
+ {
141
+ path: "busca?#{{keywords: search_string}.to_query}",
142
+ order_data: false,
143
+ custom_data: {
144
+ search_string:,
145
+ content_type: "product",
146
+ content_ids:
147
+ }
148
+ }
133
149
  end
134
150
 
135
- def country
136
- address&.country&.name&.downcase
137
- end
151
+ def order_custom_data
152
+ return {} unless order
138
153
 
139
- def name
140
- (order.name || order.user&.name)&.downcase
154
+ {
155
+ currency: order.currency,
156
+ value: products_value,
157
+ order_id: order.number,
158
+ content_type: "product",
159
+ content_ids: order.line_items.map(&:sku),
160
+ contents: content_line_items
161
+ }
141
162
  end
142
163
 
143
- def email
144
- (order.email || order.user&.email)&.downcase
164
+ def products_value
165
+ (order.total - order.shipment_total - order.shipments.sum(&:promo_total)).to_f
145
166
  end
146
167
 
147
- def phone
148
- return unless address&.phone.present?
149
- "55" + address.phone.remove(/\D/).remove(/^55/)
168
+ def content_line_items
169
+ order.line_items.map { |line_item| content_line_item(line_item, line_item.quantity) }
150
170
  end
151
171
 
152
- def sha256(value)
153
- Digest::SHA256.hexdigest(value.strip) if value.present?
172
+ def content_line_item(line_item, quantity)
173
+ {
174
+ id: line_item.sku,
175
+ quantity: quantity.to_i,
176
+ item_price: line_item.price.to_f
177
+ }
154
178
  end
155
179
  end
156
180
  end
@@ -0,0 +1,79 @@
1
+ module SolidusMeta
2
+ class UserData
3
+ def initialize(order: nil, user: nil, external_id: nil)
4
+ @order = order
5
+ @user = user || order&.user
6
+ @external_id = external_id
7
+ end
8
+
9
+ def to_h
10
+ {
11
+ em: email,
12
+ ph: phone,
13
+ fn: first_name,
14
+ ln: last_name,
15
+ ct: city,
16
+ st: state,
17
+ zp: zipcode,
18
+ country:,
19
+ external_id:
20
+ }.compact_blank
21
+ end
22
+
23
+ def hashed
24
+ to_h.transform_values { |value| value.match?(/\A\h{64}\z/) ? value : Digest::SHA256.hexdigest(value) }
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :order, :user
30
+
31
+ def address
32
+ order&.ship_address || order&.bill_address || user.try(:ship_address) || user.try(:bill_address)
33
+ end
34
+
35
+ def email
36
+ (order&.email.presence || user.try(:email).presence || user.try(:email_address))&.strip&.downcase
37
+ end
38
+
39
+ def phone
40
+ digits = address&.phone.to_s.gsub(/\D/, "").sub(/\A0+/, "")
41
+ return if digits.blank?
42
+
43
+ digits = "55#{digits}" if digits.length.in?(10..11)
44
+ digits if digits.length.in?(12..13)
45
+ end
46
+
47
+ def name_parts
48
+ @name_parts ||= (order&.name.presence || address&.name.presence || user.try(:name)).to_s.downcase.split
49
+ end
50
+
51
+ def first_name
52
+ name_parts.first
53
+ end
54
+
55
+ def last_name
56
+ name_parts.last if name_parts.size > 1
57
+ end
58
+
59
+ def city
60
+ I18n.transliterate(address.city.to_s).downcase.gsub(/[^a-z]/, "") if address
61
+ end
62
+
63
+ def state
64
+ (address.state&.abbr || address.state_name).to_s.downcase if address
65
+ end
66
+
67
+ def zipcode
68
+ address&.zipcode.to_s.gsub(/\D/, "")
69
+ end
70
+
71
+ def country
72
+ (address&.country || Spree::Country.default)&.iso&.downcase
73
+ end
74
+
75
+ def external_id
76
+ @external_id.presence || (SolidusMeta.config.external_id(order) if order)
77
+ end
78
+ end
79
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module SolidusMeta
4
4
  class Configuration
5
- attr_writer :pixel_id, :access_token, :store_url
5
+ attr_writer :pixel_id, :access_token, :store_url, :external_id
6
6
 
7
7
  def pixel_id
8
8
  @pixel_id || ENV["META_PIXEL_ID"]
@@ -18,5 +18,9 @@ module SolidusMeta
18
18
  raise Error, "store_url não configurado: defina SolidusMeta.config.store_url, META_STORE_URL ou a url da loja" unless url
19
19
  url.start_with?("http") ? url : "https://#{url}"
20
20
  end
21
+
22
+ def external_id(order)
23
+ (@external_id || ->(o) { o.user_id&.to_s || o.guest_token }).call(order)&.to_s
24
+ end
21
25
  end
22
26
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidusMeta
4
- VERSION = "1.1.1"
4
+ VERSION = "2.0.0"
5
5
  end
data/lib/solidus_meta.rb CHANGED
@@ -7,6 +7,7 @@ require "solidus_meta/engine"
7
7
 
8
8
  module SolidusMeta
9
9
  class Error < StandardError; end
10
+ class ConnectionError < Error; end
10
11
 
11
12
  def self.config
12
13
  @config ||= Configuration.new
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solidus_meta
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - marialuiza1324
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-29 00:00:00.000000000 Z
11
+ date: 2026-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ac
@@ -61,10 +61,12 @@ extra_rdoc_files: []
61
61
  files:
62
62
  - README.md
63
63
  - app/controllers/concerns/solidus_meta/controller_helpers.rb
64
+ - app/helpers/solidus_meta/pixel_helper.rb
64
65
  - app/jobs/solidus_meta/base_job.rb
65
66
  - app/jobs/solidus_meta/event_job.rb
66
67
  - app/models/solidus_meta/client.rb
67
68
  - app/models/solidus_meta/event.rb
69
+ - app/models/solidus_meta/user_data.rb
68
70
  - lib/solidus_meta.rb
69
71
  - lib/solidus_meta/configuration.rb
70
72
  - lib/solidus_meta/engine.rb