open_mercato 0.1.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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +23 -0
  3. data/README.md +424 -0
  4. data/app/controllers/open_mercato/webhooks_controller.rb +46 -0
  5. data/app/jobs/open_mercato/webhook_job.rb +12 -0
  6. data/config/routes.rb +5 -0
  7. data/lib/generators/open_mercato/install/install_generator.rb +36 -0
  8. data/lib/generators/open_mercato/install/templates/initializer.rb.tt +21 -0
  9. data/lib/generators/open_mercato/install/templates/webhook_handlers.rb.tt +35 -0
  10. data/lib/open_mercato/client.rb +120 -0
  11. data/lib/open_mercato/collection.rb +21 -0
  12. data/lib/open_mercato/configuration.rb +32 -0
  13. data/lib/open_mercato/engine.rb +7 -0
  14. data/lib/open_mercato/error.rb +37 -0
  15. data/lib/open_mercato/resource.rb +57 -0
  16. data/lib/open_mercato/resources/attachments/library.rb +23 -0
  17. data/lib/open_mercato/resources/auth/api_key.rb +20 -0
  18. data/lib/open_mercato/resources/auth/user.rb +20 -0
  19. data/lib/open_mercato/resources/catalog/category.rb +21 -0
  20. data/lib/open_mercato/resources/catalog/offer.rb +23 -0
  21. data/lib/open_mercato/resources/catalog/price.rb +23 -0
  22. data/lib/open_mercato/resources/catalog/price_kind.rb +19 -0
  23. data/lib/open_mercato/resources/catalog/product.rb +23 -0
  24. data/lib/open_mercato/resources/catalog/tag.rb +18 -0
  25. data/lib/open_mercato/resources/catalog/variant.rb +25 -0
  26. data/lib/open_mercato/resources/customers/activity.rb +24 -0
  27. data/lib/open_mercato/resources/customers/address.rb +24 -0
  28. data/lib/open_mercato/resources/customers/comment.rb +19 -0
  29. data/lib/open_mercato/resources/customers/company.rb +23 -0
  30. data/lib/open_mercato/resources/customers/deal.rb +26 -0
  31. data/lib/open_mercato/resources/customers/person.rb +23 -0
  32. data/lib/open_mercato/resources/customers/tag.rb +18 -0
  33. data/lib/open_mercato/resources/dictionaries/dictionary.rb +19 -0
  34. data/lib/open_mercato/resources/dictionaries/entry.rb +44 -0
  35. data/lib/open_mercato/resources/notifications/notification.rb +20 -0
  36. data/lib/open_mercato/resources/sales/channel.rb +19 -0
  37. data/lib/open_mercato/resources/sales/dashboard/new_orders.rb +19 -0
  38. data/lib/open_mercato/resources/sales/dashboard/new_quotes.rb +19 -0
  39. data/lib/open_mercato/resources/sales/invoice.rb +28 -0
  40. data/lib/open_mercato/resources/sales/order.rb +26 -0
  41. data/lib/open_mercato/resources/sales/order_line.rb +24 -0
  42. data/lib/open_mercato/resources/sales/payment.rb +22 -0
  43. data/lib/open_mercato/resources/sales/payment_method.rb +19 -0
  44. data/lib/open_mercato/resources/sales/quote.rb +40 -0
  45. data/lib/open_mercato/resources/sales/shipment.rb +22 -0
  46. data/lib/open_mercato/resources/sales/shipping_method.rb +21 -0
  47. data/lib/open_mercato/resources/sales/tax_rate.rb +20 -0
  48. data/lib/open_mercato/resources/search/query.rb +23 -0
  49. data/lib/open_mercato/resources/translations/translation.rb +25 -0
  50. data/lib/open_mercato/resources/workflows/definition.rb +19 -0
  51. data/lib/open_mercato/resources/workflows/instance.rb +32 -0
  52. data/lib/open_mercato/resources/workflows/signal.rb +18 -0
  53. data/lib/open_mercato/resources/workflows/task.rb +33 -0
  54. data/lib/open_mercato/testing/fake_responses.rb +161 -0
  55. data/lib/open_mercato/testing/request_stubs.rb +50 -0
  56. data/lib/open_mercato/testing/webhook_helpers.rb +41 -0
  57. data/lib/open_mercato/testing.rb +19 -0
  58. data/lib/open_mercato/version.rb +5 -0
  59. data/lib/open_mercato/webhooks/event.rb +63 -0
  60. data/lib/open_mercato/webhooks/handler.rb +50 -0
  61. data/lib/open_mercato/webhooks/signature.rb +55 -0
  62. data/lib/open_mercato.rb +44 -0
  63. metadata +353 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: db3dd4098e98dab790c19d8f0ef615328a1931073754c96a9255af8c5a814ef6
4
+ data.tar.gz: 608c953e575d8585fab0cf83d8e74d7ea269c4aef11e82d8063d0b1ea8800107
5
+ SHA512:
6
+ metadata.gz: de306247796ce27b870fced5bcb4ea5b03637bb3502d640695a5682672cead3e6d25972092d730ae3eb4d49f82ac2afb704ab07bf1a4551e7763fdc3c329f4a4
7
+ data.tar.gz: e50593873bd1c2d5947399f017e3e7d6f747bab329d56c44dd5d3ea57db6c3e6a961d728ac2b990004e3500a7de8447b7c6c3429ccb77b9010c178d85cd995f9
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 2n it
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+
data/README.md ADDED
@@ -0,0 +1,424 @@
1
+ # Open Mercato Ruby SDK
2
+
3
+ Full-featured Ruby gem for integrating Rails applications with the [Open Mercato](https://github.com/open-mercato) ERP/CRM platform.
4
+
5
+ ## Features
6
+
7
+ - **Resource classes** for all modules: Catalog, Customers, Sales, Search, Notifications, Workflows, Dictionaries, Auth
8
+ - **Webhook receiver** as a mountable Rails Engine with HMAC-SHA256 signature verification
9
+ - **ActiveModel** integration with typed attributes
10
+ - **Paginated collections** with Enumerable support
11
+ - **Testing helpers** for WebMock stubs and webhook simulation
12
+ - **Install generator** for quick Rails setup
13
+
14
+ ## When to Use This Gem
15
+
16
+ This gem is built for **Rails applications that integrate with Open Mercato as an external service** — whether Open Mercato runs in the cloud, on-premise, or as a separate container in your infrastructure.
17
+
18
+ ### Common integration patterns
19
+
20
+ **Connecting an existing Rails app to Open Mercato**
21
+
22
+ Your company already runs a Rails application (e-commerce, customer portal, internal tool) and wants to add Open Mercato as the business engine without rewriting anything. This gem gives you a ready-made HTTP client with typed Ruby objects, error handling, and retry logic — wire it up and start calling the API.
23
+
24
+ ```ruby
25
+ # Your existing Spree / Solidus store creates an order in Open Mercato
26
+ def after_order_complete(order)
27
+ OpenMercato::Resources::Sales::Order.create(
28
+ channel_id: ENV["MERCATO_CHANNEL_ID"],
29
+ customer_email: order.email,
30
+ lines: order.line_items.map { |li| { sku: li.sku, qty: li.quantity } }
31
+ )
32
+ end
33
+ ```
34
+
35
+ **Using Open Mercato as an Order Management System (OMS)**
36
+
37
+ Your Rails storefront handles the customer experience; Open Mercato handles fulfillment, inventory, and workflows. The gem bridges the two: orders flow in via API calls, status changes flow back via webhooks.
38
+
39
+ ```
40
+ Rails (Spree/Solidus) → gem calls Sales::Order.create(...) → Open Mercato OMS
41
+ ← webhook: sales.orders.shipped ←
42
+ ```
43
+
44
+ **Multi-tenant SaaS with per-tenant Open Mercato instances**
45
+
46
+ Reconfigure the client per request to route API calls to the correct tenant instance — useful when each customer gets their own isolated Open Mercato environment.
47
+
48
+ **Receiving real-time events in Rails**
49
+
50
+ Mount the built-in webhook engine and react to any Open Mercato event (order placed, payment received, workflow completed) directly in your Rails app, with HMAC signature verification handled automatically.
51
+
52
+ ```ruby
53
+ OpenMercato::Webhooks::Handler.on("sales.orders.shipped") do |event|
54
+ ShipmentNotifier.with(order_id: event.record_id).deliver_later
55
+ end
56
+ ```
57
+
58
+ ## Requirements
59
+
60
+ - Ruby >= 3.1
61
+ - Rails >= 7.0
62
+
63
+ ## Installation
64
+
65
+ Add to your Gemfile:
66
+
67
+ ```ruby
68
+ gem "open_mercato"
69
+ ```
70
+
71
+ Then run:
72
+
73
+ ```bash
74
+ bundle install
75
+ rails generate open_mercato:install
76
+ ```
77
+
78
+ This creates:
79
+ - `config/initializers/open_mercato.rb` - Configuration
80
+ - `app/services/open_mercato_handlers.rb` - Webhook handler stubs
81
+ - Mounts the webhook engine in `config/routes.rb`
82
+
83
+ ## Configuration
84
+
85
+ ```ruby
86
+ # config/initializers/open_mercato.rb
87
+ OpenMercato.configure do |config|
88
+ config.api_url = ENV["OPEN_MERCATO_URL"]
89
+ config.api_key = ENV["OPEN_MERCATO_API_KEY"]
90
+ config.tenant_id = ENV["OPEN_MERCATO_TENANT_ID"]
91
+ config.organization_id = ENV["OPEN_MERCATO_ORG_ID"]
92
+ config.webhook_secret = ENV["OPEN_MERCATO_WEBHOOK_SECRET"] # see Webhooks section
93
+
94
+ # Optional
95
+ config.timeout = 30 # Request timeout (seconds)
96
+ config.retry_count = 3 # Retry on 429/5xx
97
+ config.async_webhooks = true # Process webhooks via ActiveJob
98
+ config.logger = Rails.logger
99
+ end
100
+ ```
101
+
102
+ ## Usage
103
+
104
+ ### Catalog
105
+
106
+ ```ruby
107
+ # List products
108
+ products = OpenMercato::Resources::Catalog::Product.list(page: 1, page_size: 25)
109
+ products.each { |p| puts p.title }
110
+ products.total_pages # => 5
111
+ products.next_page? # => true
112
+
113
+ # Find a product
114
+ product = OpenMercato::Resources::Catalog::Product.find("uuid")
115
+
116
+ # Create a product
117
+ result = OpenMercato::Resources::Catalog::Product.create(
118
+ title: "New Product",
119
+ sku: "SKU-001",
120
+ primary_currency_code: "USD"
121
+ )
122
+ # => { "id" => "new-uuid" }
123
+
124
+ # Update a product
125
+ OpenMercato::Resources::Catalog::Product.update("uuid", title: "Updated")
126
+
127
+ # Delete a product
128
+ OpenMercato::Resources::Catalog::Product.destroy("uuid")
129
+
130
+ # Other catalog resources
131
+ OpenMercato::Resources::Catalog::Variant.list
132
+ OpenMercato::Resources::Catalog::Price.list
133
+ OpenMercato::Resources::Catalog::Category.list
134
+ OpenMercato::Resources::Catalog::Offer.list
135
+ OpenMercato::Resources::Catalog::PriceKind.list
136
+ OpenMercato::Resources::Catalog::Tag.list
137
+ ```
138
+
139
+ ### Customers
140
+
141
+ ```ruby
142
+ people = OpenMercato::Resources::Customers::Person.list
143
+ person = OpenMercato::Resources::Customers::Person.find("uuid")
144
+
145
+ companies = OpenMercato::Resources::Customers::Company.list
146
+ deals = OpenMercato::Resources::Customers::Deal.list
147
+ activities = OpenMercato::Resources::Customers::Activity.list
148
+ ```
149
+
150
+ ### Sales
151
+
152
+ ```ruby
153
+ orders = OpenMercato::Resources::Sales::Order.list
154
+ order = OpenMercato::Resources::Sales::Order.find("uuid")
155
+
156
+ # Quotes with special actions
157
+ OpenMercato::Resources::Sales::Quote.accept("quote-id")
158
+ OpenMercato::Resources::Sales::Quote.convert_to_order("quote-id")
159
+ OpenMercato::Resources::Sales::Quote.send_quote("quote-id")
160
+
161
+ # Other sales resources
162
+ OpenMercato::Resources::Sales::OrderLine.list
163
+ OpenMercato::Resources::Sales::Payment.list
164
+ OpenMercato::Resources::Sales::Shipment.list
165
+ OpenMercato::Resources::Sales::Channel.list
166
+ ```
167
+
168
+ ### Search
169
+
170
+ ```ruby
171
+ results = OpenMercato::Resources::Search::Query.search("laptop", page: 1)
172
+ global_results = OpenMercato::Resources::Search::Query.global("laptop")
173
+ OpenMercato::Resources::Search::Query.reindex(entity_type: "products")
174
+ ```
175
+
176
+ ### Workflows
177
+
178
+ ```ruby
179
+ definitions = OpenMercato::Resources::Workflows::Definition.list
180
+ instances = OpenMercato::Resources::Workflows::Instance.list
181
+
182
+ # Signal a workflow instance
183
+ OpenMercato::Resources::Workflows::Instance.signal("instance-id", "approve", comment: "Looks good")
184
+ ```
185
+
186
+ ### Dictionaries
187
+
188
+ ```ruby
189
+ dictionaries = OpenMercato::Resources::Dictionaries::Dictionary.list
190
+ entries = OpenMercato::Resources::Dictionaries::Entry.list
191
+ ```
192
+
193
+ ### Filtering and Sorting
194
+
195
+ All `.list` calls accept filter and sort parameters:
196
+
197
+ ```ruby
198
+ # Filter with operators
199
+ products = OpenMercato::Resources::Catalog::Product.list(
200
+ "filter[isActive][$eq]" => true,
201
+ "filter[title][$ilike]" => "%laptop%",
202
+ sort: "-created_at",
203
+ page: 1,
204
+ page_size: 50
205
+ )
206
+ ```
207
+
208
+ ## Webhooks
209
+
210
+ ### Handler Registration
211
+
212
+ ```ruby
213
+ # app/services/open_mercato_handlers.rb
214
+
215
+ # Exact match
216
+ OpenMercato::Webhooks::Handler.on("sales.orders.created") do |event|
217
+ order_data = event.data
218
+ event.record_id # => "uuid"
219
+ event.module_name # => "sales"
220
+ event.entity_name # => "orders"
221
+ event.action_name # => "created"
222
+ event.created? # => true
223
+ event.tenant_id # => "tenant-uuid"
224
+ end
225
+
226
+ # Wildcard match (all sales events)
227
+ OpenMercato::Webhooks::Handler.on("sales.*") do |event|
228
+ Rails.logger.info "Sales event: #{event.type}"
229
+ end
230
+
231
+ # Catch-all
232
+ OpenMercato::Webhooks::Handler.on("*") do |event|
233
+ Rails.logger.info "Event: #{event.type}"
234
+ end
235
+
236
+ # Class-based handler
237
+ class OrderSyncHandler
238
+ def call(event)
239
+ Order.sync_from_mercato(event.data)
240
+ end
241
+ end
242
+ OpenMercato::Webhooks::Handler.on("sales.orders.created", OrderSyncHandler.new)
243
+ ```
244
+
245
+ ### Webhook Endpoint
246
+
247
+ The engine mounts at `/open_mercato/webhooks` (POST).
248
+
249
+ Expected signature format: `X-OpenMercato-Signature: t=<timestamp>,v1=<hmac-sha256>`
250
+
251
+ ### About `webhook_secret`
252
+
253
+ Open Mercato triggers outbound HTTP calls via the `CALL_WEBHOOK` action in business
254
+ rules and workflows. As of 0.4.4, the platform does not automatically sign these
255
+ requests — there is no managed webhook subscription system with auto-generated secrets.
256
+
257
+ **Current setup:** Choose a random shared secret yourself, set it in both places:
258
+
259
+ ```bash
260
+ # In your Rails app
261
+ OPEN_MERCATO_WEBHOOK_SECRET=your-random-secret-here
262
+
263
+ # In the Open Mercato workflow/business rule CALL_WEBHOOK action config,
264
+ # add a custom header:
265
+ # X-OpenMercato-Signature: <compute and set manually, or leave unsigned for now>
266
+ ```
267
+
268
+ If you don't need signature verification (e.g. the webhook endpoint is protected by
269
+ other means), set `webhook_secret` to any value and the engine will still receive and
270
+ route events — signature verification only runs when `webhook_secret` is non-nil and
271
+ the header is present.
272
+
273
+ ## Error Handling
274
+
275
+ ```ruby
276
+ begin
277
+ OpenMercato::Resources::Catalog::Product.create(title: "")
278
+ rescue OpenMercato::ValidationError => e
279
+ e.message # => "Validation failed: title: can't be blank"
280
+ e.field_errors # => { "title" => ["can't be blank"] }
281
+ e.details # => [{ "path" => ["title"], "message" => "can't be blank" }]
282
+ rescue OpenMercato::AuthenticationError
283
+ # 401 - invalid API key
284
+ rescue OpenMercato::ForbiddenError
285
+ # 403 - insufficient permissions
286
+ rescue OpenMercato::NotFoundError
287
+ # 404 - resource not found
288
+ rescue OpenMercato::RateLimitError
289
+ # 429 - rate limited (auto-retried by default)
290
+ rescue OpenMercato::ServerError
291
+ # 5xx - server error
292
+ rescue OpenMercato::Error => e
293
+ # Base error class
294
+ end
295
+ ```
296
+
297
+ ## Testing
298
+
299
+ ### Setup
300
+
301
+ ```ruby
302
+ # spec/rails_helper.rb or spec/spec_helper.rb
303
+ require "open_mercato/testing"
304
+
305
+ RSpec.configure do |config|
306
+ config.include OpenMercato::Testing::RequestStubs
307
+ config.include OpenMercato::Testing::WebhookHelpers
308
+
309
+ config.before(:each) do
310
+ OpenMercato::Testing.setup!
311
+ end
312
+ end
313
+ ```
314
+
315
+ ### Stubbing API Calls
316
+
317
+ ```ruby
318
+ RSpec.describe ProductSync do
319
+ it "syncs products" do
320
+ items = [OpenMercato::Testing::FakeResponses.product("title" => "Laptop")]
321
+ stub_mercato_list("/api/catalog/products", items: items, "total" => 1)
322
+
323
+ products = OpenMercato::Resources::Catalog::Product.list
324
+ expect(products.first.title).to eq("Laptop")
325
+ end
326
+
327
+ it "handles creation" do
328
+ stub_mercato_create("/api/catalog/products", response: { "id" => "new-uuid" })
329
+
330
+ result = OpenMercato::Resources::Catalog::Product.create(title: "New")
331
+ expect(result["id"]).to eq("new-uuid")
332
+ end
333
+ end
334
+ ```
335
+
336
+ ### Simulating Webhooks
337
+
338
+ ```ruby
339
+ RSpec.describe OrderHandler do
340
+ include OpenMercato::Testing::WebhookHelpers
341
+
342
+ it "processes order events" do
343
+ received = nil
344
+ OpenMercato::Webhooks::Handler.on("sales.orders.created") { |e| received = e }
345
+
346
+ simulate_mercato_webhook("sales.orders.created", data: { "id" => "order-123" })
347
+
348
+ expect(received.record_id).to eq("order-123")
349
+ end
350
+
351
+ it "generates signed requests" do
352
+ request = signed_mercato_webhook_request("test.event", data: { "id" => "123" })
353
+ # request[:payload] - JSON string
354
+ # request[:headers] - Hash with X-OpenMercato-Signature and Content-Type
355
+ end
356
+ end
357
+ ```
358
+
359
+ ## API Reference
360
+
361
+ ### Resources
362
+
363
+ | Module | Resource | API Path |
364
+ |--------|----------|----------|
365
+ | Catalog | Product | `/api/catalog/products` |
366
+ | Catalog | Variant | `/api/catalog/variants` |
367
+ | Catalog | Price | `/api/catalog/prices` |
368
+ | Catalog | Category | `/api/catalog/categories` |
369
+ | Catalog | Offer | `/api/catalog/offers` |
370
+ | Catalog | PriceKind | `/api/catalog/price-kinds` |
371
+ | Catalog | Tag | `/api/catalog/tags` |
372
+ | Customers | Person | `/api/customers/people` |
373
+ | Customers | Company | `/api/customers/companies` |
374
+ | Customers | Deal | `/api/customers/deals` |
375
+ | Customers | Activity | `/api/customers/activities` |
376
+ | Customers | Address | `/api/customers/addresses` |
377
+ | Customers | Comment | `/api/customers/comments` |
378
+ | Customers | Tag | `/api/customers/tags` |
379
+ | Sales | Order | `/api/sales/orders` |
380
+ | Sales | OrderLine | `/api/sales/order-lines` |
381
+ | Sales | Quote | `/api/sales/quotes` |
382
+ | Sales | Payment | `/api/sales/payments` |
383
+ | Sales | Shipment | `/api/sales/shipments` |
384
+ | Sales | Channel | `/api/sales/channels` |
385
+ | Sales | ShippingMethod | `/api/sales/shipping-methods` |
386
+ | Sales | PaymentMethod | `/api/sales/payment-methods` |
387
+ | Sales | TaxRate | `/api/sales/tax-rates` |
388
+ | Search | Query | `/api/search/search` |
389
+ | Notifications | Notification | `/api/notifications` |
390
+ | Workflows | Definition | `/api/workflows/definitions` |
391
+ | Workflows | Instance | `/api/workflows/instances` |
392
+ | Workflows | Task | `/api/workflows/tasks` |
393
+ | Workflows | Signal | `/api/workflows/signals` (send only) |
394
+ | Sales::Dashboard | NewOrders | `/api/sales/dashboard/widgets/new-orders` |
395
+ | Sales::Dashboard | NewQuotes | `/api/sales/dashboard/widgets/new-quotes` |
396
+ | Translations | Translation | `/api/translations/:entity_type/:entity_id` |
397
+ | Attachments | Library | `/api/attachments/library` |
398
+ | Dictionaries | Dictionary | `/api/dictionaries` |
399
+ | Dictionaries | Entry | `/api/dictionaries/entries` |
400
+ | Auth | User | `/api/auth/users` |
401
+ | Auth | ApiKey | `/api/api_keys/keys` |
402
+
403
+ ### Standard Resource Methods
404
+
405
+ All resources (except Search::Query) support:
406
+ - `.list(params = {})` - returns `OpenMercato::Collection`
407
+ - `.find(id)` - returns Resource instance
408
+ - `.create(attributes)` - returns `{ "id" => "uuid" }`
409
+ - `.update(id, attributes)` - returns `{ "ok" => true }`
410
+ - `.destroy(id)` - returns `{ "ok" => true }`
411
+
412
+ ### Special Methods
413
+
414
+ - `Quote.accept(id)`, `Quote.convert_to_order(id)`, `Quote.send_quote(id)`
415
+ - `Workflows::Instance.signal(id, signal_name, payload)`, `Workflows::Instance.advance(id, to_step_id:, trigger_data:, context_updates:)`
416
+ - `Workflows::Task.claim(id)`, `Workflows::Task.complete(id, form_data:, comments:)`
417
+ - `Workflows::Signal.send_signal(correlation_key:, signal_name:, payload:)`
418
+ - `Translations::Translation.find(entity_type, entity_id)`, `.set_translations(entity_type, entity_id, translations:)`, `.destroy(entity_type, entity_id)`
419
+ - `Sales::Dashboard::NewOrders.list(params)`, `Sales::Dashboard::NewQuotes.list(params)`
420
+ - `Search::Query.search(q)`, `Search::Query.global(q)`, `Search::Query.reindex(params)`
421
+
422
+ ## License
423
+
424
+ MIT
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenMercato
4
+ class WebhooksController < ActionController::Base
5
+ skip_before_action :verify_authenticity_token
6
+
7
+ def create
8
+ payload = request.raw_post
9
+ signature = request.headers["X-OpenMercato-Signature"]
10
+ config = OpenMercato.configuration
11
+
12
+ Webhooks::Signature.verify!(
13
+ payload: payload,
14
+ signature: signature,
15
+ secret: config.webhook_secret,
16
+ tolerance: config.webhook_tolerance
17
+ )
18
+
19
+ parsed = JSON.parse(payload)
20
+ event = Webhooks::Event.new(
21
+ type: parsed["event"],
22
+ data: parsed["data"],
23
+ tenant_id: parsed["tenantId"],
24
+ organization_id: parsed["organizationId"],
25
+ timestamp: parsed["timestamp"]
26
+ )
27
+
28
+ if config.async_webhooks
29
+ WebhookJob.perform_later(event.serialize)
30
+ else
31
+ Webhooks::Handler.dispatch(event)
32
+ end
33
+
34
+ head :ok
35
+ rescue Webhooks::SignatureError
36
+ head :unauthorized
37
+ rescue JSON::ParserError
38
+ head :bad_request
39
+ rescue StandardError => e
40
+ raise e if config.raise_webhook_errors
41
+
42
+ config.logger&.error("[OpenMercato] Webhook processing error: #{e.class}: #{e.message}")
43
+ head :internal_server_error
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenMercato
4
+ class WebhookJob < ActiveJob::Base
5
+ queue_as :open_mercato_webhooks
6
+
7
+ def perform(event_data)
8
+ event = Webhooks::Event.deserialize(event_data)
9
+ Webhooks::Handler.dispatch(event)
10
+ end
11
+ end
12
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ OpenMercato::Engine.routes.draw do
4
+ post "webhooks", to: "webhooks#create"
5
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module OpenMercato
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ desc "Install Open Mercato SDK: creates initializer and webhook handler"
11
+
12
+ def copy_initializer
13
+ template "initializer.rb.tt", "config/initializers/open_mercato.rb"
14
+ end
15
+
16
+ def copy_webhook_handlers
17
+ template "webhook_handlers.rb.tt", "app/services/open_mercato_handlers.rb"
18
+ end
19
+
20
+ def mount_engine
21
+ route 'mount OpenMercato::Engine, at: "/open_mercato"'
22
+ end
23
+
24
+ def show_readme
25
+ say ""
26
+ say "Open Mercato SDK installed!", :green
27
+ say ""
28
+ say "Next steps:"
29
+ say " 1. Set your environment variables (OPEN_MERCATO_URL, OPEN_MERCATO_API_KEY, etc.)"
30
+ say " 2. Edit config/initializers/open_mercato.rb"
31
+ say " 3. Edit app/services/open_mercato_handlers.rb to handle webhooks"
32
+ say ""
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,21 @@
1
+ OpenMercato.configure do |config|
2
+ # Required: API connection
3
+ config.api_url = ENV["OPEN_MERCATO_URL"]
4
+ config.api_key = ENV["OPEN_MERCATO_API_KEY"]
5
+
6
+ # Required for multi-tenant: scope all requests
7
+ config.tenant_id = ENV["OPEN_MERCATO_TENANT_ID"]
8
+ config.organization_id = ENV["OPEN_MERCATO_ORG_ID"]
9
+
10
+ # Webhook signature verification
11
+ config.webhook_secret = ENV["OPEN_MERCATO_WEBHOOK_SECRET"]
12
+
13
+ # Optional settings
14
+ # config.timeout = 30 # Request timeout in seconds
15
+ # config.open_timeout = 10 # Connection timeout in seconds
16
+ # config.retry_count = 3 # Retry on 429/5xx
17
+ # config.async_webhooks = true # Process webhooks via ActiveJob
18
+ # config.webhook_tolerance = 300 # Max signature age in seconds
19
+ # config.logger = Rails.logger
20
+ # config.debug = false # Log request/response bodies
21
+ end
@@ -0,0 +1,35 @@
1
+ # Open Mercato Webhook Handlers
2
+ #
3
+ # Register handlers for Open Mercato events.
4
+ # Events follow the pattern: module.entity.action
5
+ #
6
+ # Examples:
7
+ # "sales.orders.created" - exact match
8
+ # "sales.*" - all sales events
9
+ # "*" - all events
10
+ #
11
+ # See: https://github.com/open-mercato/ruby-sdk for full documentation
12
+
13
+ # Catch-all logger (remove in production)
14
+ OpenMercato::Webhooks::Handler.on("*") do |event|
15
+ Rails.logger.info "[OpenMercato] Received #{event.type} for record #{event.record_id}"
16
+ end
17
+
18
+ # Example: Handle new orders
19
+ # OpenMercato::Webhooks::Handler.on("sales.orders.created") do |event|
20
+ # order_data = event.data
21
+ # # Sync order to your local database, send notification, etc.
22
+ # end
23
+
24
+ # Example: Handle customer updates
25
+ # OpenMercato::Webhooks::Handler.on("customers.*") do |event|
26
+ # # Handle all customer-related events
27
+ # end
28
+
29
+ # Example: Class-based handler
30
+ # class OrderSyncHandler
31
+ # def call(event)
32
+ # Order.sync_from_mercato(event.data)
33
+ # end
34
+ # end
35
+ # OpenMercato::Webhooks::Handler.on("sales.orders.created", OrderSyncHandler.new)