nats_wave 1.1.5 β 1.1.8
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 +4 -4
- data/.idea/nats_wave.iml +5 -5
- data/Gemfile.lock +1 -1
- data/README.md +1153 -401
- data/lib/generators/nats_wave/templates/initializer.rb +76 -19
- data/lib/nats_wave/adapters/datadog_metrics.rb +1 -1
- data/lib/nats_wave/client.rb +646 -26
- data/lib/nats_wave/concerns/mappable.rb +9 -3
- data/lib/nats_wave/configuration.rb +2 -2
- data/lib/nats_wave/dead_letter_queue.rb +4 -4
- data/lib/nats_wave/model_mapper.rb +1 -1
- data/lib/nats_wave/model_registry.rb +39 -15
- data/lib/nats_wave/publisher.rb +50 -47
- data/lib/nats_wave/subscriber.rb +279 -33
- data/lib/nats_wave/version.rb +1 -1
- metadata +2 -2
data/README.md
CHANGED
@@ -18,9 +18,12 @@ different teams, products, and services in asset management and valuation workfl
|
|
18
18
|
- [Publishing Events](#-publishing-events)
|
19
19
|
- [Subscribing to Events](#-subscribing-to-events)
|
20
20
|
- [Data Mapping & Transformation](#-data-mapping--transformation)
|
21
|
+
- [Cross-Service Integration](#-cross-service-integration)
|
22
|
+
- [Message Processing & Handlers](#-message-processing--handlers)
|
21
23
|
- [Monitoring & Metrics](#-monitoring--metrics)
|
22
24
|
- [Error Handling](#-error-handling)
|
23
25
|
- [Production Deployment](#-production-deployment)
|
26
|
+
- [Testing](#-testing)
|
24
27
|
- [API Reference](#-api-reference)
|
25
28
|
- [Examples](#-examples)
|
26
29
|
- [Contributing](#-contributing)
|
@@ -30,13 +33,16 @@ different teams, products, and services in asset management and valuation workfl
|
|
30
33
|
|
31
34
|
- **π Model-Based Configuration** - Configure subscriptions and mappings directly in your models
|
32
35
|
- **π Automatic Event Publishing** - ActiveRecord integration publishes model changes automatically
|
33
|
-
- **πΊοΈ Intelligent Data Mapping** - Transform data between different schemas and field names
|
36
|
+
- **πΊοΈ Intelligent Data Mapping** - Transform data between different schemas and field names with automatic field
|
37
|
+
mapping
|
38
|
+
- **π Cross-Service Synchronization** - Sync data between services with different model names and schemas
|
39
|
+
- **π― Smart Message Routing** - Route messages based on service origin, model type, and custom conditions
|
34
40
|
- **π Datadog Integration** - Built-in metrics and monitoring support
|
35
|
-
- **π‘οΈ Error Handling** - Dead letter queues, retries, and graceful failure handling
|
41
|
+
- **π‘οΈ Robust Error Handling** - Dead letter queues, retries, and graceful failure handling
|
36
42
|
- **π§ Middleware System** - Authentication, validation, and logging middleware
|
37
43
|
- **β‘ High Performance** - Connection pooling, async publishing, and batch operations
|
38
44
|
- **π₯ Health Monitoring** - Built-in health checks and status endpoints
|
39
|
-
- **π Auto-Discovery** - Automatic registration of model subscriptions
|
45
|
+
- **π Auto-Discovery** - Automatic registration of model subscriptions via ModelRegistry
|
40
46
|
- **π§ͺ Test-Friendly** - Comprehensive test helpers and mocking support
|
41
47
|
|
42
48
|
## π¦ Installation
|
@@ -73,6 +79,14 @@ NatsWave.configure do |config|
|
|
73
79
|
config.nats_url = ENV.fetch('NATS_URL', 'nats://localhost:4222')
|
74
80
|
config.service_name = 'valuation_service'
|
75
81
|
config.default_subject_prefix = "#{config.service_name}.events"
|
82
|
+
|
83
|
+
# Publishing
|
84
|
+
config.publishing_enabled = !Rails.env.test?
|
85
|
+
config.async_publishing = Rails.env.production?
|
86
|
+
|
87
|
+
# Subscription
|
88
|
+
config.subscription_enabled = !Rails.env.test?
|
89
|
+
config.queue_group = "#{config.service_name}_consumers"
|
76
90
|
end
|
77
91
|
```
|
78
92
|
|
@@ -88,14 +102,14 @@ class Asset < ApplicationRecord
|
|
88
102
|
nats_publishable(
|
89
103
|
actions: [:create, :update, :destroy],
|
90
104
|
skip_attributes: [:notes, :edge_data],
|
91
|
-
include_associations: [:package, :estimates]
|
105
|
+
include_associations: [:package, :estimates],
|
106
|
+
subject_prefix: 'assets'
|
92
107
|
)
|
93
108
|
|
94
|
-
#
|
95
|
-
nats_wave_maps_from '
|
96
|
-
subjects: ['ims.assets.*', 'inventory.assets.*'],
|
109
|
+
# Map from external inventory system
|
110
|
+
nats_wave_maps_from 'InventoryItem',
|
97
111
|
field_mappings: {
|
98
|
-
'
|
112
|
+
'item_id' => 'unique_id',
|
99
113
|
'description' => 'description',
|
100
114
|
'manufacturer' => 'make',
|
101
115
|
'model_name' => 'model',
|
@@ -105,18 +119,33 @@ class Asset < ApplicationRecord
|
|
105
119
|
},
|
106
120
|
transformations: {
|
107
121
|
'make' => ->(make) { make&.upcase&.strip },
|
108
|
-
'year' => ->(year) { year.to_i if year.present? }
|
122
|
+
'year' => ->(year) { year.to_i if year.present? },
|
123
|
+
'estimated_hammer' => ->(value) { value.to_f.round(2) }
|
109
124
|
},
|
110
|
-
unique_fields: [:unique_id, :vin, :serial]
|
125
|
+
unique_fields: [:unique_id, :vin, :serial],
|
126
|
+
sync_strategy: :upsert,
|
127
|
+
subjects: ['inventory_service.items.*']
|
128
|
+
|
129
|
+
# Subscribe to external asset events with custom handling
|
130
|
+
nats_wave_subscribes_to "auction_service.assets.*" do |message|
|
131
|
+
AuctionAssetProcessor.process(message)
|
132
|
+
end
|
111
133
|
end
|
112
134
|
```
|
113
135
|
|
114
136
|
### 3. Start the Subscriber
|
115
137
|
|
138
|
+
The subscriber automatically starts with Rails and processes registered model subscriptions:
|
139
|
+
|
116
140
|
```bash
|
117
141
|
# Development - auto-starts with Rails
|
118
142
|
rails server
|
119
143
|
|
144
|
+
# You'll see logs like:
|
145
|
+
# I, [2025-07-21T12:10:35.910694 #29] INFO -- : Starting NATS subscriber for valuation_service
|
146
|
+
# I, [2025-07-21T12:10:35.911316 #29] INFO -- : β
Successfully subscribed to inventory_service.items.* (total: 1)
|
147
|
+
# I, [2025-07-21T12:10:35.912749 #29] INFO -- : Started 3 subscriptions from Model Registry
|
148
|
+
|
120
149
|
# Production - run dedicated subscriber process
|
121
150
|
rails nats_wave:start
|
122
151
|
```
|
@@ -127,11 +156,17 @@ rails nats_wave:start
|
|
127
156
|
# Test connectivity
|
128
157
|
rails nats_wave:health
|
129
158
|
|
130
|
-
# Show model subscriptions
|
159
|
+
# Show model subscriptions
|
131
160
|
rails nats_wave:model_subscriptions
|
132
161
|
|
133
162
|
# Publish a test message
|
134
|
-
rails
|
163
|
+
rails console
|
164
|
+
> NatsWave.publish(
|
165
|
+
subject: 'assets.create',
|
166
|
+
model: 'Asset',
|
167
|
+
action: 'create',
|
168
|
+
data: { id: 123, make: 'Caterpillar', model: '320D' }
|
169
|
+
)
|
135
170
|
```
|
136
171
|
|
137
172
|
## βοΈ Configuration
|
@@ -141,60 +176,309 @@ rails nats_wave:test
|
|
141
176
|
```bash
|
142
177
|
# Required
|
143
178
|
NATS_URL=nats://your-nats-server:4222
|
179
|
+
|
180
|
+
# Optional - defaults to Rails application name
|
144
181
|
NATS_SERVICE_NAME=valuation_service
|
145
182
|
|
146
|
-
# Optional
|
183
|
+
# Optional Authentication & Security
|
147
184
|
NATS_AUTH_SECRET=your-secret-key
|
185
|
+
SCHEMA_REGISTRY_URL=http://schema-registry:8081
|
186
|
+
|
187
|
+
# Optional Monitoring
|
148
188
|
DD_AGENT_HOST=localhost # For Datadog metrics
|
149
189
|
DD_AGENT_PORT=8125
|
150
190
|
```
|
151
191
|
|
152
|
-
###
|
192
|
+
### Complete Configuration Setup
|
193
|
+
|
194
|
+
Create or update `config/initializers/nats_wave.rb`:
|
153
195
|
|
154
196
|
```ruby
|
155
|
-
#
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
config.service_name = ENV.fetch('NATS_SERVICE_NAME')
|
160
|
-
config.connection_pool_size = 10
|
161
|
-
config.reconnect_attempts = 3
|
162
|
-
|
163
|
-
# Publishing
|
164
|
-
config.publishing_enabled = !Rails.env.test?
|
165
|
-
config.async_publishing = Rails.env.production?
|
166
|
-
config.default_subject_prefix = "#{config.service_name}.events"
|
197
|
+
# frozen_string_literal: true
|
198
|
+
|
199
|
+
begin
|
200
|
+
Rails.logger.info "Initializing NATS Wave with URL: #{ENV['NATS_URL']}"
|
167
201
|
|
168
|
-
|
169
|
-
|
170
|
-
|
202
|
+
NatsWave.configure do |config|
|
203
|
+
# === CORE CONNECTION SETTINGS ===
|
204
|
+
config.nats_url = ENV.fetch('NATS_URL', 'nats://localhost:4222')
|
205
|
+
config.service_name = ENV.fetch('NATS_SERVICE_NAME',
|
206
|
+
Rails.application.class.name.deconstantize.underscore)
|
207
|
+
config.connection_pool_size = 10
|
208
|
+
config.reconnect_attempts = 3
|
209
|
+
|
210
|
+
Rails.logger.info "NATS URL: #{config.nats_url}"
|
211
|
+
Rails.logger.info "Service Name: #{config.service_name}"
|
212
|
+
|
213
|
+
# === PUBLISHING CONFIGURATION ===
|
214
|
+
config.publishing_enabled = !Rails.env.test? # Disable in test environment
|
215
|
+
config.async_publishing = Rails.env.production? # Async only in production
|
216
|
+
config.default_subject_prefix = config.service_name # Use service name as prefix
|
217
|
+
|
218
|
+
# === SUBSCRIPTION CONFIGURATION ===
|
219
|
+
config.subscription_enabled = !Rails.env.test? # Disable in test environment
|
220
|
+
config.queue_group = "#{config.service_name}_consumers" # Queue group for load balancing
|
221
|
+
|
222
|
+
# === ERROR HANDLING ===
|
223
|
+
config.max_retries = 3 # Max retry attempts
|
224
|
+
config.retry_delay = 5 # Base retry delay in seconds
|
225
|
+
config.dead_letter_queue = "failed_messages" # Failed message storage
|
226
|
+
|
227
|
+
# === AUTHENTICATION (Optional) ===
|
228
|
+
if ENV['NATS_AUTH_SECRET'].present?
|
229
|
+
config.middleware_authentication_enabled = true
|
230
|
+
config.auth_secret_key = ENV['NATS_AUTH_SECRET']
|
231
|
+
end
|
232
|
+
|
233
|
+
# === VALIDATION (Optional) ===
|
234
|
+
if ENV['SCHEMA_REGISTRY_URL'].present?
|
235
|
+
config.middleware_validation_enabled = true
|
236
|
+
config.schema_registry_url = ENV['SCHEMA_REGISTRY_URL']
|
237
|
+
end
|
238
|
+
|
239
|
+
# === LOGGING ===
|
240
|
+
config.middleware_logging_enabled = true
|
241
|
+
config.log_level = Rails.env.production? ? 'info' : 'debug'
|
242
|
+
end
|
171
243
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
config.log_level = Rails.env.production? ? 'info' : 'debug'
|
244
|
+
Rails.logger.info "NATS Wave configuration completed successfully"
|
245
|
+
rescue => e
|
246
|
+
Rails.logger.error "Failed to configure NATS Wave: #{e.message}"
|
247
|
+
Rails.logger.error e.backtrace.join("\n")
|
177
248
|
|
178
|
-
#
|
179
|
-
|
180
|
-
config.retry_delay = 5
|
249
|
+
# Optional: Re-raise in development to catch configuration issues early
|
250
|
+
raise e if Rails.env.development?
|
181
251
|
end
|
182
252
|
|
183
|
-
#
|
184
|
-
|
253
|
+
# === AUTOMATIC SUBSCRIBER STARTUP ===
|
254
|
+
Rails.application.config.after_initialize do
|
255
|
+
Thread.new do
|
256
|
+
sleep 2 # Give Rails time to boot and load all models
|
257
|
+
begin
|
258
|
+
# Start subscriber - this will automatically process all registered model subscriptions
|
259
|
+
NatsWave.start_subscriber
|
260
|
+
Rails.logger.info "β
NatsWave subscriber started successfully"
|
261
|
+
|
262
|
+
# Optional: Log subscription statistics
|
263
|
+
stats = NatsWave::ModelRegistry.subscription_stats
|
264
|
+
Rails.logger.info "π Active subscriptions: #{stats[:total_subscriptions]} across #{stats[:models_with_subscriptions]} models"
|
265
|
+
|
266
|
+
rescue StandardError => e
|
267
|
+
Rails.logger.error "Failed to start NatsWave subscriber: #{e.message}"
|
268
|
+
Rails.logger.error e.backtrace.join("\n")
|
269
|
+
|
270
|
+
# Optional: Alert monitoring systems
|
271
|
+
if defined?(Sentry)
|
272
|
+
Sentry.capture_exception(e, tags: { component: 'nats_wave_startup' })
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
# === DATADOG METRICS INTEGRATION (Optional) ===
|
279
|
+
if defined?(Datadog::Statsd) && !Rails.env.test?
|
280
|
+
# Get application version safely
|
281
|
+
app_version = begin
|
282
|
+
Rails.application.config.version
|
283
|
+
rescue
|
284
|
+
'unknown'
|
285
|
+
end
|
286
|
+
|
185
287
|
NatsWave::Metrics.configure_datadog(
|
186
|
-
namespace:
|
288
|
+
namespace: "#{Rails.application.class.name.deconstantize.underscore}.nats_wave",
|
187
289
|
tags: {
|
188
290
|
service: NatsWave.configuration.service_name,
|
189
|
-
environment: Rails.env
|
291
|
+
environment: Rails.env,
|
292
|
+
version: app_version
|
190
293
|
}
|
191
294
|
)
|
295
|
+
Rails.logger.info "π Datadog metrics integration enabled"
|
296
|
+
end
|
297
|
+
|
298
|
+
# === GRACEFUL SHUTDOWN (Optional) ===
|
299
|
+
at_exit do
|
300
|
+
begin
|
301
|
+
if defined?(NatsWave) && NatsWave.client
|
302
|
+
Rails.logger.info "π Shutting down NatsWave connections..."
|
303
|
+
NatsWave.client.disconnect!
|
304
|
+
Rails.logger.info "β
NatsWave shutdown complete"
|
305
|
+
end
|
306
|
+
rescue => e
|
307
|
+
Rails.logger.error "Error during NatsWave shutdown: #{e.message}"
|
308
|
+
end
|
192
309
|
end
|
193
310
|
```
|
194
311
|
|
312
|
+
### Environment-Specific Configuration
|
313
|
+
|
314
|
+
#### Development Environment
|
315
|
+
|
316
|
+
```ruby
|
317
|
+
# config/environments/development.rb
|
318
|
+
config.after_initialize do
|
319
|
+
if defined?(NatsWave)
|
320
|
+
# Enable debug logging in development
|
321
|
+
NatsWave.logger.level = Logger::DEBUG
|
322
|
+
|
323
|
+
# Optional: Show all registered subscriptions on startup
|
324
|
+
if NatsWave::ModelRegistry.subscriptions.any?
|
325
|
+
Rails.logger.info "π Registered NATS subscriptions:"
|
326
|
+
NatsWave::ModelRegistry.subscriptions.each do |subscription|
|
327
|
+
Rails.logger.info " π‘ #{subscription[:model]} -> #{subscription[:subjects].join(', ')}"
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
```
|
333
|
+
|
334
|
+
#### Production Environment
|
335
|
+
|
336
|
+
```ruby
|
337
|
+
# config/environments/production.rb
|
338
|
+
config.after_initialize do
|
339
|
+
if defined?(NatsWave)
|
340
|
+
# Enable authentication in production
|
341
|
+
NatsWave.configuration.middleware_authentication_enabled = true
|
342
|
+
|
343
|
+
# Set production logging level
|
344
|
+
NatsWave.logger.level = Logger::INFO
|
345
|
+
|
346
|
+
# Enable health check endpoint
|
347
|
+
Rails.application.routes.draw do
|
348
|
+
get '/health/nats_wave', to: 'health#nats_wave'
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
```
|
353
|
+
|
354
|
+
#### Test Environment
|
355
|
+
|
356
|
+
```ruby
|
357
|
+
# config/environments/test.rb or spec/spec_helper.rb
|
358
|
+
RSpec.configure do |config|
|
359
|
+
config.before(:suite) do
|
360
|
+
# Clear ModelRegistry before tests
|
361
|
+
NatsWave::ModelRegistry.clear! if defined?(NatsWave::ModelRegistry)
|
362
|
+
end
|
363
|
+
|
364
|
+
config.before(:each) do
|
365
|
+
# Mock NATS operations in tests
|
366
|
+
if defined?(NatsWave)
|
367
|
+
allow(NatsWave.client).to receive(:publish).and_return(true)
|
368
|
+
allow(NatsWave.client).to receive(:subscribe).and_return(true)
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
```
|
373
|
+
|
374
|
+
### Configuration Options Reference
|
375
|
+
|
376
|
+
| Option | Default | Description |
|
377
|
+
|-------------------------------------|-------------------------|---------------------------------|
|
378
|
+
| `nats_url` | `nats://localhost:4222` | NATS server connection URL |
|
379
|
+
| `service_name` | Rails app name | Unique service identifier |
|
380
|
+
| `connection_pool_size` | `10` | Max concurrent NATS connections |
|
381
|
+
| `reconnect_attempts` | `3` | Auto-reconnect attempts |
|
382
|
+
| `publishing_enabled` | `!Rails.env.test?` | Enable/disable publishing |
|
383
|
+
| `async_publishing` | `Rails.env.production?` | Publish asynchronously |
|
384
|
+
| `subscription_enabled` | `!Rails.env.test?` | Enable/disable subscriptions |
|
385
|
+
| `queue_group` | `{service}_consumers` | NATS queue group name |
|
386
|
+
| `max_retries` | `3` | Failed message retry attempts |
|
387
|
+
| `retry_delay` | `5` | Base retry delay (seconds) |
|
388
|
+
| `middleware_authentication_enabled` | `false` | Enable auth middleware |
|
389
|
+
| `middleware_validation_enabled` | `false` | Enable validation middleware |
|
390
|
+
| `middleware_logging_enabled` | `true` | Enable logging middleware |
|
391
|
+
|
392
|
+
### Health Check Endpoint
|
393
|
+
|
394
|
+
Add a health check endpoint to monitor NatsWave status:
|
395
|
+
|
396
|
+
```ruby
|
397
|
+
# config/routes.rb
|
398
|
+
Rails.application.routes.draw do
|
399
|
+
# ... your other routes
|
400
|
+
get '/health/nats_wave', to: 'health#nats_wave'
|
401
|
+
end
|
402
|
+
|
403
|
+
# app/controllers/health_controller.rb
|
404
|
+
class HealthController < ApplicationController
|
405
|
+
def nats_wave
|
406
|
+
health_data = NatsWave.health_check
|
407
|
+
|
408
|
+
if health_data[:nats_connected]
|
409
|
+
render json: health_data, status: :ok
|
410
|
+
else
|
411
|
+
render json: health_data.merge(error: 'NATS not connected'), status: :service_unavailable
|
412
|
+
end
|
413
|
+
rescue => e
|
414
|
+
render json: {
|
415
|
+
error: e.message,
|
416
|
+
nats_connected: false,
|
417
|
+
timestamp: Time.current.iso8601
|
418
|
+
}, status: :service_unavailable
|
419
|
+
end
|
420
|
+
end
|
421
|
+
```
|
422
|
+
|
423
|
+
### Docker Environment Configuration
|
424
|
+
|
425
|
+
```yaml
|
426
|
+
# docker-compose.yml
|
427
|
+
version: '3.8'
|
428
|
+
services:
|
429
|
+
nats:
|
430
|
+
image: nats:latest
|
431
|
+
ports:
|
432
|
+
- "4222:4222"
|
433
|
+
- "8222:8222" # Management interface
|
434
|
+
command: "--http_port 8222 --js"
|
435
|
+
|
436
|
+
app:
|
437
|
+
build: .
|
438
|
+
depends_on:
|
439
|
+
- nats
|
440
|
+
- postgres
|
441
|
+
- redis
|
442
|
+
environment:
|
443
|
+
- NATS_URL=nats://nats:4222
|
444
|
+
- NATS_SERVICE_NAME=valuation_service
|
445
|
+
- NATS_AUTH_SECRET=your-secret-key
|
446
|
+
- DD_AGENT_HOST=datadog-agent
|
447
|
+
ports:
|
448
|
+
- "3000:3000"
|
449
|
+
healthcheck:
|
450
|
+
test: [ "CMD", "curl", "-f", "http://localhost:3000/health/nats_wave" ]
|
451
|
+
interval: 30s
|
452
|
+
timeout: 10s
|
453
|
+
retries: 3
|
454
|
+
start_period: 40s
|
455
|
+
```
|
456
|
+
|
457
|
+
### Kubernetes ConfigMap
|
458
|
+
|
459
|
+
```yaml
|
460
|
+
# k8s/nats-wave-config.yaml
|
461
|
+
apiVersion: v1
|
462
|
+
kind: ConfigMap
|
463
|
+
metadata:
|
464
|
+
name: nats-wave-config
|
465
|
+
data:
|
466
|
+
NATS_URL: "nats://nats-cluster:4222"
|
467
|
+
NATS_SERVICE_NAME: "valuation-service"
|
468
|
+
---
|
469
|
+
apiVersion: v1
|
470
|
+
kind: Secret
|
471
|
+
metadata:
|
472
|
+
name: nats-wave-secrets
|
473
|
+
type: Opaque
|
474
|
+
data:
|
475
|
+
NATS_AUTH_SECRET: <base64-encoded-secret>
|
476
|
+
```
|
477
|
+
|
195
478
|
## ποΈ Model-Based Architecture
|
196
479
|
|
197
|
-
NatsWave uses a model-centric approach where each model defines its own subscriptions and mappings
|
480
|
+
NatsWave uses a model-centric approach where each model defines its own subscriptions and mappings through the *
|
481
|
+
*ModelRegistry** system.
|
198
482
|
|
199
483
|
### Publishing Events
|
200
484
|
|
@@ -235,7 +519,6 @@ class Asset < ApplicationRecord
|
|
235
519
|
|
236
520
|
# Map from inventory management system
|
237
521
|
nats_wave_maps_from 'InventoryAsset',
|
238
|
-
subjects: ['inventory.assets.*', 'warehouse.assets.*'],
|
239
522
|
field_mappings: {
|
240
523
|
'inventory_id' => 'unique_id',
|
241
524
|
'asset_description' => 'description',
|
@@ -255,44 +538,28 @@ class Asset < ApplicationRecord
|
|
255
538
|
unique_fields: [:unique_id, :vin, :serial],
|
256
539
|
sync_strategy: :upsert,
|
257
540
|
conditions: {
|
541
|
+
'source.service' => ->(service) { service != ENV['NATS_SERVICE_NAME'] }, # Do not process event messages on your server
|
258
542
|
'status' => 'available',
|
259
543
|
'condition' => ['good', 'fair', 'excellent']
|
260
|
-
}
|
544
|
+
},
|
545
|
+
subjects: ['inventory_service.assets.*', 'warehouse_service.assets.*']
|
261
546
|
|
262
|
-
# Map from auction system
|
547
|
+
# Map from auction system (different external model name)
|
263
548
|
nats_wave_maps_from 'AuctionItem',
|
264
|
-
subjects: ['auction.items.*'],
|
265
549
|
field_mappings: {
|
266
550
|
'lot_number' => 'lot_number',
|
267
551
|
'hammer_price' => 'pw_contract_price',
|
268
552
|
'sale_date' => 'pw_contract_date'
|
269
553
|
},
|
270
554
|
conditions: {
|
555
|
+
'source.service' => ->(service) { service != ENV['NATS_SERVICE_NAME'] }, # Do not process event messages on your server
|
271
556
|
'auction_status' => 'sold'
|
272
|
-
}
|
273
|
-
|
274
|
-
```
|
275
|
-
|
276
|
-
### Custom Event Handlers
|
277
|
-
|
278
|
-
```ruby
|
279
|
-
|
280
|
-
class Estimate < ApplicationRecord
|
281
|
-
include NatsWave::Concerns::Mappable
|
282
|
-
|
283
|
-
# Subscribe to valuation requests
|
284
|
-
nats_wave_subscribes_to 'valuation.requests.*' do |message|
|
285
|
-
ValuationRequestProcessor.process_request(message)
|
286
|
-
end
|
287
|
-
|
288
|
-
# Subscribe to market price updates
|
289
|
-
nats_wave_subscribes_to 'market.prices.*' do |message|
|
290
|
-
MarketPriceService.update_estimates(message)
|
291
|
-
end
|
557
|
+
},
|
558
|
+
subjects: ['auction_service.items.*']
|
292
559
|
|
293
|
-
#
|
294
|
-
nats_wave_subscribes_to
|
295
|
-
|
560
|
+
# Custom subscription with handler
|
561
|
+
nats_wave_subscribes_to "market_service.prices.*" do |message|
|
562
|
+
MarketPriceService.update_asset_estimates(message)
|
296
563
|
end
|
297
564
|
end
|
298
565
|
```
|
@@ -301,10 +568,10 @@ end
|
|
301
568
|
|
302
569
|
### Automatic Publishing
|
303
570
|
|
304
|
-
Events are automatically published when you include `NatsPublishable
|
571
|
+
Events are automatically published when you include `NatsPublishable`:
|
305
572
|
|
306
573
|
```ruby
|
307
|
-
# This automatically publishes to "valuation_service.events.
|
574
|
+
# This automatically publishes to "valuation_service.events.assets.create"
|
308
575
|
asset = Asset.create!(
|
309
576
|
make: "Caterpillar",
|
310
577
|
model: "320D",
|
@@ -313,10 +580,10 @@ asset = Asset.create!(
|
|
313
580
|
description: "Excavator - Track Type"
|
314
581
|
)
|
315
582
|
|
316
|
-
# This publishes to "valuation_service.events.
|
583
|
+
# This publishes to "valuation_service.events.assets.update"
|
317
584
|
asset.update!(estimated_hammer: 125000)
|
318
585
|
|
319
|
-
# This publishes to "valuation_service.events.
|
586
|
+
# This publishes to "valuation_service.events.assets.destroy"
|
320
587
|
asset.destroy!
|
321
588
|
```
|
322
589
|
|
@@ -354,43 +621,22 @@ end
|
|
354
621
|
NatsWave.client.publish_batch(events)
|
355
622
|
```
|
356
623
|
|
357
|
-
### Conditional Publishing
|
358
|
-
|
359
|
-
```ruby
|
360
|
-
|
361
|
-
class Asset < ApplicationRecord
|
362
|
-
include NatsWave::NatsPublishable
|
363
|
-
|
364
|
-
nats_publishable(
|
365
|
-
if: -> { should_publish_to_external_systems? },
|
366
|
-
unless: -> { asset_status == 'draft' }
|
367
|
-
)
|
368
|
-
|
369
|
-
private
|
370
|
-
|
371
|
-
def should_publish_to_external_systems?
|
372
|
-
asset_status == 'completed' &&
|
373
|
-
package.workflow_status == 'finalized' &&
|
374
|
-
!package.is_shared?
|
375
|
-
end
|
376
|
-
end
|
377
|
-
```
|
378
|
-
|
379
624
|
## π₯ Subscribing to Events
|
380
625
|
|
381
|
-
### Automatic Model Syncing
|
626
|
+
### Automatic Model Syncing with Field Mapping
|
382
627
|
|
383
628
|
When you configure `nats_wave_maps_from`, NatsWave automatically:
|
384
629
|
|
385
|
-
1.
|
386
|
-
2.
|
387
|
-
3.
|
388
|
-
4.
|
630
|
+
1. **Registers the subscription** in the ModelRegistry
|
631
|
+
2. **Subscribes to specified subjects** when the application starts
|
632
|
+
3. **Transforms incoming data** using field mappings and transformations
|
633
|
+
4. **Applies conditions** to filter which messages to process
|
634
|
+
5. **Syncs data** to your local models using the specified strategy
|
389
635
|
|
390
636
|
```ruby
|
391
|
-
# Incoming message from inventory system
|
637
|
+
# Example: Incoming message from inventory system
|
392
638
|
{
|
393
|
-
"subject": "
|
639
|
+
"subject": "inventory_service.assets.update",
|
394
640
|
"model": "InventoryAsset",
|
395
641
|
"action": "update",
|
396
642
|
"data": {
|
@@ -401,139 +647,327 @@ When you configure `nats_wave_maps_from`, NatsWave automatically:
|
|
401
647
|
"year_built": "2020",
|
402
648
|
"serial_num": "1FTFW1ET5LFA12345",
|
403
649
|
"vin_code": "1FTFW1ET5LFA12345",
|
404
|
-
"estimated_value": 35000.00
|
650
|
+
"estimated_value": 35000.00,
|
651
|
+
"status": "available"
|
652
|
+
},
|
653
|
+
"source": {
|
654
|
+
"service": "inventory_service",
|
655
|
+
"instance_id": "inv-worker-1"
|
405
656
|
}
|
406
657
|
}
|
407
658
|
|
408
|
-
#
|
659
|
+
# NatsWave automatically transforms this to:
|
409
660
|
{
|
410
661
|
"unique_id": "INV-2024-001",
|
411
662
|
"description": "2020 Ford F-150 Pickup Truck",
|
412
|
-
"make": "FORD",
|
663
|
+
"make": "FORD", # Uppercased via transformation
|
413
664
|
"model": "F-150",
|
414
|
-
"year": 2020,
|
665
|
+
"year": 2020, # Converted to integer
|
415
666
|
"serial": "1FTFW1ET5LFA12345",
|
416
667
|
"vin": "1FTFW1ET5LFA12345",
|
417
668
|
"estimated_hammer": 35000.00
|
418
669
|
}
|
419
670
|
|
420
|
-
# And updates
|
671
|
+
# And creates/updates the local Asset record using unique_fields [:unique_id, :vin, :serial]
|
672
|
+
```
|
673
|
+
|
674
|
+
## πΊοΈ Data Mapping & Transformation
|
675
|
+
|
676
|
+
### Complete Field Mapping Options
|
677
|
+
|
678
|
+
```ruby
|
679
|
+
nats_wave_maps_from 'ExternalModel',
|
680
|
+
# === FIELD MAPPINGS ===
|
681
|
+
field_mappings: {
|
682
|
+
'external_field' => 'local_field',
|
683
|
+
'their_id' => 'external_id',
|
684
|
+
'their_name' => 'title',
|
685
|
+
'created_timestamp' => 'created_at'
|
686
|
+
},
|
687
|
+
|
688
|
+
# === DATA TRANSFORMATIONS ===
|
689
|
+
transformations: {
|
690
|
+
'created_at' => :parse_timestamp, # Method call
|
691
|
+
'make' => ->(value) { value&.upcase&.strip }, # Lambda
|
692
|
+
'year' => ->(year) { year.to_i if year.present? },
|
693
|
+
'price' => :convert_to_cents, # Custom method
|
694
|
+
'status' => ->(val, record) { "#{record['prefix']}_#{val}" } # With record access
|
695
|
+
},
|
696
|
+
|
697
|
+
# === CONDITIONAL SYNC ===
|
698
|
+
conditions: {
|
699
|
+
'source.service' => ->(service) { service != ENV['NATS_SERVICE_NAME'] }, # Do not process event messages on your server
|
700
|
+
'status' => 'active', # Exact match
|
701
|
+
'service_type' => ['premium', 'basic'], # Array inclusion
|
702
|
+
'value' => ->(val) { val.to_f > 1000 }, # Custom logic
|
703
|
+
'category' => /^(vehicle|equipment)$/ # Regex match
|
704
|
+
},
|
705
|
+
|
706
|
+
# === SYNC STRATEGY ===
|
707
|
+
sync_strategy: :upsert, # Options: :upsert, :create_only, :update_only
|
708
|
+
|
709
|
+
# === UNIQUE IDENTIFICATION ===
|
710
|
+
unique_fields: [:external_id, :vin, :serial], # Fields to find existing records
|
711
|
+
|
712
|
+
# === SKIP FIELDS ===
|
713
|
+
skip_fields: [:internal_notes, :secret_key], # Fields to ignore
|
714
|
+
|
715
|
+
# === SUBSCRIPTION CONFIGURATION ===
|
716
|
+
subjects: [
|
717
|
+
'inventory_service.items.*',
|
718
|
+
'warehouse_service.assets.*',
|
719
|
+
'external_api.inventory.*'
|
720
|
+
]
|
421
721
|
```
|
422
722
|
|
423
|
-
###
|
723
|
+
### Advanced Transformations
|
424
724
|
|
425
725
|
```ruby
|
426
726
|
|
427
|
-
class
|
727
|
+
class Asset < ApplicationRecord
|
428
728
|
include NatsWave::Concerns::Mappable
|
429
729
|
|
430
|
-
|
431
|
-
|
432
|
-
|
730
|
+
nats_wave_maps_from 'ExternalAsset',
|
731
|
+
transformations: {
|
732
|
+
# Simple lambda transformations
|
733
|
+
'estimated_hammer' => ->(value) { value.to_f.round(2) },
|
734
|
+
'make' => ->(make) { make&.upcase&.strip },
|
735
|
+
|
736
|
+
# Method transformations
|
737
|
+
'vin' => :normalize_vin,
|
738
|
+
'serial' => :normalize_serial,
|
739
|
+
|
740
|
+
# Complex transformations with full record access
|
741
|
+
'description' => ->(value, record) {
|
742
|
+
"#{record['year']} #{record['make']} #{record['model']} #{value}".strip
|
743
|
+
},
|
744
|
+
|
745
|
+
# Conditional transformations
|
746
|
+
'condition' => ->(condition) {
|
747
|
+
case condition&.downcase
|
748
|
+
when 'excellent', 'like new' then 'excellent'
|
749
|
+
when 'good', 'fair' then 'good'
|
750
|
+
when 'poor', 'needs repair' then 'poor'
|
751
|
+
else 'unknown'
|
752
|
+
end
|
753
|
+
}
|
754
|
+
}
|
755
|
+
|
756
|
+
private
|
757
|
+
|
758
|
+
def self.normalize_vin(vin)
|
759
|
+
vin&.upcase&.gsub(/[^A-Z0-9]/, '')&.strip
|
760
|
+
end
|
761
|
+
|
762
|
+
def self.normalize_serial(serial)
|
763
|
+
serial&.upcase&.strip&.gsub(/\s+/, '')
|
433
764
|
end
|
434
765
|
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
766
|
+
def self.parse_timestamp(timestamp)
|
767
|
+
return nil if timestamp.blank?
|
768
|
+
Time.parse(timestamp.to_s)
|
769
|
+
rescue ArgumentError
|
770
|
+
nil
|
771
|
+
end
|
772
|
+
|
773
|
+
def self.convert_to_cents(dollars)
|
774
|
+
(dollars.to_f * 100).round
|
439
775
|
end
|
440
776
|
end
|
441
777
|
```
|
442
778
|
|
443
|
-
##
|
779
|
+
## π Cross-Service Integration
|
444
780
|
|
445
|
-
###
|
781
|
+
### Handling Different Services with Same Model Names
|
446
782
|
|
447
783
|
```ruby
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
784
|
+
|
785
|
+
class Package < ApplicationRecord
|
786
|
+
include NatsWave::Concerns::Mappable
|
787
|
+
|
788
|
+
# Map from inventory service's Package model
|
789
|
+
nats_wave_maps_from 'Package',
|
790
|
+
field_mappings: { 'sku' => 'product_code', 'inventory_status' => 'status' },
|
791
|
+
subjects: ['inventory_service.packages.*'],
|
792
|
+
conditions: {
|
793
|
+
'source.service' => ->(service) { service != ENV['NATS_SERVICE_NAME'] }, # Do not process event messages on your server
|
794
|
+
},
|
795
|
+
unique_fields: [:product_code, :source_service]
|
796
|
+
|
797
|
+
# Map from order service's Package model (different fields)
|
798
|
+
nats_wave_maps_from 'Package',
|
799
|
+
field_mappings: { 'order_item_id' => 'external_order_id', 'item_name' => 'name' },
|
800
|
+
subjects: ['order_service.packages.*'],
|
801
|
+
conditions: {
|
802
|
+
'source.service' => ->(service) { service != ENV['NATS_SERVICE_NAME'] }, # Do not process event messages on your server
|
803
|
+
},
|
804
|
+
unique_fields: [:external_order_id, :source_service]
|
805
|
+
|
806
|
+
# Custom subscription with service-specific routing
|
807
|
+
nats_wave_subscribes_to(
|
808
|
+
'inventory_service.packages.*',
|
809
|
+
'order_service.packages.*',
|
810
|
+
'fulfillment_service.packages.*'
|
811
|
+
) do |message|
|
812
|
+
service_name = message.dig('source', 'service')
|
813
|
+
|
814
|
+
case service_name
|
815
|
+
when 'inventory_service'
|
816
|
+
InventoryPackageProcessor.process(message)
|
817
|
+
when 'order_service'
|
818
|
+
OrderPackageProcessor.process(message)
|
819
|
+
when 'fulfillment_service'
|
820
|
+
FulfillmentPackageProcessor.process(message)
|
821
|
+
else
|
822
|
+
Rails.logger.warn "Unknown service: #{service_name}"
|
823
|
+
end
|
824
|
+
end
|
825
|
+
end
|
458
826
|
```
|
459
827
|
|
460
|
-
###
|
828
|
+
### Using Different External Model Names
|
461
829
|
|
462
830
|
```ruby
|
463
|
-
nats_wave_maps_from 'ExternalAsset',
|
464
|
-
transformations: {
|
465
|
-
# Lambda transformations
|
466
|
-
'estimated_hammer' => ->(value) { value.to_f.round(2) },
|
467
|
-
'make' => ->(make) { make&.upcase&.strip },
|
468
|
-
'model' => ->(model) { model&.titleize&.strip },
|
469
|
-
|
470
|
-
# Method transformations
|
471
|
-
'vin' => :normalize_vin,
|
472
|
-
'serial' => :normalize_serial,
|
473
|
-
|
474
|
-
# Complex transformations with access to full record
|
475
|
-
'description' => ->(value, record) {
|
476
|
-
"#{record['year']} #{record['make']} #{record['model']} #{value}".strip
|
477
|
-
}
|
478
|
-
}
|
479
|
-
|
480
|
-
private
|
481
831
|
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
832
|
+
class Asset < ApplicationRecord
|
833
|
+
include NatsWave::Concerns::Mappable
|
834
|
+
|
835
|
+
# Map different external model names to local Asset
|
836
|
+
nats_wave_maps_from 'InventoryItem',
|
837
|
+
field_mappings: { 'sku' => 'product_code', 'stock_level' => 'quantity' },
|
838
|
+
subjects: ['inventory_service.inventory_items.*'],
|
839
|
+
unique_fields: [:product_code]
|
840
|
+
|
841
|
+
nats_wave_maps_from 'OrderItem',
|
842
|
+
field_mappings: { 'item_id' => 'external_order_id', 'item_name' => 'name' },
|
843
|
+
subjects: ['order_service.order_items.*'],
|
844
|
+
unique_fields: [:external_order_id]
|
845
|
+
|
846
|
+
nats_wave_maps_from 'ShipmentItem',
|
847
|
+
field_mappings: { 'tracking_code' => 'tracking_number' },
|
848
|
+
subjects: ['fulfillment_service.shipment_items.*'],
|
849
|
+
unique_fields: [:tracking_number]
|
488
850
|
end
|
489
851
|
```
|
490
852
|
|
491
|
-
|
853
|
+
## π― Message Processing & Handlers
|
854
|
+
|
855
|
+
### Automatic Message Processing
|
856
|
+
|
857
|
+
NatsWave includes a comprehensive `MessageHandler` that automatically processes mapped messages:
|
492
858
|
|
493
859
|
```ruby
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
860
|
+
# When a message is received, NatsWave automatically:
|
861
|
+
# 1. Finds local models that can handle the external model
|
862
|
+
# 2. Applies field mappings and transformations
|
863
|
+
# 3. Checks conditions
|
864
|
+
# 4. Performs create/update/delete based on the action
|
865
|
+
# 5. Uses unique_fields to find existing records for updates
|
866
|
+
|
867
|
+
module NatsWave
|
868
|
+
class MessageHandler
|
869
|
+
def self.process(message)
|
870
|
+
# Find which local model(s) can handle this external model
|
871
|
+
local_models = ModelRegistry.local_models_for(message['model'])
|
872
|
+
|
873
|
+
local_models.each do |local_model_name, mapping_config|
|
874
|
+
# Transform data using field mappings
|
875
|
+
transformed_data = transform_data(message['data'], mapping_config)
|
876
|
+
|
877
|
+
# Get model class and perform the sync
|
878
|
+
model_class = local_model_name.constantize
|
879
|
+
|
880
|
+
case message['action']
|
881
|
+
when 'create', 'created'
|
882
|
+
sync_create(model_class, transformed_data, mapping_config)
|
883
|
+
when 'update', 'updated'
|
884
|
+
sync_update(model_class, transformed_data, mapping_config)
|
885
|
+
when 'delete', 'deleted', 'destroy'
|
886
|
+
sync_delete(model_class, transformed_data, mapping_config)
|
887
|
+
end
|
888
|
+
end
|
889
|
+
end
|
890
|
+
end
|
891
|
+
end
|
501
892
|
```
|
502
893
|
|
503
|
-
###
|
894
|
+
### Custom Message Handlers
|
504
895
|
|
505
896
|
```ruby
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
897
|
+
|
898
|
+
class CustomAssetHandler
|
899
|
+
def self.process(message)
|
900
|
+
case message['action']
|
901
|
+
when 'valuation_requested'
|
902
|
+
ValuationService.queue_valuation(message['data'])
|
903
|
+
when 'images_updated'
|
904
|
+
ImageProcessingService.process_asset_images(message['data'])
|
905
|
+
when 'auction_scheduled'
|
906
|
+
AuctionNotificationService.notify_subscribers(message['data'])
|
907
|
+
end
|
908
|
+
end
|
909
|
+
end
|
910
|
+
|
911
|
+
class Asset < ApplicationRecord
|
912
|
+
include NatsWave::Concerns::Mappable
|
913
|
+
|
914
|
+
# Use custom handler for specific subjects
|
915
|
+
nats_wave_subscribes_to 'valuation.assets.*' do |message|
|
916
|
+
CustomAssetHandler.process(message)
|
917
|
+
end
|
918
|
+
end
|
511
919
|
```
|
512
920
|
|
513
921
|
## π Monitoring & Metrics
|
514
922
|
|
923
|
+
### ModelRegistry Inspection
|
924
|
+
|
925
|
+
```ruby
|
926
|
+
# View all registered subscriptions
|
927
|
+
NatsWave::ModelRegistry.debug_registrations
|
928
|
+
|
929
|
+
# Get subscription statistics
|
930
|
+
stats = NatsWave::ModelRegistry.subscription_stats
|
931
|
+
# => {
|
932
|
+
# total_subscriptions: 5,
|
933
|
+
# unique_subjects: 8,
|
934
|
+
# models_with_subscriptions: 3,
|
935
|
+
# subscription_breakdown: {"Asset"=>2, "Package"=>2, "User"=>1}
|
936
|
+
# }
|
937
|
+
|
938
|
+
# View all mappings
|
939
|
+
mappings = NatsWave::ModelRegistry.all_mappings
|
940
|
+
# => {
|
941
|
+
# local_to_external: {...},
|
942
|
+
# external_to_local: {"InventoryItem" => {"Asset" => {...}}}
|
943
|
+
# }
|
944
|
+
|
945
|
+
# Find local models for external model
|
946
|
+
local_models = NatsWave::ModelRegistry.local_models_for('InventoryItem')
|
947
|
+
# => {"Asset" => {field_mappings: {...}, transformations: {...}}}
|
948
|
+
```
|
949
|
+
|
515
950
|
### Datadog Integration
|
516
951
|
|
517
|
-
NatsWave provides comprehensive Datadog metrics
|
952
|
+
NatsWave provides comprehensive Datadog metrics:
|
518
953
|
|
519
954
|
```ruby
|
520
955
|
# Automatic metrics tracked:
|
521
956
|
-nats_wave.messages.published # Messages published by subject/model
|
522
|
-
-nats_wave.messages.received # Messages received by subject/model
|
523
|
-
-nats_wave.processing_time # Message processing duration
|
957
|
+
-nats_wave.messages.received # Messages received by subject/model
|
958
|
+
-nats_wave.processing_time # Message processing duration
|
524
959
|
-nats_wave.errors # Errors by type and subject
|
525
960
|
-nats_wave.connection_status # NATS connection health
|
526
961
|
-nats_wave.sync.operations # Model sync operations
|
527
962
|
-nats_wave.retries # Retry attempts
|
528
|
-
-nats_wave.
|
529
|
-
-nats_wave.assets.synced # Asset synchronization events
|
963
|
+
-nats_wave.model_registry.subscriptions # Active subscriptions
|
530
964
|
```
|
531
965
|
|
532
966
|
### Health Checks
|
533
967
|
|
534
968
|
```ruby
|
535
|
-
# Built-in health check
|
536
|
-
|
969
|
+
# Built-in health check
|
970
|
+
health = NatsWave.health_check
|
537
971
|
|
538
972
|
# Response:
|
539
973
|
{
|
@@ -541,71 +975,51 @@ GET /health/n ats_wave
|
|
541
975
|
"database_connected": true,
|
542
976
|
"timestamp": "2024-01-01T12:00:00Z",
|
543
977
|
"service_name": "valuation_service",
|
544
|
-
"version": "1.
|
978
|
+
"version": "1.2.0",
|
545
979
|
"instance_id": "web-1",
|
546
980
|
"published_subjects": [
|
547
|
-
"valuation_service.events.
|
548
|
-
"valuation_service.events.
|
549
|
-
"valuation_service.events.estimate.*"
|
981
|
+
"valuation_service.events.assets.*",
|
982
|
+
"valuation_service.events.packages.*"
|
550
983
|
],
|
551
984
|
"subscribed_subjects": [
|
552
|
-
"
|
553
|
-
"
|
554
|
-
"market.prices.*"
|
985
|
+
"inventory_service.items.*",
|
986
|
+
"auction_service.assets.*"
|
555
987
|
],
|
556
|
-
"
|
988
|
+
"model_registry_stats": {
|
989
|
+
"total_subscriptions": 5,
|
990
|
+
"unique_subjects": 8
|
991
|
+
}
|
557
992
|
}
|
558
993
|
```
|
559
994
|
|
560
|
-
### Monitoring Commands
|
561
|
-
|
562
|
-
```bash
|
563
|
-
# Check overall health
|
564
|
-
rails nats_wave:health
|
565
|
-
|
566
|
-
# Show all model subscriptions
|
567
|
-
rails nats_wave:model_subscriptions
|
568
|
-
|
569
|
-
# Monitor live NATS activity
|
570
|
-
rails nats_wave:monitor
|
571
|
-
|
572
|
-
# Show failed messages
|
573
|
-
rails nats_wave:show_failed
|
574
|
-
|
575
|
-
# Retry failed messages
|
576
|
-
rails nats_wave:retry_failed
|
577
|
-
```
|
578
|
-
|
579
995
|
## π‘οΈ Error Handling
|
580
996
|
|
581
997
|
### Dead Letter Queue
|
582
998
|
|
583
|
-
Failed messages are automatically stored
|
999
|
+
Failed messages are automatically stored with retry logic:
|
584
1000
|
|
585
1001
|
```ruby
|
586
1002
|
# Automatic retry with exponential backoff
|
587
1003
|
# Retry 1: after 5 seconds
|
588
|
-
# Retry 2: after 25 seconds
|
589
|
-
# Retry 3: after 125 seconds
|
1004
|
+
# Retry 2: after 25 seconds
|
1005
|
+
# Retry 3: after 125 seconds
|
590
1006
|
# After max retries: moved to permanent failure storage
|
591
1007
|
```
|
592
1008
|
|
593
|
-
### Error Recovery
|
1009
|
+
### Error Recovery Commands
|
1010
|
+
|
1011
|
+
```bash
|
1012
|
+
# View failed messages
|
1013
|
+
rails nats_wave:show_failed
|
594
1014
|
|
595
|
-
```ruby
|
596
1015
|
# Retry failed messages
|
597
|
-
rails nats_wave:
|
1016
|
+
rails nats_wave:retry_failed
|
598
1017
|
|
599
1018
|
# Clear failed messages (after manual review)
|
600
|
-
rails nats_wave:
|
1019
|
+
rails nats_wave:clear_failed
|
601
1020
|
|
602
|
-
#
|
603
|
-
|
604
|
-
failed_messages.get_failed_messages.each do |msg|
|
605
|
-
puts "Error: #{msg[:error]}"
|
606
|
-
puts "Retry Count: #{msg[:retry_count]}"
|
607
|
-
puts "Message: #{msg[:message]}"
|
608
|
-
end
|
1021
|
+
# Monitor live activity
|
1022
|
+
rails nats_wave:monitor
|
609
1023
|
```
|
610
1024
|
|
611
1025
|
### Custom Error Handling
|
@@ -621,9 +1035,85 @@ class CustomErrorHandler
|
|
621
1035
|
service: 'nats_wave'
|
622
1036
|
})
|
623
1037
|
|
624
|
-
# Custom retry logic for
|
625
|
-
if
|
626
|
-
|
1038
|
+
# Custom retry logic for specific errors
|
1039
|
+
if error.is_a?(ValidationError) && retry_count < 5
|
1040
|
+
RetryValidationJob.set(wait: 30.seconds).perform_later(message)
|
1041
|
+
end
|
1042
|
+
end
|
1043
|
+
end
|
1044
|
+
```
|
1045
|
+
|
1046
|
+
## π§ͺ Testing
|
1047
|
+
|
1048
|
+
### Test Configuration
|
1049
|
+
|
1050
|
+
```ruby
|
1051
|
+
# spec/spec_helper.rb or test/test_helper.rb
|
1052
|
+
RSpec.configure do |config|
|
1053
|
+
config.before(:suite) do
|
1054
|
+
# Clear ModelRegistry before tests
|
1055
|
+
NatsWave::ModelRegistry.clear!
|
1056
|
+
end
|
1057
|
+
|
1058
|
+
config.before(:each) do
|
1059
|
+
# Mock NATS in tests
|
1060
|
+
allow(NatsWave.client).to receive(:publish)
|
1061
|
+
allow(NatsWave.client).to receive(:subscribe)
|
1062
|
+
end
|
1063
|
+
end
|
1064
|
+
```
|
1065
|
+
|
1066
|
+
### Testing Model Subscriptions
|
1067
|
+
|
1068
|
+
```ruby
|
1069
|
+
# spec/models/asset_spec.rb
|
1070
|
+
RSpec.describe Asset, type: :model do
|
1071
|
+
describe 'NATS Wave configuration' do
|
1072
|
+
it 'registers correct subscriptions' do
|
1073
|
+
subscriptions = NatsWave::ModelRegistry.subscriptions_for_model('Asset')
|
1074
|
+
expect(subscriptions.size).to eq(2)
|
1075
|
+
|
1076
|
+
subjects = subscriptions.flat_map { |s| s[:subjects] }
|
1077
|
+
expect(subjects).to include('inventory_service.items.*')
|
1078
|
+
expect(subjects).to include('auction_service.assets.*')
|
1079
|
+
end
|
1080
|
+
|
1081
|
+
it 'has correct field mappings' do
|
1082
|
+
mapping = Asset.nats_wave_mapping_for('InventoryItem')
|
1083
|
+
expect(mapping[:field_mappings]['item_id']).to eq('unique_id')
|
1084
|
+
expect(mapping[:field_mappings]['manufacturer']).to eq('make')
|
1085
|
+
end
|
1086
|
+
end
|
1087
|
+
end
|
1088
|
+
```
|
1089
|
+
|
1090
|
+
### Testing Message Processing
|
1091
|
+
|
1092
|
+
```ruby
|
1093
|
+
# spec/services/message_handler_spec.rb
|
1094
|
+
RSpec.describe NatsWave::MessageHandler do
|
1095
|
+
describe '.process' do
|
1096
|
+
it 'creates asset from inventory message' do
|
1097
|
+
message = {
|
1098
|
+
'model' => 'InventoryItem',
|
1099
|
+
'action' => 'create',
|
1100
|
+
'data' => {
|
1101
|
+
'item_id' => 'INV-001',
|
1102
|
+
'manufacturer' => 'caterpillar',
|
1103
|
+
'model_number' => '320D',
|
1104
|
+
'year_built' => '2020'
|
1105
|
+
},
|
1106
|
+
'source' => { 'service' => 'inventory_service' }
|
1107
|
+
}
|
1108
|
+
|
1109
|
+
expect {
|
1110
|
+
NatsWave::MessageHandler.process(message)
|
1111
|
+
}.to change(Asset, :count).by(1)
|
1112
|
+
|
1113
|
+
asset = Asset.last
|
1114
|
+
expect(asset.unique_id).to eq('INV-001')
|
1115
|
+
expect(asset.make).to eq('CATERPILLAR') # Uppercased by transformation
|
1116
|
+
expect(asset.year).to eq(2020) # Converted to integer
|
627
1117
|
end
|
628
1118
|
end
|
629
1119
|
end
|
@@ -651,30 +1141,21 @@ EXPOSE 3000
|
|
651
1141
|
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]
|
652
1142
|
```
|
653
1143
|
|
654
|
-
###
|
1144
|
+
### Environment Configuration
|
655
1145
|
|
656
|
-
```
|
657
|
-
#
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
image: nats:latest
|
662
|
-
ports:
|
663
|
-
- "4222:4222"
|
664
|
-
- "8222:8222"
|
665
|
-
command: "--http_port 8222 --js"
|
1146
|
+
```bash
|
1147
|
+
# Production environment variables
|
1148
|
+
NATS_URL=nats://nats-cluster:4222
|
1149
|
+
NATS_SERVICE_NAME=valuation_service
|
1150
|
+
NATS_AUTH_SECRET=your-production-secret
|
666
1151
|
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
- NATS_URL=nats://nats:4222
|
675
|
-
- NATS_SERVICE_NAME=valuation_service
|
676
|
-
ports:
|
677
|
-
- "3000:3000"
|
1152
|
+
# Monitoring
|
1153
|
+
DD_AGENT_HOST=datadog-agent
|
1154
|
+
DD_AGENT_PORT=8125
|
1155
|
+
|
1156
|
+
# Performance
|
1157
|
+
RAILS_MAX_THREADS=10
|
1158
|
+
WEB_CONCURRENCY=3
|
678
1159
|
```
|
679
1160
|
|
680
1161
|
### Kubernetes Deployment
|
@@ -711,131 +1192,442 @@ spec:
|
|
711
1192
|
port: 3000
|
712
1193
|
initialDelaySeconds: 30
|
713
1194
|
periodSeconds: 10
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
```bash
|
721
|
-
# Procfile
|
722
|
-
web: bundle exec rails server -p $PORT
|
723
|
-
worker: bundle exec rails nats_wave:start
|
1195
|
+
readinessProbe:
|
1196
|
+
httpGet:
|
1197
|
+
path: /health/nats_wave
|
1198
|
+
port: 3000
|
1199
|
+
initialDelaySeconds: 5
|
1200
|
+
periodSeconds: 5
|
724
1201
|
```
|
725
1202
|
|
726
1203
|
## π‘ Examples
|
727
1204
|
|
728
|
-
### Asset
|
1205
|
+
### Complete Asset Valuation Integration
|
729
1206
|
|
730
1207
|
```ruby
|
731
1208
|
# app/models/asset.rb
|
732
1209
|
class Asset < ApplicationRecord
|
733
|
-
|
734
|
-
|
1210
|
+
include NatsWave::NatsPublishable
|
1211
|
+
include NatsWave::Concerns::Mappable
|
1212
|
+
|
1213
|
+
belongs_to :package
|
1214
|
+
belongs_to :creator, class_name: 'User', optional: true
|
1215
|
+
belongs_to :last_updater, class_name: 'User', optional: true
|
1216
|
+
belongs_to :subpackage, optional: true
|
1217
|
+
has_many :estimates, dependent: :destroy
|
1218
|
+
has_many :images, dependent: :destroy
|
1219
|
+
has_many :videos, dependent: :destroy
|
1220
|
+
has_many :links, dependent: :destroy
|
1221
|
+
has_many :attachments, dependent: :destroy
|
1222
|
+
|
1223
|
+
# Publish asset changes to other services
|
1224
|
+
nats_publishable(
|
1225
|
+
actions: [:create, :update],
|
1226
|
+
skip_attributes: [:notes, :edge_data, :source_data, :ims_forms_data],
|
1227
|
+
include_associations: [:package, :estimates],
|
1228
|
+
subject_prefix: 'assets',
|
1229
|
+
if: -> { asset_status != 'draft' }
|
1230
|
+
)
|
1231
|
+
|
1232
|
+
# Sync from external inventory management system
|
1233
|
+
nats_wave_maps_from 'InventoryAsset',
|
1234
|
+
field_mappings: {
|
1235
|
+
'inventory_id' => 'unique_id',
|
1236
|
+
'asset_description' => 'description',
|
1237
|
+
'manufacturer' => 'make',
|
1238
|
+
'model_number' => 'model',
|
1239
|
+
'year_manufactured' => 'year',
|
1240
|
+
'serial_number' => 'serial',
|
1241
|
+
'vin_number' => 'vin',
|
1242
|
+
'location_name' => 'location',
|
1243
|
+
'inventory_tag' => 'inventory_tag',
|
1244
|
+
'sticker_number' => 'sticker',
|
1245
|
+
'estimated_value' => 'estimated_hammer'
|
1246
|
+
},
|
1247
|
+
transformations: {
|
1248
|
+
'make' => ->(make) { make&.upcase&.strip },
|
1249
|
+
'model' => ->(model) { model&.titleize&.strip },
|
1250
|
+
'year' => ->(year) { year.to_i if year.present? },
|
1251
|
+
'estimated_hammer' => ->(value) { (value.to_f * 100).round }, # Convert to cents
|
1252
|
+
'asset_status' => ->(status) { status&.downcase || 'in_progress' }
|
1253
|
+
},
|
1254
|
+
unique_fields: [:unique_id, :vin, :serial],
|
1255
|
+
sync_strategy: :upsert,
|
1256
|
+
conditions: {
|
1257
|
+
'source.service' => ->(service) { service != ENV['NATS_SERVICE_NAME'] },
|
1258
|
+
'status' => ['available', 'reserved', 'active'],
|
1259
|
+
'estimated_value' => ->(value) { value.to_f > 0 }
|
1260
|
+
},
|
1261
|
+
subjects: ['inventory_service.assets.*']
|
735
1262
|
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
1263
|
+
# Sync from auction management system
|
1264
|
+
nats_wave_maps_from 'AuctionLot',
|
1265
|
+
field_mappings: {
|
1266
|
+
'lot_number' => 'lot_number',
|
1267
|
+
'hammer_price' => 'pw_contract_price',
|
1268
|
+
'sale_date' => 'pw_contract_date',
|
1269
|
+
'auction_house' => 'pw_auction'
|
1270
|
+
},
|
1271
|
+
transformations: {
|
1272
|
+
'pw_contract_price' => ->(price) { (price.to_f * 100).round }, # Convert to cents
|
1273
|
+
'pw_contract_date' => ->(date) { Time.parse(date.to_s) rescue nil }
|
1274
|
+
},
|
1275
|
+
conditions: {
|
1276
|
+
'source.service' => ->(service) { service != ENV['NATS_SERVICE_NAME'] },
|
1277
|
+
'status' => 'sold'
|
1278
|
+
},
|
1279
|
+
subjects: ['auction_service.lots.*']
|
740
1280
|
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
nats_wave_maps_from 'AuctionAsset',
|
761
|
-
subjects: ['auction.assets.*'],
|
762
|
-
field_mappings: {
|
763
|
-
'lot_number' => 'lot_number',
|
764
|
-
'hammer_price' => 'pw_contract_price',
|
765
|
-
'sale_date' => 'pw_contract_date'
|
766
|
-
},
|
767
|
-
conditions: {
|
768
|
-
'status' => 'sold'
|
769
|
-
}
|
1281
|
+
# Listen for valuation requests
|
1282
|
+
nats_wave_subscribes_to 'valuation_service.requests.*' do |message|
|
1283
|
+
AssetValuationProcessor.process_request(message)
|
1284
|
+
end
|
1285
|
+
|
1286
|
+
private
|
1287
|
+
|
1288
|
+
def nats_wave_metadata
|
1289
|
+
{
|
1290
|
+
package_key: package&.key,
|
1291
|
+
organization_id: package&.organization_id,
|
1292
|
+
creator_name: creator&.name,
|
1293
|
+
valuation_type: valuation_type,
|
1294
|
+
estimated_value: estimated_hammer,
|
1295
|
+
image_count: images.where(deleted: false).count,
|
1296
|
+
video_count: videos.where(deleted: false).count,
|
1297
|
+
has_estimates: estimates.where(status: 'current').exists?
|
1298
|
+
}
|
1299
|
+
end
|
770
1300
|
end
|
771
1301
|
|
772
1302
|
# app/models/package.rb
|
773
1303
|
class Package < ApplicationRecord
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
1304
|
+
include NatsWave::NatsPublishable
|
1305
|
+
include NatsWave::Concerns::Mappable
|
1306
|
+
|
1307
|
+
belongs_to :organization
|
1308
|
+
belongs_to :contact_user, class_name: 'User', optional: true
|
1309
|
+
has_many :assets, dependent: :destroy
|
1310
|
+
has_many :subpackages, dependent: :destroy
|
1311
|
+
has_many :valuation_requests, dependent: :destroy
|
1312
|
+
has_many :ims_submissions, dependent: :destroy
|
1313
|
+
|
1314
|
+
# Publish package workflow changes
|
1315
|
+
nats_publishable(
|
1316
|
+
actions: [:create, :update],
|
1317
|
+
skip_attributes: [:notes, :source_data],
|
1318
|
+
include_associations: [:organization, :assets],
|
1319
|
+
subject_prefix: 'packages',
|
1320
|
+
if: -> { workflow_status.in?(['active', 'finalized']) }
|
1321
|
+
)
|
1322
|
+
|
1323
|
+
# Sync from CRM/Project management systems
|
1324
|
+
nats_wave_maps_from 'CRMProject',
|
1325
|
+
field_mappings: {
|
1326
|
+
'project_id' => 'key',
|
1327
|
+
'client_name' => 'owner_name',
|
1328
|
+
'client_email' => 'owner_email',
|
1329
|
+
'project_location' => 'location',
|
1330
|
+
'project_status' => 'workflow_status',
|
1331
|
+
'project_notes' => 'notes',
|
1332
|
+
'opportunity_reference' => 'opportunity_id',
|
1333
|
+
'contact_person' => 'primary_contact'
|
1334
|
+
},
|
1335
|
+
transformations: {
|
1336
|
+
'workflow_status' => ->(status) {
|
1337
|
+
case status&.downcase
|
1338
|
+
when 'new', 'created' then 'active'
|
1339
|
+
when 'in_progress', 'working' then 'active'
|
1340
|
+
when 'completed', 'done' then 'finalized'
|
1341
|
+
else 'active'
|
1342
|
+
end
|
1343
|
+
}
|
1344
|
+
},
|
1345
|
+
unique_fields: [:key],
|
1346
|
+
conditions: {
|
1347
|
+
'source.service' => ->(service) { service != ENV['NATS_SERVICE_NAME'] }
|
1348
|
+
},
|
1349
|
+
subjects: ['crm_service.projects.*']
|
780
1350
|
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
1351
|
+
# Listen for import completion notifications
|
1352
|
+
nats_wave_subscribes_to 'ims_service.imports.*' do |message|
|
1353
|
+
IMSImportProcessor.process_completion(message)
|
1354
|
+
end
|
1355
|
+
|
1356
|
+
private
|
1357
|
+
|
1358
|
+
def nats_wave_metadata
|
1359
|
+
{
|
1360
|
+
organization_name: organization&.name,
|
1361
|
+
organization_key: organization&.key,
|
1362
|
+
asset_count: assets.count,
|
1363
|
+
total_estimated_value: assets.sum(:estimated_hammer),
|
1364
|
+
workflow_status: workflow_status,
|
1365
|
+
import_status: {
|
1366
|
+
requested: import_requested,
|
1367
|
+
completed: import_completed
|
1368
|
+
},
|
1369
|
+
valuation_summary: {
|
1370
|
+
reasons: valuation_reasons,
|
1371
|
+
notes_present: valuation_notes.present?
|
1372
|
+
}
|
1373
|
+
}
|
1374
|
+
end
|
1375
|
+
end
|
1376
|
+
|
1377
|
+
# app/models/estimate.rb
|
1378
|
+
class Estimate < ApplicationRecord
|
1379
|
+
include NatsWave::NatsPublishable
|
1380
|
+
include NatsWave::Concerns::Mappable
|
1381
|
+
|
1382
|
+
belongs_to :asset
|
1383
|
+
belongs_to :user
|
1384
|
+
belongs_to :estimate_type
|
1385
|
+
|
1386
|
+
# Publish estimate changes for consensus tracking
|
1387
|
+
nats_publishable(
|
1388
|
+
actions: [:create, :update],
|
1389
|
+
skip_attributes: [:notes, :source_data],
|
1390
|
+
subject_prefix: 'estimates',
|
1391
|
+
if: -> { status == 'current' && value_category == 'final' }
|
1392
|
+
)
|
1393
|
+
|
1394
|
+
# Sync from external valuation services
|
1395
|
+
nats_wave_maps_from 'ExternalValuation',
|
1396
|
+
field_mappings: {
|
1397
|
+
'valuation_id' => 'key',
|
1398
|
+
'asset_identifier' => 'asset_id',
|
1399
|
+
'appraiser_id' => 'user_id',
|
1400
|
+
'estimated_value' => 'value',
|
1401
|
+
'valuation_notes' => 'notes',
|
1402
|
+
'valuation_type' => 'estimate_type_id'
|
1403
|
+
},
|
1404
|
+
transformations: {
|
1405
|
+
'value' => ->(value) { (value.to_f * 100).round }, # Convert to cents
|
1406
|
+
'estimate_type_id' => ->(type_name) {
|
1407
|
+
EstimateType.find_by(name: type_name)&.id || 1
|
1408
|
+
},
|
1409
|
+
'status' => ->(_) { 'current' },
|
1410
|
+
'value_category' => ->(_) { 'temporary' },
|
1411
|
+
'version_number' => ->(_) { 1 }
|
1412
|
+
},
|
1413
|
+
unique_fields: [:key],
|
1414
|
+
conditions: {
|
1415
|
+
'source.service' => ->(service) { service != ENV['NATS_SERVICE_NAME'] }
|
1416
|
+
},
|
1417
|
+
subjects: ['valuation_service.estimates.*']
|
1418
|
+
end
|
1419
|
+
|
1420
|
+
# app/models/user.rb
|
1421
|
+
class User < ApplicationRecord
|
1422
|
+
include NatsWave::NatsPublishable
|
1423
|
+
include NatsWave::Concerns::Mappable
|
1424
|
+
|
1425
|
+
has_many :organization_users
|
1426
|
+
has_many :organizations, through: :organization_users
|
1427
|
+
has_many :estimates
|
1428
|
+
has_many :created_assets, class_name: 'Asset', foreign_key: 'creator_id'
|
1429
|
+
has_many :updated_assets, class_name: 'Asset', foreign_key: 'last_updater_id'
|
1430
|
+
|
1431
|
+
# Publish user changes for authentication sync
|
1432
|
+
nats_publishable(
|
1433
|
+
actions: [:create, :update],
|
1434
|
+
skip_attributes: [:api_auth_key, :source_details],
|
1435
|
+
subject_prefix: 'users',
|
1436
|
+
if: -> { active? }
|
1437
|
+
)
|
1438
|
+
|
1439
|
+
# Sync from authentication service
|
1440
|
+
nats_wave_maps_from 'AuthUser',
|
1441
|
+
field_mappings: {
|
1442
|
+
'user_id' => 'key',
|
1443
|
+
'email_address' => 'email',
|
1444
|
+
'full_name' => 'name',
|
1445
|
+
'is_active' => 'active',
|
1446
|
+
'organization_key' => 'current_organization_key'
|
1447
|
+
},
|
1448
|
+
transformations: {
|
1449
|
+
'email' => ->(email) { email&.downcase&.strip },
|
1450
|
+
'name' => ->(name) { name&.strip },
|
1451
|
+
'active' => ->(active) { active == 'true' || active == true }
|
1452
|
+
},
|
1453
|
+
unique_fields: [:key, :email],
|
1454
|
+
conditions: {
|
1455
|
+
'source.service' => ->(service) { service != ENV['NATS_SERVICE_NAME'] }
|
1456
|
+
},
|
1457
|
+
subjects: ['auth_service.users.*']
|
1458
|
+
end
|
1459
|
+
|
1460
|
+
# app/models/organization.rb
|
1461
|
+
class Organization < ApplicationRecord
|
1462
|
+
include NatsWave::NatsPublishable
|
1463
|
+
include NatsWave::Concerns::Mappable
|
1464
|
+
|
1465
|
+
has_many :packages
|
1466
|
+
has_many :organization_users
|
1467
|
+
has_many :users, through: :organization_users
|
1468
|
+
has_many :organization_estimate_types
|
1469
|
+
has_many :estimate_types, through: :organization_estimate_types
|
1470
|
+
has_one :organization_branding
|
1471
|
+
|
1472
|
+
# Publish organization changes for CRM sync
|
1473
|
+
nats_publishable(
|
1474
|
+
actions: [:create, :update],
|
1475
|
+
skip_attributes: [:api_auth_key, :workflow_config],
|
1476
|
+
subject_prefix: 'organizations',
|
1477
|
+
if: -> { active? }
|
1478
|
+
)
|
1479
|
+
|
1480
|
+
# Sync from CRM system
|
1481
|
+
nats_wave_maps_from 'CRMAccount',
|
1482
|
+
field_mappings: {
|
1483
|
+
'account_id' => 'key',
|
1484
|
+
'company_name' => 'name',
|
1485
|
+
'is_active' => 'active',
|
1486
|
+
'primary_contact_name' => 'contact',
|
1487
|
+
'primary_email' => 'email',
|
1488
|
+
'primary_phone' => 'phone',
|
1489
|
+
'account_group' => 'group_name'
|
1490
|
+
},
|
1491
|
+
transformations: {
|
1492
|
+
'name' => ->(name) { name&.strip },
|
1493
|
+
'email' => ->(email) { email&.downcase&.strip },
|
1494
|
+
'active' => ->(active) { active == 'true' || active == true }
|
1495
|
+
},
|
1496
|
+
unique_fields: [:key, :name],
|
1497
|
+
conditions: {
|
1498
|
+
'source.service' => ->(service) { service != ENV['NATS_SERVICE_NAME'] }
|
1499
|
+
},
|
1500
|
+
subjects: ['crm_service.accounts.*']
|
1501
|
+
end
|
1502
|
+
|
1503
|
+
# app/models/image.rb
|
1504
|
+
class Image < ApplicationRecord
|
1505
|
+
include NatsWave::NatsPublishable
|
1506
|
+
|
1507
|
+
belongs_to :asset
|
1508
|
+
|
1509
|
+
# Publish image events for media processing
|
1510
|
+
nats_publishable(
|
1511
|
+
actions: [:create, :update],
|
1512
|
+
skip_attributes: [:source_data, :import_url],
|
1513
|
+
subject_prefix: 'images',
|
1514
|
+
if: -> { !deleted? && sizes.present? }
|
1515
|
+
)
|
1516
|
+
|
1517
|
+
private
|
1518
|
+
|
1519
|
+
def nats_wave_metadata
|
1520
|
+
{
|
1521
|
+
asset_key: asset&.key,
|
1522
|
+
package_key: asset&.package&.key,
|
1523
|
+
image_view: image_view,
|
1524
|
+
custom_view: custom_image_view,
|
1525
|
+
position: position,
|
1526
|
+
file_info: {
|
1527
|
+
original_filename: original_filename,
|
1528
|
+
file_type: file_type,
|
1529
|
+
file_size: file_size,
|
1530
|
+
dimensions: {
|
1531
|
+
width: original_x,
|
1532
|
+
height: original_y
|
1533
|
+
}
|
1534
|
+
},
|
1535
|
+
ims_submission: {
|
1536
|
+
name: ims_submission_name,
|
1537
|
+
key: ims_image_key,
|
1538
|
+
submitted_at: ims_submitted_at,
|
1539
|
+
linked_at: ims_linked_at
|
1540
|
+
}
|
1541
|
+
}
|
1542
|
+
end
|
786
1543
|
end
|
787
1544
|
```
|
788
1545
|
|
789
|
-
###
|
1546
|
+
### Cross-Service Workflow Example
|
790
1547
|
|
791
1548
|
```ruby
|
792
|
-
# app/
|
793
|
-
class
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
1549
|
+
# app/services/asset_valuation_workflow.rb
|
1550
|
+
class AssetValuationWorkflow
|
1551
|
+
def self.process_inventory_asset(message)
|
1552
|
+
# Message from inventory service creates/updates local asset
|
1553
|
+
inventory_data = message['data']
|
1554
|
+
|
1555
|
+
# Find or create asset using field mappings
|
1556
|
+
asset = Asset.find_or_initialize_by(unique_id: inventory_data['inventory_id'])
|
1557
|
+
|
1558
|
+
# Update with transformed data
|
1559
|
+
asset.assign_attributes(
|
1560
|
+
make: inventory_data['manufacturer']&.upcase,
|
1561
|
+
model: inventory_data['model_number']&.titleize,
|
1562
|
+
year: inventory_data['year_manufactured'].to_i,
|
1563
|
+
serial: inventory_data['serial_number'],
|
1564
|
+
vin: inventory_data['vin_number'],
|
1565
|
+
location: inventory_data['location_name'],
|
1566
|
+
estimated_hammer: (inventory_data['estimated_value'].to_f * 100).round
|
1567
|
+
)
|
1568
|
+
|
1569
|
+
asset.save!
|
1570
|
+
|
1571
|
+
# Automatically request valuation if needed
|
1572
|
+
if asset.estimated_hammer.nil? || asset.estimated_hammer.zero?
|
1573
|
+
ValuationRequest.create!(
|
1574
|
+
package: asset.package,
|
1575
|
+
user: User.find_by(email: 'system@valuation.com'),
|
1576
|
+
notes: 'Auto-requested from inventory sync'
|
1577
|
+
)
|
1578
|
+
end
|
1579
|
+
end
|
816
1580
|
|
817
|
-
|
818
|
-
|
819
|
-
|
1581
|
+
def self.process_auction_result(message)
|
1582
|
+
# Message from auction service updates contract prices
|
1583
|
+
auction_data = message['data']
|
1584
|
+
|
1585
|
+
asset = Asset.find_by(lot_number: auction_data['lot_number'])
|
1586
|
+
return unless asset
|
1587
|
+
|
1588
|
+
asset.update!(
|
1589
|
+
pw_contract_price: (auction_data['hammer_price'].to_f * 100).round,
|
1590
|
+
pw_contract_date: Time.parse(auction_data['sale_date']),
|
1591
|
+
pw_auction: auction_data['auction_house']
|
1592
|
+
)
|
1593
|
+
|
1594
|
+
# Create final estimate based on actual sale
|
1595
|
+
Estimate.create!(
|
1596
|
+
asset: asset,
|
1597
|
+
user: User.find_by(email: 'auction@system.com'),
|
1598
|
+
estimate_type: EstimateType.find_by(name: 'Auction Result'),
|
1599
|
+
status: 'current',
|
1600
|
+
value_category: 'final',
|
1601
|
+
value: asset.pw_contract_price,
|
1602
|
+
version_number: 1,
|
1603
|
+
notes: 'Actual auction sale price'
|
1604
|
+
)
|
820
1605
|
end
|
821
1606
|
end
|
822
1607
|
|
823
|
-
#
|
824
|
-
class
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
1608
|
+
# Message handlers
|
1609
|
+
class NatsWave::MessageHandler
|
1610
|
+
def self.process(message)
|
1611
|
+
Rails.logger.info "π Processing #{message['action']} for #{message['model']} from #{message.dig('source', 'service')}"
|
1612
|
+
|
1613
|
+
case message['model']
|
1614
|
+
when 'InventoryAsset'
|
1615
|
+
AssetValuationWorkflow.process_inventory_asset(message)
|
1616
|
+
when 'AuctionLot'
|
1617
|
+
AssetValuationWorkflow.process_auction_result(message)
|
1618
|
+
when 'CRMProject'
|
1619
|
+
CRMProjectHandler.process(message)
|
1620
|
+
when 'AuthUser'
|
1621
|
+
AuthUserHandler.process(message)
|
1622
|
+
else
|
1623
|
+
# Use automatic field mapping for registered models
|
1624
|
+
super
|
1625
|
+
end
|
834
1626
|
end
|
835
1627
|
end
|
836
1628
|
```
|
837
1629
|
|
838
|
-
###
|
1630
|
+
### User & Organization Sync
|
839
1631
|
|
840
1632
|
```ruby
|
841
1633
|
# app/models/user.rb
|
@@ -844,37 +1636,45 @@ class User < ApplicationRecord
|
|
844
1636
|
include NatsWave::Concerns::Mappable
|
845
1637
|
|
846
1638
|
nats_publishable(
|
847
|
-
skip_attributes: [:api_auth_key, :source_details]
|
1639
|
+
skip_attributes: [:api_auth_key, :source_details],
|
1640
|
+
subject_prefix: 'users'
|
848
1641
|
)
|
849
1642
|
|
850
1643
|
# Sync from authentication service
|
851
1644
|
nats_wave_maps_from 'AuthUser',
|
852
|
-
subjects: ['auth.users.*'],
|
853
1645
|
field_mappings: {
|
854
1646
|
'user_id' => 'key',
|
855
1647
|
'email_address' => 'email',
|
856
1648
|
'full_name' => 'name',
|
857
|
-
'is_active' => 'active'
|
1649
|
+
'is_active' => 'active',
|
1650
|
+
'organization_id' => 'current_organization_key'
|
858
1651
|
},
|
859
1652
|
transformations: {
|
860
|
-
'email' => ->(email) { email&.downcase },
|
1653
|
+
'email' => ->(email) { email&.downcase&.strip },
|
1654
|
+
'name' => ->(name) { name&.titleize&.strip },
|
861
1655
|
'active' => ->(active) { active == 'true' || active == true }
|
862
1656
|
},
|
863
|
-
unique_fields: [:key, :email]
|
1657
|
+
unique_fields: [:key, :email],
|
1658
|
+
subjects: ['auth_service.users.*']
|
1659
|
+
|
1660
|
+
# Custom subscription for user activity tracking
|
1661
|
+
nats_wave_subscribes_to 'activity_service.user_actions.*' do |message|
|
1662
|
+
UserActivityTracker.track_activity(message)
|
1663
|
+
end
|
864
1664
|
end
|
865
1665
|
|
866
|
-
# app/models/organization.rb
|
1666
|
+
# app/models/organization.rb
|
867
1667
|
class Organization < ApplicationRecord
|
868
1668
|
include NatsWave::NatsPublishable
|
869
1669
|
include NatsWave::Concerns::Mappable
|
870
1670
|
|
871
1671
|
nats_publishable(
|
872
|
-
skip_attributes: [:api_auth_key, :workflow_config]
|
1672
|
+
skip_attributes: [:api_auth_key, :workflow_config],
|
1673
|
+
subject_prefix: 'organizations'
|
873
1674
|
)
|
874
1675
|
|
875
1676
|
# Sync from CRM system
|
876
1677
|
nats_wave_maps_from 'CRMOrganization',
|
877
|
-
subjects: ['crm.organizations.*'],
|
878
1678
|
field_mappings: {
|
879
1679
|
'organization_id' => 'key',
|
880
1680
|
'company_name' => 'name',
|
@@ -883,53 +1683,18 @@ class Organization < ApplicationRecord
|
|
883
1683
|
'phone_number' => 'phone',
|
884
1684
|
'is_active' => 'active'
|
885
1685
|
},
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
```ruby
|
893
|
-
# app/models/image.rb
|
894
|
-
class Image < ApplicationRecord
|
895
|
-
include NatsWave::NatsPublishable
|
896
|
-
|
897
|
-
nats_publishable(
|
898
|
-
actions: [:create, :update],
|
899
|
-
skip_attributes: [:source_data, :import_url],
|
900
|
-
if: -> { !deleted? && sizes.present? }
|
901
|
-
)
|
902
|
-
|
903
|
-
# Listen for image processing completion
|
904
|
-
nats_wave_subscribes_to 'media.processing.completed.*' do |message|
|
905
|
-
ImageProcessingService.handle_completion(message)
|
906
|
-
end
|
907
|
-
|
908
|
-
# Listen for IMS image submissions
|
909
|
-
nats_wave_subscribes_to 'ims.images.*' do |message|
|
910
|
-
IMSImageProcessor.process_submission(message)
|
911
|
-
end
|
912
|
-
end
|
913
|
-
|
914
|
-
# app/models/video.rb
|
915
|
-
class Video < ApplicationRecord
|
916
|
-
include NatsWave::NatsPublishable
|
917
|
-
|
918
|
-
nats_publishable(
|
919
|
-
actions: [:create, :update],
|
920
|
-
if: -> { !deleted? && url.present? }
|
921
|
-
)
|
922
|
-
|
923
|
-
# Listen for video processing updates
|
924
|
-
nats_wave_subscribes_to 'media.videos.*' do |message|
|
925
|
-
VideoProcessingService.handle_update(message)
|
926
|
-
end
|
1686
|
+
transformations: {
|
1687
|
+
'name' => ->(name) { name&.strip },
|
1688
|
+
'email' => ->(email) { email&.downcase&.strip }
|
1689
|
+
},
|
1690
|
+
unique_fields: [:key, :name],
|
1691
|
+
subjects: ['crm_service.organizations.*']
|
927
1692
|
end
|
928
1693
|
```
|
929
1694
|
|
930
1695
|
## π€ Contributing
|
931
1696
|
|
932
|
-
We welcome contributions! Please see our [Contributing Guide](
|
1697
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
933
1698
|
|
934
1699
|
### Development Setup
|
935
1700
|
|
@@ -948,19 +1713,6 @@ bundle exec rspec
|
|
948
1713
|
bundle exec rubocop
|
949
1714
|
```
|
950
1715
|
|
951
|
-
### Testing
|
952
|
-
|
953
|
-
```bash
|
954
|
-
# Run all tests
|
955
|
-
bundle exec rspec
|
956
|
-
|
957
|
-
# Run specific test files
|
958
|
-
bundle exec rspec spec/nats_wave/publisher_spec.rb
|
959
|
-
|
960
|
-
# Run with coverage
|
961
|
-
COVERAGE=true bundle exec rspec
|
962
|
-
```
|
963
|
-
|
964
1716
|
## π Changelog
|
965
1717
|
|
966
1718
|
See [CHANGELOG.md](CHANGELOG.md) for details about changes in each version.
|
@@ -982,4 +1734,4 @@ our microservices and teams in the asset valuation and auction industry.
|
|
982
1734
|
|
983
1735
|
---
|
984
1736
|
|
985
|
-
**Made with β€οΈ by
|
1737
|
+
**Made with β€οΈ by the Purple Wave Team**
|