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.
- checksums.yaml +7 -0
- data/LICENSE +23 -0
- data/README.md +424 -0
- data/app/controllers/open_mercato/webhooks_controller.rb +46 -0
- data/app/jobs/open_mercato/webhook_job.rb +12 -0
- data/config/routes.rb +5 -0
- data/lib/generators/open_mercato/install/install_generator.rb +36 -0
- data/lib/generators/open_mercato/install/templates/initializer.rb.tt +21 -0
- data/lib/generators/open_mercato/install/templates/webhook_handlers.rb.tt +35 -0
- data/lib/open_mercato/client.rb +120 -0
- data/lib/open_mercato/collection.rb +21 -0
- data/lib/open_mercato/configuration.rb +32 -0
- data/lib/open_mercato/engine.rb +7 -0
- data/lib/open_mercato/error.rb +37 -0
- data/lib/open_mercato/resource.rb +57 -0
- data/lib/open_mercato/resources/attachments/library.rb +23 -0
- data/lib/open_mercato/resources/auth/api_key.rb +20 -0
- data/lib/open_mercato/resources/auth/user.rb +20 -0
- data/lib/open_mercato/resources/catalog/category.rb +21 -0
- data/lib/open_mercato/resources/catalog/offer.rb +23 -0
- data/lib/open_mercato/resources/catalog/price.rb +23 -0
- data/lib/open_mercato/resources/catalog/price_kind.rb +19 -0
- data/lib/open_mercato/resources/catalog/product.rb +23 -0
- data/lib/open_mercato/resources/catalog/tag.rb +18 -0
- data/lib/open_mercato/resources/catalog/variant.rb +25 -0
- data/lib/open_mercato/resources/customers/activity.rb +24 -0
- data/lib/open_mercato/resources/customers/address.rb +24 -0
- data/lib/open_mercato/resources/customers/comment.rb +19 -0
- data/lib/open_mercato/resources/customers/company.rb +23 -0
- data/lib/open_mercato/resources/customers/deal.rb +26 -0
- data/lib/open_mercato/resources/customers/person.rb +23 -0
- data/lib/open_mercato/resources/customers/tag.rb +18 -0
- data/lib/open_mercato/resources/dictionaries/dictionary.rb +19 -0
- data/lib/open_mercato/resources/dictionaries/entry.rb +44 -0
- data/lib/open_mercato/resources/notifications/notification.rb +20 -0
- data/lib/open_mercato/resources/sales/channel.rb +19 -0
- data/lib/open_mercato/resources/sales/dashboard/new_orders.rb +19 -0
- data/lib/open_mercato/resources/sales/dashboard/new_quotes.rb +19 -0
- data/lib/open_mercato/resources/sales/invoice.rb +28 -0
- data/lib/open_mercato/resources/sales/order.rb +26 -0
- data/lib/open_mercato/resources/sales/order_line.rb +24 -0
- data/lib/open_mercato/resources/sales/payment.rb +22 -0
- data/lib/open_mercato/resources/sales/payment_method.rb +19 -0
- data/lib/open_mercato/resources/sales/quote.rb +40 -0
- data/lib/open_mercato/resources/sales/shipment.rb +22 -0
- data/lib/open_mercato/resources/sales/shipping_method.rb +21 -0
- data/lib/open_mercato/resources/sales/tax_rate.rb +20 -0
- data/lib/open_mercato/resources/search/query.rb +23 -0
- data/lib/open_mercato/resources/translations/translation.rb +25 -0
- data/lib/open_mercato/resources/workflows/definition.rb +19 -0
- data/lib/open_mercato/resources/workflows/instance.rb +32 -0
- data/lib/open_mercato/resources/workflows/signal.rb +18 -0
- data/lib/open_mercato/resources/workflows/task.rb +33 -0
- data/lib/open_mercato/testing/fake_responses.rb +161 -0
- data/lib/open_mercato/testing/request_stubs.rb +50 -0
- data/lib/open_mercato/testing/webhook_helpers.rb +41 -0
- data/lib/open_mercato/testing.rb +19 -0
- data/lib/open_mercato/version.rb +5 -0
- data/lib/open_mercato/webhooks/event.rb +63 -0
- data/lib/open_mercato/webhooks/handler.rb +50 -0
- data/lib/open_mercato/webhooks/signature.rb +55 -0
- data/lib/open_mercato.rb +44 -0
- 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,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)
|