solidus_meta 1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b9df7c2815f8fcf61690d5f9728822ed9d2e38c2395bfd3a903e7765b8e629fc
4
+ data.tar.gz: 79881489f2cd955db11445883dd359fa415392525b33728d734f5ebcb94be54c
5
+ SHA512:
6
+ metadata.gz: 27f7e2c7c2c6301b40f1053d8dec9a531acb5cf34081b2ae5bceddccb8e8a9f022fca3dddfd6ca543690cda4542723652bf586fa389e93015e3a38326723a6dc
7
+ data.tar.gz: 9dfbf78a4f965421222ee53d00051727a5600c6e3f3b216a7329ad5063d199da37bd21094bffd130f916c493572cc7df7f13e19bd8611f38afeaf149c0538e2d
data/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # SolidusMeta
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`.
4
+
5
+ ## Instalação
6
+
7
+ ```ruby
8
+ gem "solidus_meta"
9
+ ```
10
+
11
+ ## Configuração
12
+
13
+ Por padrão lê `META_PIXEL_ID`, `META_ACCESS_TOKEN` e `META_STORE_URL` do ambiente. Para sobrescrever:
14
+
15
+ ```ruby
16
+ # config/initializers/solidus_meta.rb
17
+ SolidusMeta.configure do |config|
18
+ config.pixel_id = ENV["META_PIXEL_ID"]
19
+ config.access_token = ENV["META_ACCESS_TOKEN"]
20
+ config.store_url = "https://minhaloja.com.br"
21
+ end
22
+ ```
23
+
24
+ `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
+
26
+ ## Pixel
27
+
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.
29
+
30
+ 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
+
32
+ ## Uso
33
+
34
+ Inclua o helper no controller:
35
+
36
+ ```ruby
37
+ class ApplicationController < ActionController::Base
38
+ include SolidusMeta::ControllerHelpers
39
+ end
40
+ ```
41
+
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:
43
+
44
+ ```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)
49
+ ```
50
+
51
+ 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.
52
+
53
+ O `event_time` enviado à Meta é o momento em que o job foi enfileirado.
54
+
55
+ ### Adicionando um evento
56
+
57
+ 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 `event_id`, `path` e, opcionalmente, `fallback` e `custom_data` (que sobrescreve os campos padrão).
@@ -0,0 +1,15 @@
1
+ module SolidusMeta
2
+ module ControllerHelpers
3
+ extend ActiveSupport::Concern
4
+
5
+ private
6
+
7
+ def cookies_meta
8
+ {fbp: cookies[:_fbp], fbc: cookies[:_fbc]}
9
+ end
10
+
11
+ def meta_tracking_data
12
+ {cookies_meta:, ahoy_visit_id: (current_visit&.id if respond_to?(:current_visit, true))}
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ module SolidusMeta
2
+ class BaseJob < ActiveJob::Base
3
+ retry_on Ac::TooManyRequestsError, wait: :polynomially_longer, attempts: 5
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ module SolidusMeta
2
+ class EventJob < BaseJob
3
+ def perform(event_name, cookies_meta:, ahoy_visit_id: nil, **options)
4
+ Event.new.track(event_name, cookies_meta:, ahoy_visit_id:, event_time: enqueued_at || Time.current, **options)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,29 @@
1
+ module SolidusMeta
2
+ class Client < Ac::Base
3
+ BASE_URL = "https://graph.facebook.com/v24.0/"
4
+ SAVE_RESPONSES = true
5
+
6
+ def initialize(pixel_id: SolidusMeta.config.pixel_id, access_token: SolidusMeta.config.access_token)
7
+ @pixel_id = pixel_id
8
+ @meta_access_token = access_token
9
+ end
10
+
11
+ def send_event(event_name, event_id:, event_source_url:, event_time: Time.current, user_data: {}, custom_data: {})
12
+ params = {access_token: @meta_access_token}
13
+ body = {
14
+ data: [
15
+ {
16
+ event_name:,
17
+ event_time: event_time.to_time.to_i,
18
+ action_source: "website",
19
+ event_source_url:,
20
+ event_id:,
21
+ user_data:,
22
+ custom_data:
23
+ }
24
+ ]
25
+ }
26
+ post("#{@pixel_id}/events", body:, params:)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,147 @@
1
+ module SolidusMeta
2
+ class Event
3
+ EVENTS = %w[AddToCart InitiateCheckout AddPaymentInfo Purchase].freeze
4
+
5
+ attr_reader :order
6
+
7
+ def initialize(client: Client.new)
8
+ @client = client
9
+ end
10
+
11
+ def track(event_name, cookies_meta:, ahoy_visit_id: nil, event_time: Time.current, **options)
12
+ raise Error, "evento não suportado: #{event_name}" unless EVENTS.include?(event_name)
13
+ event = send(event_name.underscore, **options)
14
+
15
+ @client.send_event(
16
+ event_name,
17
+ event_time:,
18
+ event_id: event[:event_id],
19
+ event_source_url: File.join(SolidusMeta.config.store_url(order.store), event[:path]),
20
+ user_data: user_data(cookies_meta, ahoy_visit_id, fallback: event[:fallback]),
21
+ custom_data: {
22
+ currency: order.currency,
23
+ value: order.total.to_f,
24
+ order_id: order.number,
25
+ content_type: "product",
26
+ contents: content_line_items,
27
+ **address_data,
28
+ **event.fetch(:custom_data, {})
29
+ }.compact
30
+ )
31
+ end
32
+
33
+ private
34
+
35
+ def add_to_cart(line_item_id:, quantity:)
36
+ line_item = Spree::LineItem.find(line_item_id)
37
+ @order = line_item.order
38
+
39
+ {
40
+ event_id: "#{line_item.id}#{line_item.updated_at.to_i}",
41
+ path: "carrinho",
42
+ fallback: "sem usuário",
43
+ custom_data: {
44
+ currency: line_item.currency,
45
+ value: line_item.total.to_f,
46
+ contents: [content_line_item(line_item, quantity)]
47
+ }
48
+ }
49
+ end
50
+
51
+ def initiate_checkout(order_id:)
52
+ @order = Spree::Order.find(order_id)
53
+
54
+ {
55
+ event_id: order.number,
56
+ path: "checkout/address/edit",
57
+ custom_data: {num_items: order.line_items.count}
58
+ }
59
+ end
60
+
61
+ def add_payment_info(payment_id:)
62
+ payment = Spree::Payment.find(payment_id)
63
+ @order = payment.order
64
+
65
+ {
66
+ event_id: order.number,
67
+ path: "checkout/payment",
68
+ custom_data: {
69
+ currency: payment.currency,
70
+ value: payment.amount.to_f,
71
+ content_id: payment.number
72
+ }
73
+ }
74
+ end
75
+
76
+ def purchase(order_id:)
77
+ @order = Spree::Order.find(order_id)
78
+
79
+ {event_id: order.number, path: "pedidos"}
80
+ end
81
+
82
+ def user_data(cookies_meta, ahoy_visit_id, fallback: nil)
83
+ cookies_meta = cookies_meta.to_h.with_indifferent_access
84
+ visit = Ahoy::Visit.find_by(id: ahoy_visit_id) if ahoy_visit_id && defined?(Ahoy::Visit)
85
+
86
+ {
87
+ em: sha256(email || fallback),
88
+ fn: sha256(name || fallback),
89
+ ph: sha256(phone),
90
+ ct: sha256(address&.city&.downcase),
91
+ st: sha256(address&.state&.name&.downcase),
92
+ zp: sha256(address&.zipcode),
93
+ country: sha256(country),
94
+ external_id: address&.id&.to_s,
95
+ client_user_agent: visit&.user_agent,
96
+ client_ip_address: visit&.ip,
97
+ fbp: cookies_meta[:fbp],
98
+ fbc: cookies_meta[:fbc]
99
+ }.compact
100
+ end
101
+
102
+ def address_data
103
+ {
104
+ postal_code: address&.zipcode,
105
+ region: state,
106
+ country:
107
+ }
108
+ end
109
+
110
+ def content_line_items
111
+ order.line_items.map { |line_item| content_line_item(line_item, line_item.quantity) }
112
+ end
113
+
114
+ def content_line_item(line_item, quantity)
115
+ {id: line_item.sku, quantity:, item_price: line_item.price.to_f}
116
+ end
117
+
118
+ def address
119
+ order.ship_address || order.user&.address
120
+ end
121
+
122
+ def state
123
+ address&.state&.name&.downcase
124
+ end
125
+
126
+ def country
127
+ address&.country&.name&.downcase
128
+ end
129
+
130
+ def name
131
+ (order.name || order.user&.name)&.downcase
132
+ end
133
+
134
+ def email
135
+ (order.email || order.user&.email)&.downcase
136
+ end
137
+
138
+ def phone
139
+ return unless address&.phone.present?
140
+ "55" + address.phone.remove(/\D/).remove(/^55/)
141
+ end
142
+
143
+ def sha256(value)
144
+ Digest::SHA256.hexdigest(value.strip) if value.present?
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusMeta
4
+ class Configuration
5
+ attr_writer :pixel_id, :access_token, :store_url
6
+
7
+ def pixel_id
8
+ @pixel_id || ENV["META_PIXEL_ID"]
9
+ end
10
+
11
+ def access_token
12
+ @access_token || ENV["META_ACCESS_TOKEN"]
13
+ end
14
+
15
+ def store_url(store = nil)
16
+ url = @store_url.presence || ENV["META_STORE_URL"].presence || store&.url.presence
17
+
18
+ raise Error, "store_url não configurado: defina SolidusMeta.config.store_url, META_STORE_URL ou a url da loja" unless url
19
+ url.start_with?("http") ? url : "https://#{url}"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "solidus_core"
4
+ require "solidus_support"
5
+
6
+ module SolidusMeta
7
+ class Engine < Rails::Engine
8
+ include SolidusSupport::EngineExtensions
9
+
10
+ isolate_namespace SolidusMeta
11
+
12
+ engine_name "solidus_meta"
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusMeta
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ac"
4
+ require "solidus_meta/version"
5
+ require "solidus_meta/configuration"
6
+ require "solidus_meta/engine"
7
+
8
+ module SolidusMeta
9
+ class Error < StandardError; end
10
+
11
+ def self.config
12
+ @config ||= Configuration.new
13
+ end
14
+
15
+ def self.configure
16
+ yield config
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: solidus_meta
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - marialuiza1324
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-10-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ac
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.1.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.1.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: solidus_core
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: solidus_support
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Envia eventos para a Conversions API da Meta a partir de pedidos do Solidus.
56
+ email:
57
+ - marialuiza.nerypinto@yahoo.com.br
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - README.md
63
+ - app/controllers/concerns/solidus_meta/controller_helpers.rb
64
+ - app/jobs/solidus_meta/base_job.rb
65
+ - app/jobs/solidus_meta/event_job.rb
66
+ - app/models/solidus_meta/client.rb
67
+ - app/models/solidus_meta/event.rb
68
+ - lib/solidus_meta.rb
69
+ - lib/solidus_meta/configuration.rb
70
+ - lib/solidus_meta/engine.rb
71
+ - lib/solidus_meta/version.rb
72
+ homepage: https://github.com/todasessascoisas/solidus_meta
73
+ licenses: []
74
+ metadata: {}
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '3.1'
84
+ - - "<"
85
+ - !ruby/object:Gem::Version
86
+ version: '4'
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubygems_version: 3.5.3
94
+ signing_key:
95
+ specification_version: 4
96
+ summary: Eventos da Conversions API da Meta para lojas Solidus
97
+ test_files: []