nats_wave 1.1.13 β†’ 1.1.15

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.
data/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # 🌊 NatsWave
2
2
 
3
- A comprehensive NATS-based event messaging system for Rails applications that enables seamless communication between
4
- different teams, products, and services in asset management and valuation workflows.
3
+ A comprehensive NATS-based event messaging system for Rails applications that enables seamless communication between different teams, products, and services in asset management and valuation workflows.
5
4
 
6
5
  [![Gem Version](https://badge.fury.io/rb/nats_wave.svg)](https://badge.fury.io/rb/nats_wave)
7
6
  [![Build Status](https://github.com/PurpleWave/nats_wave/workflows/CI/badge.svg)](https://github.com/PurpleWave/nats_wave/actions)
@@ -31,18 +30,17 @@ different teams, products, and services in asset management and valuation workfl
31
30
 
32
31
  ## ✨ Features
33
32
 
34
- - **πŸš€ Model-Based Configuration** - Configure subscriptions and mappings directly in your models
35
- - **πŸ”„ Automatic Event Publishing** - ActiveRecord integration publishes model changes automatically
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
33
+ - **πŸš€ Streamlined Model Configuration** - Simple, declarative model-based subscriptions and publishing
34
+ - **πŸ“€ Smart Publishing** - Automatic event publishing with custom field mappings and transformations
35
+ - **πŸ“₯ Intelligent Subscriptions** - Auto-sync with custom handlers for complex processing
36
+ - **πŸ—ΊοΈ Advanced Data Mapping** - Transform data between different schemas with field mappings and transformations
37
+ - **πŸ”€ Cross-Service Synchronization** - Seamlessly sync data between services with different models and schemas
38
+ - **🎯 Custom Message Handlers** - Optional custom processing after automatic sync operations
40
39
  - **πŸ“Š Datadog Integration** - Built-in metrics and monitoring support
41
40
  - **πŸ›‘οΈ Robust Error Handling** - Dead letter queues, retries, and graceful failure handling
42
41
  - **πŸ”§ Middleware System** - Authentication, validation, and logging middleware
43
42
  - **⚑ High Performance** - Connection pooling, async publishing, and batch operations
44
- - **πŸ₯ Health Monitoring** - Built-in health checks and status endpoints
45
- - **πŸ“ˆ Auto-Discovery** - Automatic registration of model subscriptions via ModelRegistry
43
+ - **πŸ”„ Auto-Reconnection** - Automatic reconnection with exponential backoff and health monitoring
46
44
  - **πŸ§ͺ Test-Friendly** - Comprehensive test helpers and mocking support
47
45
 
48
46
  ## πŸ“¦ Installation
@@ -51,6 +49,7 @@ Add this line to your Rails application's Gemfile:
51
49
 
52
50
  ```ruby
53
51
  gem 'nats_wave'
52
+ gem 'nats', '~> 0.11' # NATS Ruby client
54
53
 
55
54
  # Optional: For enhanced monitoring
56
55
  gem 'dogstatsd-ruby' # For Datadog metrics
@@ -66,7 +65,6 @@ Generate the configuration files:
66
65
 
67
66
  ```bash
68
67
  rails generate nats_wave:install
69
- rails db:migrate
70
68
  ```
71
69
 
72
70
  ## πŸš€ Quick Start
@@ -76,17 +74,16 @@ rails db:migrate
76
74
  ```ruby
77
75
  # config/initializers/nats_wave.rb
78
76
  NatsWave.configure do |config|
79
- config.nats_url = ENV.fetch('NATS_URL', 'nats://localhost:4222')
80
- config.service_name = 'valuation_service'
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"
77
+ config.nats_url = ENV.fetch('NATS_URL', 'nats://localhost:4222')
78
+ config.service_name = ENV.fetch('NATS_SERVICE_NAME', 'valuation_service')
79
+
80
+ # Publishing
81
+ config.publishing_enabled = !Rails.env.test?
82
+ config.async_publishing = Rails.env.production?
83
+
84
+ # Subscription
85
+ config.subscription_enabled = !Rails.env.test?
86
+ config.queue_group = "#{config.service_name}_consumers"
90
87
  end
91
88
  ```
92
89
 
@@ -95,78 +92,86 @@ end
95
92
  ```ruby
96
93
  # app/models/asset.rb
97
94
  class Asset < ApplicationRecord
98
- include NatsWave::NatsPublishable
99
- include NatsWave::Concerns::Mappable
100
-
101
- # Automatically publish asset events
102
- nats_publishable(
103
- actions: [:create, :update, :destroy],
104
- skip_attributes: [:notes, :edge_data],
105
- include_associations: [:package, :estimates],
106
- subject_prefix: 'assets'
107
- )
108
-
109
- # Map from external inventory system
110
- nats_wave_maps_from 'InventoryItem',
111
- field_mappings: {
112
- 'item_id' => 'unique_id',
113
- 'description' => 'description',
114
- 'manufacturer' => 'make',
115
- 'model_name' => 'model',
116
- 'year_manufactured' => 'year',
117
- 'serial_number' => 'serial',
118
- 'vin_number' => 'vin'
119
- },
120
- transformations: {
121
- 'make' => ->(make) { make&.upcase&.strip },
122
- 'year' => ->(year) { year.to_i if year.present? },
123
- 'estimated_hammer' => ->(value) { value.to_f.round(2) }
124
- },
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)
95
+ include NatsWave::Concerns::Mappable
96
+
97
+ # Subscribe to external inventory system - auto-sync + custom handler
98
+ nats_wave_subscribes_to "inventory_service.items.*",
99
+ field_mappings: {
100
+ 'item_id' => 'unique_id',
101
+ 'description' => 'description',
102
+ 'manufacturer' => 'make',
103
+ 'model_name' => 'model',
104
+ 'year_manufactured' => 'year',
105
+ 'serial_number' => 'serial'
106
+ },
107
+ transformations: {
108
+ 'make' => ->(make) { make&.upcase&.strip },
109
+ 'year' => ->(year) { year.to_i if year.present? }
110
+ },
111
+ unique_fields: [:unique_id],
112
+ skip_fields: ['internal_notes'] do |model_name, action, processed_data, message|
113
+ # Custom handler runs AFTER auto-sync
114
+ Rails.logger.info "πŸ“¨ Processed #{action} for #{model_name}: #{processed_data[:unique_id]}"
115
+
116
+ # Custom logic here
117
+ if action == 'create' && processed_data[:estimated_value].blank?
118
+ ValuationRequestJob.perform_later(processed_data[:unique_id])
119
+ end
120
+ end
121
+
122
+ # Publish asset changes to other services
123
+ nats_wave_publishes_to "#{ENV['NATS_SERVICE_NAME']}.assets.{action}",
124
+ field_mappings: {
125
+ 'unique_id' => 'asset_id',
126
+ 'make' => 'manufacturer',
127
+ 'model' => 'model_name'
128
+ },
129
+ transformations: {
130
+ 'created_at' => lambda(&:iso8601),
131
+ 'updated_at' => lambda(&:iso8601),
132
+ 'estimated_hammer' => ->(cents) { cents.to_f / 100 }
133
+ },
134
+ skip_fields: %w[notes edge_data source_data],
135
+ actions: %i[create update] do |model_name, action, published_data, record|
136
+ # Custom handler runs AFTER publishing
137
+ Rails.logger.info "πŸ“€ Published #{action} for #{model_name}: #{record.id}"
138
+
139
+ # Send notifications, update caches, etc.
140
+ if action == 'update' && record.valuation_complete?
141
+ NotificationService.notify_valuation_complete(record)
142
+ end
132
143
  end
133
144
  end
134
145
  ```
135
146
 
136
147
  ### 3. Start the Subscriber
137
148
 
138
- The subscriber automatically starts with Rails and processes registered model subscriptions:
149
+ The subscriber automatically starts with Rails:
139
150
 
140
151
  ```bash
141
- # Development - auto-starts with Rails
152
+ # Development - auto-starts with Rails server
142
153
  rails server
143
154
 
144
155
  # 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
156
+ # I, [2025-01-21T12:10:35.910694 #29] INFO -- : Starting NATS subscriber for valuation_service
157
+ # I, [2025-01-21T12:10:35.911316 #29] INFO -- : βœ… Successfully subscribed to inventory_service.items.*
158
+ # I, [2025-01-21T12:10:35.912749 #29] INFO -- : Started 3 subscriptions from Model Registry
148
159
 
149
- # Production - run dedicated subscriber process
150
- rails nats_wave:start
160
+ # Production - run dedicated subscriber process if needed
161
+ bundle exec rake nats_wave:start
151
162
  ```
152
163
 
153
164
  ### 4. Test the Integration
154
165
 
155
166
  ```bash
156
167
  # Test connectivity
157
- rails nats_wave:health
158
-
159
- # Show model subscriptions
160
- rails nats_wave:model_subscriptions
168
+ rails runner "puts NatsWave.health_check"
161
169
 
162
170
  # Publish a test message
163
171
  rails console
164
- > NatsWave.publish(
165
- subject: 'assets.create',
166
- model: 'Asset',
167
- action: 'create',
168
- data: { id: 123, make: 'Caterpillar', model: '320D' }
169
- )
172
+ > asset = Asset.first
173
+ > asset.update!(make: 'Updated Manufacturer')
174
+ # This automatically publishes via nats_wave_publishes_to
170
175
  ```
171
176
 
172
177
  ## βš™οΈ Configuration
@@ -176,13 +181,10 @@ rails console
176
181
  ```bash
177
182
  # Required
178
183
  NATS_URL=nats://your-nats-server:4222
179
-
180
- # Optional - defaults to Rails application name
181
184
  NATS_SERVICE_NAME=valuation_service
182
185
 
183
186
  # Optional Authentication & Security
184
187
  NATS_AUTH_SECRET=your-secret-key
185
- SCHEMA_REGISTRY_URL=http://schema-registry:8081
186
188
 
187
189
  # Optional Monitoring
188
190
  DD_AGENT_HOST=localhost # For Datadog metrics
@@ -191,484 +193,251 @@ DD_AGENT_PORT=8125
191
193
 
192
194
  ### Complete Configuration Setup
193
195
 
194
- Create or update `config/initializers/nats_wave.rb`:
195
-
196
196
  ```ruby
197
- # frozen_string_literal: true
198
-
197
+ # config/initializers/nats_wave.rb
199
198
  begin
200
- Rails.logger.info "Initializing NATS Wave with URL: #{ENV['NATS_URL']}"
199
+ Rails.logger.info "Initializing NATS Wave..."
200
+
201
+ NatsWave.configure do |config|
202
+ # === CORE CONNECTION SETTINGS ===
203
+ config.nats_url = ENV.fetch('NATS_URL', 'nats://localhost:4222')
204
+ config.service_name = ENV.fetch('NATS_SERVICE_NAME',
205
+ Rails.application.class.name.deconstantize.underscore)
206
+ config.connection_pool_size = 10
207
+ config.reconnect_attempts = 3
201
208
 
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'
209
+ # === PUBLISHING CONFIGURATION ===
210
+ config.publishing_enabled = !Rails.env.test?
211
+ config.async_publishing = Rails.env.production?
212
+
213
+ # === SUBSCRIPTION CONFIGURATION ===
214
+ config.subscription_enabled = !Rails.env.test?
215
+ config.queue_group = "#{config.service_name}_consumers"
216
+
217
+ # === ERROR HANDLING ===
218
+ config.max_retries = 3
219
+ config.retry_delay = 5
220
+ config.dead_letter_queue = "failed_messages"
221
+
222
+ # === AUTHENTICATION (Optional) ===
223
+ if ENV['NATS_AUTH_SECRET'].present?
224
+ config.middleware_authentication_enabled = true
225
+ config.auth_secret_key = ENV['NATS_AUTH_SECRET']
242
226
  end
243
227
 
244
- Rails.logger.info "NATS Wave configuration completed successfully"
228
+ # === LOGGING ===
229
+ config.middleware_logging_enabled = true
230
+ config.log_level = Rails.env.production? ? 'info' : 'debug'
231
+ end
232
+
233
+ Rails.logger.info "βœ… NATS Wave configuration completed successfully"
245
234
  rescue => e
246
- Rails.logger.error "Failed to configure NATS Wave: #{e.message}"
247
- Rails.logger.error e.backtrace.join("\n")
248
-
249
- # Optional: Re-raise in development to catch configuration issues early
250
- raise e if Rails.env.development?
235
+ Rails.logger.error "❌ Failed to configure NATS Wave: #{e.message}"
236
+ raise e if Rails.env.development?
251
237
  end
252
238
 
253
239
  # === AUTOMATIC SUBSCRIBER STARTUP ===
254
240
  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
241
+ Thread.new do
242
+ sleep 2 # Give Rails time to boot and load all models
243
+ begin
244
+ NatsWave.start_subscriber
245
+ Rails.logger.info "βœ… NatsWave subscriber started successfully"
246
+
247
+ # Log subscription statistics
248
+ stats = NatsWave::ModelRegistry.subscription_stats
249
+ Rails.logger.info "πŸ“Š Active subscriptions: #{stats[:total_subscriptions]} across #{stats[:models_with_subscriptions]} models"
286
250
 
287
- NatsWave::Metrics.configure_datadog(
288
- namespace: "#{Rails.application.class.name.deconstantize.underscore}.nats_wave",
289
- tags: {
290
- service: NatsWave.configuration.service_name,
291
- environment: Rails.env,
292
- version: app_version
293
- }
294
- )
295
- Rails.logger.info "πŸ“ˆ Datadog metrics integration enabled"
251
+ rescue StandardError => e
252
+ Rails.logger.error "❌ Failed to start NatsWave subscriber: #{e.message}"
253
+
254
+ # Optional: Alert monitoring systems
255
+ Sentry.capture_exception(e) if defined?(Sentry)
256
+ end
257
+ end
296
258
  end
297
259
 
298
- # === GRACEFUL SHUTDOWN (Optional) ===
260
+ # === GRACEFUL SHUTDOWN ===
299
261
  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}"
262
+ begin
263
+ if defined?(NatsWave) && NatsWave.client
264
+ Rails.logger.info "πŸ›‘ Shutting down NatsWave connections..."
265
+ NatsWave.client.disconnect!
266
+ Rails.logger.info "βœ… NatsWave shutdown complete"
308
267
  end
268
+ rescue => e
269
+ Rails.logger.error "Error during NatsWave shutdown: #{e.message}"
270
+ end
309
271
  end
310
272
  ```
311
273
 
312
- ### Environment-Specific Configuration
274
+ ## πŸ—οΈ Model-Based Architecture
313
275
 
314
- #### Development Environment
276
+ NatsWave uses a streamlined model-centric approach where each model defines its own subscriptions and publishing through the **ModelRegistry** system.
315
277
 
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
- ```
278
+ ### Core Concepts
333
279
 
334
- #### Production Environment
280
+ - **Auto-Sync**: Automatic field mapping and database synchronization
281
+ - **Custom Handlers**: Optional custom processing after auto-sync or publishing
282
+ - **Field Mappings**: Transform field names between external and local models
283
+ - **Transformations**: Apply data transformations during sync/publish
284
+ - **Unique Fields**: Define how to find existing records for updates
335
285
 
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
- ```
286
+ ## πŸ“€ Publishing Events
353
287
 
354
- #### Test Environment
288
+ ### Automatic Publishing with Custom Processing
355
289
 
356
290
  ```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
291
+ class Package < ApplicationRecord
292
+ include NatsWave::Concerns::Mappable
293
+
294
+ # Publish package events with field mapping and custom processing
295
+ nats_wave_publishes_to "#{ENV['NATS_SERVICE_NAME']}.packages.{action}",
296
+ field_mappings: {
297
+ 'id' => 'package_id',
298
+ 'key' => 'package_key',
299
+ 'owner_name' => 'client_name',
300
+ 'workflow_status' => 'status'
301
+ },
302
+ transformations: {
303
+ 'created_at' => lambda(&:iso8601),
304
+ 'updated_at' => lambda(&:iso8601),
305
+ 'workflow_status' => ->(status) { status&.upcase }
306
+ },
307
+ skip_fields: %w[notes source_data edge_data],
308
+ actions: %i[create update] do |model_name, action, published_data, record|
309
+ # Custom processing AFTER publishing
310
+ Rails.logger.info "πŸ“€ Published #{action} for #{model_name}: #{record.key}"
311
+
312
+ # Notify related services
313
+ if action == 'update' && record.workflow_status_changed?
314
+ WorkflowNotificationService.notify_status_change(record)
315
+ end
316
+
317
+ # Update search indexes
318
+ SearchIndexUpdateJob.perform_later(record.id, 'Package') if action == 'update'
370
319
  end
371
320
  end
372
321
  ```
373
322
 
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:
323
+ ### Dynamic Subject Generation
395
324
 
396
325
  ```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>
326
+ # Use {action} placeholder for dynamic subjects
327
+ nats_wave_publishes_to "#{ENV['NATS_SERVICE_NAME']}.assets.{action}"
328
+ # Creates: valuation_service.assets.create, valuation_service.assets.update, etc.
329
+
330
+ # Multiple subject patterns
331
+ nats_wave_publishes_to [
332
+ "#{ENV['NATS_SERVICE_NAME']}.assets.{action}",
333
+ "global.assets.{action}",
334
+ "audit.asset_changes"
335
+ ]
476
336
  ```
477
337
 
478
- ## πŸ—οΈ Model-Based Architecture
479
-
480
- NatsWave uses a model-centric approach where each model defines its own subscriptions and mappings through the *
481
- *ModelRegistry** system.
482
-
483
- ### Publishing Events
338
+ ### Conditional Publishing
484
339
 
485
340
  ```ruby
486
-
487
- class Package < ApplicationRecord
488
- include NatsWave::NatsPublishable
489
-
490
- nats_publishable(
491
- actions: [:create, :update], # Which actions to publish
492
- skip_attributes: [:notes, :valuation_notes], # Attributes to exclude
493
- include_associations: [:organization, :assets, :contact_user], # Include related data
494
- if: -> { workflow_status == 'active' }, # Conditional publishing
495
- unless: -> { is_shared? }, # Skip conditions
496
- subject_prefix: 'packages' # Custom subject prefix
497
- )
498
-
499
- private
500
-
501
- # Add custom metadata to published events
502
- def nats_wave_metadata
503
- {
504
- organization_name: organization&.name,
505
- asset_count: assets.count,
506
- total_estimated_value: assets.sum(&:estimated_hammer),
507
- valuation_complete: valuation_complete?
508
- }
341
+ nats_wave_publishes_to "inventory.assets.{action}",
342
+ actions: %i[create update],
343
+ # Add conditions in the custom handler
344
+ skip_fields: %w[internal_notes] do |model_name, action, published_data, record|
345
+ # Only process if certain conditions are met
346
+ if record.asset_status == 'active' && !record.is_internal?
347
+ Rails.logger.info "πŸ“€ Published #{action} for active asset: #{record.id}"
348
+
349
+ # Custom processing here
350
+ ExternalInventoryService.sync_asset(record)
351
+ else
352
+ Rails.logger.debug "⏭️ Skipped publishing for #{record.id} (inactive or internal)"
509
353
  end
510
- end
354
+ end
511
355
  ```
512
356
 
513
- ### Subscribing to External Events
357
+ ## πŸ“₯ Subscribing to Events
514
358
 
515
- ```ruby
359
+ ### Auto-Sync with Custom Processing
516
360
 
361
+ ```ruby
517
362
  class Asset < ApplicationRecord
518
- include NatsWave::Concerns::Mappable
519
-
520
- # Map from inventory management system
521
- nats_wave_maps_from 'InventoryAsset',
522
- field_mappings: {
523
- 'inventory_id' => 'unique_id',
524
- 'asset_description' => 'description',
525
- 'manufacturer' => 'make',
526
- 'model_number' => 'model',
527
- 'year_built' => 'year',
528
- 'serial_num' => 'serial',
529
- 'vin_code' => 'vin',
530
- 'location_code' => 'location'
531
- },
532
- transformations: {
533
- 'make' => ->(make) { make&.upcase&.strip },
534
- 'model' => ->(model) { model&.titleize&.strip },
535
- 'year' => ->(year) { year.to_i if year.present? },
536
- 'estimated_hammer' => ->(value) { value.to_f.round(2) }
537
- },
538
- unique_fields: [:unique_id, :vin, :serial],
539
- sync_strategy: :upsert,
540
- conditions: {
541
- 'source.service' => ->(service) { service != ENV['NATS_SERVICE_NAME'] }, # Do not process event messages on your server
542
- 'status' => 'available',
543
- 'condition' => ['good', 'fair', 'excellent']
544
- },
545
- subjects: ['inventory_service.assets.*', 'warehouse_service.assets.*']
546
-
547
- # Map from auction system (different external model name)
548
- nats_wave_maps_from 'AuctionItem',
549
- field_mappings: {
550
- 'lot_number' => 'lot_number',
551
- 'hammer_price' => 'pw_contract_price',
552
- 'sale_date' => 'pw_contract_date'
553
- },
554
- conditions: {
555
- 'source.service' => ->(service) { service != ENV['NATS_SERVICE_NAME'] }, # Do not process event messages on your server
556
- 'auction_status' => 'sold'
557
- },
558
- subjects: ['auction_service.items.*']
559
-
560
- # Custom subscription with handler
561
- nats_wave_subscribes_to "market_service.prices.*" do |message|
562
- MarketPriceService.update_asset_estimates(message)
363
+ include NatsWave::Concerns::Mappable
364
+
365
+ # Subscribe to inventory system - auto-sync + custom processing
366
+ nats_wave_subscribes_to "inventory_service.items.*", "warehouse_service.assets.*",
367
+ field_mappings: {
368
+ 'item_id' => 'unique_id',
369
+ 'description' => 'description',
370
+ 'manufacturer' => 'make',
371
+ 'model_name' => 'model',
372
+ 'year_manufactured' => 'year',
373
+ 'serial_number' => 'serial',
374
+ 'vin_number' => 'vin',
375
+ 'estimated_value' => 'estimated_hammer'
376
+ },
377
+ transformations: {
378
+ 'make' => ->(make) { make&.upcase&.strip },
379
+ 'model' => ->(model) { model&.titleize&.strip },
380
+ 'year' => ->(year) { year.to_i if year.present? },
381
+ 'estimated_hammer' => ->(value) { (value.to_f * 100).round }, # Convert to cents
382
+ 'asset_status' => ->(status) { status&.downcase || 'active' }
383
+ },
384
+ unique_fields: [:unique_id, :vin, :serial],
385
+ skip_fields: ['internal_inventory_code', 'warehouse_location'] do |model_name, action, processed_data, message|
386
+ # Custom handler runs AFTER auto-sync
387
+ Rails.logger.info "πŸ“¨ Auto-synced #{action} for #{model_name}: #{processed_data[:unique_id]}"
388
+
389
+ case action
390
+ when 'create'
391
+ # Request valuation for new assets
392
+ if processed_data[:estimated_hammer].blank?
393
+ ValuationRequestJob.perform_later(processed_data[:unique_id])
394
+ end
395
+
396
+ # Download images
397
+ if message.dig('data', 'image_urls').present?
398
+ ImageDownloadJob.perform_later(processed_data[:unique_id], message['data']['image_urls'])
399
+ end
400
+
401
+ when 'update'
402
+ # Check for significant changes
403
+ if processed_data[:estimated_hammer] != message.dig('previous_data', 'estimated_value')
404
+ AssetValueChangeNotificationJob.perform_later(processed_data[:unique_id])
405
+ end
406
+
407
+ when 'delete', 'destroy'
408
+ # Clean up related data
409
+ CleanupAssetDataJob.perform_later(processed_data[:unique_id])
410
+ end
411
+
412
+ # Update search index
413
+ SearchIndexUpdateJob.perform_later(processed_data[:unique_id], 'Asset')
563
414
  end
564
415
  end
565
416
  ```
566
417
 
567
- ## πŸ“€ Publishing Events
568
-
569
- ### Automatic Publishing
570
-
571
- Events are automatically published when you include `NatsPublishable`:
572
-
573
- ```ruby
574
- # This automatically publishes to "valuation_service.events.assets.create"
575
- asset = Asset.create!(
576
- make: "Caterpillar",
577
- model: "320D",
578
- year: 2018,
579
- serial: "ABC123456",
580
- description: "Excavator - Track Type"
581
- )
582
-
583
- # This publishes to "valuation_service.events.assets.update"
584
- asset.update!(estimated_hammer: 125000)
585
-
586
- # This publishes to "valuation_service.events.assets.destroy"
587
- asset.destroy!
588
- ```
589
-
590
- ### Manual Publishing
418
+ ### Pure Custom Handlers (No Auto-Sync)
591
419
 
592
420
  ```ruby
593
- # Publish custom valuation events
594
- NatsWave.publish(
595
- subject: 'valuation.completed',
596
- model: 'ValuationSession',
597
- action: 'complete',
598
- data: {
599
- package_id: package.id,
600
- asset_count: package.assets.count,
601
- total_value: package.total_estimated_value,
602
- appraiser_id: current_user.id
603
- },
604
- metadata: {
605
- organization_id: package.organization.id,
606
- completion_time: Time.current,
607
- valuation_method: 'comparative_analysis'
608
- }
609
- )
610
-
611
- # Batch publishing for performance
612
- events = assets.map do |asset|
613
- {
614
- subject: 'asset.valued',
615
- model: 'Asset',
616
- action: 'update',
617
- data: asset.attributes.except('notes', 'edge_data')
618
- }
421
+ class User < ApplicationRecord
422
+ include NatsWave::Concerns::Mappable
423
+
424
+ # Custom handler only - no auto-sync
425
+ nats_wave_subscribes_to "auth_service.user_sessions.*" do |model_name, action, processed_data, message|
426
+ # Pure custom processing - no database sync
427
+ case action
428
+ when 'login'
429
+ UserActivityTracker.track_login(message['data']['user_id'], message['data']['ip_address'])
430
+ Rails.cache.write("user_session:#{message['data']['user_id']}", message['data']['session_token'], expires_in: 1.hour)
431
+
432
+ when 'logout'
433
+ UserActivityTracker.track_logout(message['data']['user_id'])
434
+ Rails.cache.delete("user_session:#{message['data']['user_id']}")
435
+
436
+ when 'password_reset'
437
+ UserMailer.password_reset_notification(message['data']['user_id']).deliver_later
438
+ end
439
+ end
619
440
  end
620
-
621
- NatsWave.client.publish_batch(events)
622
- ```
623
-
624
- ## πŸ“₯ Subscribing to Events
625
-
626
- ### Automatic Model Syncing with Field Mapping
627
-
628
- When you configure `nats_wave_maps_from`, NatsWave automatically:
629
-
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
635
-
636
- ```ruby
637
- # Example: Incoming message from inventory system
638
- {
639
- "subject": "inventory_service.assets.update",
640
- "model": "InventoryAsset",
641
- "action": "update",
642
- "data": {
643
- "inventory_id": "INV-2024-001",
644
- "asset_description": "2020 Ford F-150 Pickup Truck",
645
- "manufacturer": "ford",
646
- "model_number": "F-150",
647
- "year_built": "2020",
648
- "serial_num": "1FTFW1ET5LFA12345",
649
- "vin_code": "1FTFW1ET5LFA12345",
650
- "estimated_value": 35000.00,
651
- "status": "available"
652
- },
653
- "source": {
654
- "service": "inventory_service",
655
- "instance_id": "inv-worker-1"
656
- }
657
- }
658
-
659
- # NatsWave automatically transforms this to:
660
- {
661
- "unique_id": "INV-2024-001",
662
- "description": "2020 Ford F-150 Pickup Truck",
663
- "make": "FORD", # Uppercased via transformation
664
- "model": "F-150",
665
- "year": 2020, # Converted to integer
666
- "serial": "1FTFW1ET5LFA12345",
667
- "vin": "1FTFW1ET5LFA12345",
668
- "estimated_hammer": 35000.00
669
- }
670
-
671
- # And creates/updates the local Asset record using unique_fields [:unique_id, :vin, :serial]
672
441
  ```
673
442
 
674
443
  ## πŸ—ΊοΈ Data Mapping & Transformation
@@ -676,246 +445,255 @@ When you configure `nats_wave_maps_from`, NatsWave automatically:
676
445
  ### Complete Field Mapping Options
677
446
 
678
447
  ```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
- ]
448
+ nats_wave_subscribes_to "external_service.models.*",
449
+ # === FIELD MAPPINGS ===
450
+ field_mappings: {
451
+ 'external_field' => 'local_field',
452
+ 'their_id' => 'external_id',
453
+ 'their_name' => 'title',
454
+ 'created_timestamp' => 'created_at',
455
+ 'nested.field.value' => 'flattened_value' # Flatten nested fields
456
+ },
457
+
458
+ # === DATA TRANSFORMATIONS ===
459
+ transformations: {
460
+ 'created_at' => :parse_timestamp, # Method call
461
+ 'make' => ->(value) { value&.upcase&.strip }, # Lambda
462
+ 'year' => ->(year) { year.to_i if year.present? },
463
+ 'price' => :convert_to_cents, # Custom method
464
+ 'status' => ->(val, record) { "#{record['prefix']}_#{val}" }, # With record access
465
+ 'tags' => ->(tags) { tags.is_a?(Array) ? tags.join(',') : tags } # Array handling
466
+ },
467
+
468
+ # === UNIQUE IDENTIFICATION ===
469
+ unique_fields: [:external_id, :vin, :serial], # Fields to find existing records
470
+
471
+ # === SKIP FIELDS ===
472
+ skip_fields: [:internal_notes, :secret_key] # Fields to ignore completely
721
473
  ```
722
474
 
723
475
  ### Advanced Transformations
724
476
 
725
477
  ```ruby
726
-
727
478
  class Asset < ApplicationRecord
728
- include NatsWave::Concerns::Mappable
729
-
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+/, '')
764
- end
765
-
766
- def self.parse_timestamp(timestamp)
767
- return nil if timestamp.blank?
768
- Time.parse(timestamp.to_s)
769
- rescue ArgumentError
479
+ include NatsWave::Concerns::Mappable
480
+
481
+ nats_wave_subscribes_to "external_assets.*",
482
+ transformations: {
483
+ # Simple lambda transformations
484
+ 'estimated_hammer' => ->(value) { value.to_f.round(2) },
485
+ 'make' => ->(make) { make&.upcase&.strip },
486
+
487
+ # Method transformations
488
+ 'vin' => :normalize_vin,
489
+ 'serial' => :normalize_serial,
490
+
491
+ # Complex transformations with full record access
492
+ 'description' => ->(value, record) {
493
+ parts = [record['year'], record['make'], record['model'], value].compact
494
+ parts.join(' ').strip
495
+ },
496
+
497
+ # Conditional transformations
498
+ 'condition' => ->(condition) {
499
+ case condition&.downcase
500
+ when 'excellent', 'like new' then 'excellent'
501
+ when 'good', 'fair' then 'good'
502
+ when 'poor', 'needs repair' then 'poor'
503
+ else 'unknown'
504
+ end
505
+ },
506
+
507
+ # Date/time parsing
508
+ 'manufactured_date' => ->(date_str) {
509
+ return nil if date_str.blank?
510
+ Date.parse(date_str.to_s)
511
+ rescue ArgumentError
770
512
  nil
513
+ }
514
+ } do |model_name, action, processed_data, message|
515
+ Rails.logger.info "πŸ”„ Transformed and synced #{model_name}: #{processed_data[:unique_id]}"
771
516
  end
772
-
773
- def self.convert_to_cents(dollars)
774
- (dollars.to_f * 100).round
775
- end
517
+
518
+ private
519
+
520
+ def self.normalize_vin(vin)
521
+ vin&.upcase&.gsub(/[^A-Z0-9]/, '')&.strip
522
+ end
523
+
524
+ def self.normalize_serial(serial)
525
+ serial&.upcase&.strip&.gsub(/\s+/, '')
526
+ end
527
+
528
+ def self.parse_timestamp(timestamp)
529
+ return nil if timestamp.blank?
530
+ Time.parse(timestamp.to_s)
531
+ rescue ArgumentError
532
+ nil
533
+ end
534
+
535
+ def self.convert_to_cents(dollars)
536
+ (dollars.to_f * 100).round
537
+ end
776
538
  end
777
539
  ```
778
540
 
779
541
  ## πŸ”€ Cross-Service Integration
780
542
 
781
- ### Handling Different Services with Same Model Names
543
+ ### Multiple Services with Same Model Names
782
544
 
783
545
  ```ruby
784
-
785
546
  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
547
+ include NatsWave::Concerns::Mappable
548
+
549
+ # Handle packages from different services
550
+ nats_wave_subscribes_to "inventory_service.packages.*",
551
+ field_mappings: {
552
+ 'sku' => 'product_code',
553
+ 'inventory_status' => 'status'
554
+ },
555
+ unique_fields: [:product_code] do |model_name, action, processed_data, message|
556
+ source_service = message.dig('source', 'service')
557
+ Rails.logger.info "πŸ“¦ Inventory package #{action}: #{processed_data[:product_code]} from #{source_service}"
558
+
559
+ # Inventory-specific processing
560
+ InventoryPackageProcessor.process(processed_data, action)
561
+ end
562
+
563
+ nats_wave_subscribes_to "order_service.packages.*",
564
+ field_mappings: {
565
+ 'order_item_id' => 'external_order_id',
566
+ 'item_name' => 'name'
567
+ },
568
+ unique_fields: [:external_order_id] do |model_name, action, processed_data, message|
569
+ source_service = message.dig('source', 'service')
570
+ Rails.logger.info "πŸ“¦ Order package #{action}: #{processed_data[:external_order_id]} from #{source_service}"
571
+
572
+ # Order-specific processing
573
+ OrderPackageProcessor.process(processed_data, action)
824
574
  end
825
575
  end
826
576
  ```
827
577
 
828
- ### Using Different External Model Names
578
+ ### Different External Model Names
829
579
 
830
580
  ```ruby
831
-
832
581
  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]
582
+ include NatsWave::Concerns::Mappable
583
+
584
+ # Map different external model names to local Asset model
585
+ nats_wave_subscribes_to "inventory_service.inventory_items.*",
586
+ field_mappings: {
587
+ 'sku' => 'product_code',
588
+ 'stock_level' => 'quantity'
589
+ },
590
+ unique_fields: [:product_code] do |model_name, action, processed_data, message|
591
+ Rails.logger.info "πŸ“¦ Inventory item -> Asset: #{processed_data[:product_code]}"
592
+ end
593
+
594
+ nats_wave_subscribes_to "order_service.order_items.*",
595
+ field_mappings: {
596
+ 'item_id' => 'external_order_id',
597
+ 'item_name' => 'name'
598
+ },
599
+ unique_fields: [:external_order_id] do |model_name, action, processed_data, message|
600
+ Rails.logger.info "πŸ›’ Order item -> Asset: #{processed_data[:external_order_id]}"
601
+ end
602
+
603
+ nats_wave_subscribes_to "fulfillment_service.shipment_items.*",
604
+ field_mappings: {
605
+ 'tracking_code' => 'tracking_number'
606
+ },
607
+ unique_fields: [:tracking_number] do |model_name, action, processed_data, message|
608
+ Rails.logger.info "🚚 Shipment item -> Asset: #{processed_data[:tracking_number]}"
609
+ end
850
610
  end
851
611
  ```
852
612
 
853
613
  ## 🎯 Message Processing & Handlers
854
614
 
855
- ### Automatic Message Processing
615
+ ### Auto-Sync Process Flow
616
+
617
+ When a message is received, NatsWave automatically:
856
618
 
857
- NatsWave includes a comprehensive `MessageHandler` that automatically processes mapped messages:
619
+ 1. **Parses the message** and extracts data
620
+ 2. **Applies field mappings** to transform field names
621
+ 3. **Runs transformations** on the mapped data
622
+ 4. **Finds existing records** using unique_fields
623
+ 5. **Performs database operation** (create/update/delete)
624
+ 6. **Calls custom handler** with the processed data (if provided)
858
625
 
859
626
  ```ruby
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
627
+ # Example flow for incoming message:
628
+ {
629
+ "subject": "inventory_service.items.update",
630
+ "model": "InventoryItem",
631
+ "action": "update",
632
+ "data": {
633
+ "item_id": "INV-2024-001",
634
+ "manufacturer": "ford",
635
+ "estimated_value": 35000.00
636
+ }
637
+ }
638
+
639
+ # NatsWave automatically:
640
+ # 1. Maps 'item_id' -> 'unique_id', 'manufacturer' -> 'make'
641
+ # 2. Transforms 'make' to 'FORD' (uppercase)
642
+ # 3. Finds Asset.find_by(unique_id: 'INV-2024-001')
643
+ # 4. Updates the asset with transformed data
644
+ # 5. Calls your custom handler with processed data
892
645
  ```
893
646
 
894
- ### Custom Message Handlers
647
+ ### Custom Handler Parameters
895
648
 
896
649
  ```ruby
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
650
+ nats_wave_subscribes_to "external_service.items.*" do |model_name, action, processed_data, original_message|
651
+ # model_name: The local model name ('Asset')
652
+ # action: The action performed ('create', 'update', 'delete')
653
+ # processed_data: Hash of the final processed/transformed data
654
+ # original_message: Full original NATS message hash
655
+
656
+ Rails.logger.info "Handler called for #{model_name}##{action}"
657
+ Rails.logger.info "Processed data keys: #{processed_data.keys}"
658
+ Rails.logger.info "Original message source: #{original_message.dig('source', 'service')}"
659
+
660
+ # Your custom logic here
661
+ case action
662
+ when 'create'
663
+ WelcomeEmailJob.perform_later(processed_data[:id])
664
+ when 'update'
665
+ CacheInvalidationJob.perform_later(processed_data[:id])
666
+ when 'delete'
667
+ CleanupJob.perform_later(processed_data[:id])
668
+ end
909
669
  end
670
+ ```
910
671
 
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)
672
+ ### Error Handling in Handlers
673
+
674
+ ```ruby
675
+ nats_wave_subscribes_to "risky_service.items.*",
676
+ unique_fields: [:external_id] do |model_name, action, processed_data, message|
677
+ begin
678
+ # Your processing logic
679
+ RiskyExternalService.process(processed_data)
680
+
681
+ Rails.logger.info "βœ… Successfully processed #{model_name} #{action}"
682
+
683
+ rescue RiskyExternalService::RateLimitError => e
684
+ # Retry later for rate limits
685
+ Rails.logger.warn "⏱️ Rate limited, retrying later: #{e.message}"
686
+ RetryProcessingJob.set(wait: 5.minutes).perform_later(processed_data, message)
687
+
688
+ rescue => e
689
+ # Log error but don't re-raise (would kill subscription)
690
+ Rails.logger.error "❌ Handler error for #{model_name} #{action}: #{e.message}"
691
+ ErrorTracker.notify(e, context: { model: model_name, action: action, data: processed_data })
692
+
693
+ # Send to dead letter queue for manual review
694
+ DeadLetterQueue.store(message, e)
917
695
  end
918
- end
696
+ end
919
697
  ```
920
698
 
921
699
  ## πŸ“Š Monitoring & Metrics
@@ -923,28 +701,46 @@ end
923
701
  ### ModelRegistry Inspection
924
702
 
925
703
  ```ruby
926
- # View all registered subscriptions
927
- NatsWave::ModelRegistry.debug_registrations
704
+ # View all registered subscriptions and mappings
705
+ rails console
928
706
 
929
- # Get subscription statistics
930
- stats = NatsWave::ModelRegistry.subscription_stats
707
+ # Debug all registrations
708
+ > NatsWave::ModelRegistry.debug_registrations
709
+
710
+ # Get subscription statistics
711
+ > stats = NatsWave::ModelRegistry.subscription_stats
931
712
  # => {
932
713
  # total_subscriptions: 5,
933
- # unique_subjects: 8,
714
+ # unique_subjects: 8,
934
715
  # models_with_subscriptions: 3,
935
716
  # subscription_breakdown: {"Asset"=>2, "Package"=>2, "User"=>1}
936
717
  # }
937
718
 
938
- # View all mappings
939
- mappings = NatsWave::ModelRegistry.all_mappings
719
+ # View subscriptions for specific model
720
+ > NatsWave::ModelRegistry.subscriptions_for_model('Asset')
721
+
722
+ # Check if external model can be synced
723
+ > NatsWave::ModelRegistry.can_sync_external_model?('InventoryItem')
724
+ # => true
725
+ ```
726
+
727
+ ### Built-in Health Checks
728
+
729
+ ```ruby
730
+ # Check overall system health
731
+ > NatsWave.health_check
940
732
  # => {
941
- # local_to_external: {...},
942
- # external_to_local: {"InventoryItem" => {"Asset" => {...}}}
733
+ # "nats_connected": true,
734
+ # "database_connected": true,
735
+ # "timestamp": "2024-01-21T12:00:00Z",
736
+ # "service_name": "valuation_service",
737
+ # "version": "1.2.0",
738
+ # "instance_id": "web-1",
739
+ # "model_registry_stats": {
740
+ # "total_subscriptions": 5,
741
+ # "unique_subjects": 8
742
+ # }
943
743
  # }
944
-
945
- # Find local models for external model
946
- local_models = NatsWave::ModelRegistry.local_models_for('InventoryItem')
947
- # => {"Asset" => {field_mappings: {...}, transformations: {...}}}
948
744
  ```
949
745
 
950
746
  ### Datadog Integration
@@ -953,93 +749,117 @@ NatsWave provides comprehensive Datadog metrics:
953
749
 
954
750
  ```ruby
955
751
  # Automatic metrics tracked:
956
- -nats_wave.messages.published # Messages published by subject/model
957
- -nats_wave.messages.received # Messages received by subject/model
958
- -nats_wave.processing_time # Message processing duration
959
- -nats_wave.errors # Errors by type and subject
960
- -nats_wave.connection_status # NATS connection health
961
- -nats_wave.sync.operations # Model sync operations
962
- -nats_wave.retries # Retry attempts
963
- -nats_wave.model_registry.subscriptions # Active subscriptions
752
+ # - nats_wave.messages.published (by subject/model/action)
753
+ # - nats_wave.messages.received (by subject/model/action)
754
+ # - nats_wave.processing_time (message processing duration)
755
+ # - nats_wave.sync_operations (auto-sync operations by model/action)
756
+ # - nats_wave.handler_execution_time (custom handler duration)
757
+ # - nats_wave.errors (errors by type and subject)
758
+ # - nats_wave.connection_status (NATS connection health)
759
+ # - nats_wave.retries (retry attempts)
760
+
761
+ # Enable in config/initializers/nats_wave.rb
762
+ if defined?(Datadog::Statsd) && !Rails.env.test?
763
+ NatsWave::Metrics.configure_datadog(
764
+ namespace: "#{Rails.application.class.name.deconstantize.underscore}.nats_wave",
765
+ tags: {
766
+ service: NatsWave.configuration.service_name,
767
+ environment: Rails.env,
768
+ version: Rails.application.config.version
769
+ }
770
+ )
771
+ end
964
772
  ```
965
773
 
966
- ### Health Checks
774
+ ### Health Check Endpoint
967
775
 
968
776
  ```ruby
969
- # Built-in health check
970
- health = NatsWave.health_check
777
+ # config/routes.rb
778
+ Rails.application.routes.draw do
779
+ get '/health/nats_wave', to: 'health#nats_wave'
780
+ end
971
781
 
972
- # Response:
973
- {
974
- "nats_connected": true,
975
- "database_connected": true,
976
- "timestamp": "2024-01-01T12:00:00Z",
977
- "service_name": "valuation_service",
978
- "version": "1.2.0",
979
- "instance_id": "web-1",
980
- "published_subjects": [
981
- "valuation_service.events.assets.*",
982
- "valuation_service.events.packages.*"
983
- ],
984
- "subscribed_subjects": [
985
- "inventory_service.items.*",
986
- "auction_service.assets.*"
987
- ],
988
- "model_registry_stats": {
989
- "total_subscriptions": 5,
990
- "unique_subjects": 8
991
- }
992
- }
782
+ # app/controllers/health_controller.rb
783
+ class HealthController < ApplicationController
784
+ def nats_wave
785
+ health_data = NatsWave.health_check
786
+
787
+ if health_data[:nats_connected]
788
+ render json: health_data, status: :ok
789
+ else
790
+ render json: health_data.merge(error: 'NATS not connected'),
791
+ status: :service_unavailable
792
+ end
793
+ rescue => e
794
+ render json: {
795
+ error: e.message,
796
+ nats_connected: false,
797
+ timestamp: Time.current.iso8601
798
+ }, status: :service_unavailable
799
+ end
800
+ end
993
801
  ```
994
802
 
995
803
  ## πŸ›‘οΈ Error Handling
996
804
 
997
- ### Dead Letter Queue
805
+ ### Automatic Error Recovery
998
806
 
999
- Failed messages are automatically stored with retry logic:
807
+ NatsWave includes robust automatic error handling:
1000
808
 
1001
- ```ruby
1002
- # Automatic retry with exponential backoff
1003
- # Retry 1: after 5 seconds
1004
- # Retry 2: after 25 seconds
1005
- # Retry 3: after 125 seconds
1006
- # After max retries: moved to permanent failure storage
1007
- ```
809
+ - **Auto-Reconnection**: Automatic reconnection with exponential backoff
810
+ - **Health Monitoring**: Continuous connection health checks
811
+ - **Dead Letter Queue**: Failed messages stored for manual review
812
+ - **Retry Logic**: Configurable retry attempts with delays
1008
813
 
1009
- ### Error Recovery Commands
1010
-
1011
- ```bash
1012
- # View failed messages
1013
- rails nats_wave:show_failed
814
+ ### Connection Recovery
1014
815
 
1015
- # Retry failed messages
1016
- rails nats_wave:retry_failed
1017
-
1018
- # Clear failed messages (after manual review)
1019
- rails nats_wave:clear_failed
1020
-
1021
- # Monitor live activity
1022
- rails nats_wave:monitor
816
+ ```ruby
817
+ # The client automatically handles:
818
+ # - Network disconnections
819
+ # - NATS server restarts
820
+ # - Temporary connection issues
821
+ # - Subscription restoration after reconnection
822
+
823
+ # Monitor connection health in logs:
824
+ # πŸ’œ Subscriber connection healthy - 3 active subscriptions
825
+ # πŸ”Œ NATS disconnected: Connection lost
826
+ # πŸ”„ Attempting to reconnect to NATS... (attempt #1)
827
+ # βœ… NATS client reconnected successfully
828
+ # βœ… Subscriptions restored (3 active)
1023
829
  ```
1024
830
 
1025
831
  ### Custom Error Handling
1026
832
 
1027
833
  ```ruby
1028
-
1029
834
  class CustomErrorHandler
1030
- def self.handle_failed_message(message, error, retry_count)
1031
- # Send to monitoring service
1032
- ErrorTracker.notify(error, {
1033
- message: message,
1034
- retry_count: retry_count,
1035
- service: 'nats_wave'
1036
- })
1037
-
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
835
+ def self.handle_subscription_error(error, message, model_name)
836
+ # Custom error processing
837
+ case error
838
+ when ValidationError
839
+ Rails.logger.warn "⚠️ Validation failed for #{model_name}: #{error.message}"
840
+ # Maybe send to a separate queue for data cleanup
841
+
842
+ when ExternalServiceError
843
+ Rails.logger.error "πŸ”Œ External service error: #{error.message}"
844
+ # Retry with exponential backoff
845
+ RetryJob.set(wait: 2.minutes).perform_later(message)
846
+
847
+ else
848
+ Rails.logger.error "❌ Unexpected error processing #{model_name}: #{error.message}"
849
+ # Send to monitoring
850
+ ErrorTracker.notify(error, context: { model: model_name, message: message })
1042
851
  end
852
+ end
853
+ end
854
+
855
+ # Use in your model handlers
856
+ nats_wave_subscribes_to "external.items.*" do |model_name, action, data, message|
857
+ begin
858
+ # Your processing logic
859
+ ExternalService.process(data)
860
+ rescue => e
861
+ CustomErrorHandler.handle_subscription_error(e, message, model_name)
862
+ end
1043
863
  end
1044
864
  ```
1045
865
 
@@ -1050,72 +870,160 @@ end
1050
870
  ```ruby
1051
871
  # spec/spec_helper.rb or test/test_helper.rb
1052
872
  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)
873
+ config.before(:suite) do
874
+ # Clear ModelRegistry before tests
875
+ NatsWave::ModelRegistry.clear! if defined?(NatsWave::ModelRegistry)
876
+ end
877
+
878
+ config.before(:each) do
879
+ # Mock NATS operations in tests by default
880
+ if defined?(NatsWave)
881
+ allow(NatsWave).to receive(:publish)
882
+ allow(NatsWave.client).to receive(:publish) if NatsWave.client
883
+ allow(NatsWave.client).to receive(:subscribe) if NatsWave.client
1062
884
  end
885
+ end
1063
886
  end
1064
887
  ```
1065
888
 
1066
- ### Testing Model Subscriptions
889
+ ### Testing Model Configuration
1067
890
 
1068
891
  ```ruby
1069
892
  # spec/models/asset_spec.rb
1070
893
  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
894
+ describe 'NATS Wave configuration' do
895
+ it 'registers correct subscriptions' do
896
+ subscriptions = NatsWave::ModelRegistry.subscriptions_for_model('Asset')
897
+ expect(subscriptions.size).to eq(1)
898
+
899
+ subjects = subscriptions.flat_map { |s| s[:subjects] }
900
+ expect(subjects).to include('inventory_service.items.*')
901
+ end
902
+
903
+ it 'has correct field mappings for inventory items' do
904
+ # Test that the model is registered to handle InventoryItem
905
+ local_models = NatsWave::ModelRegistry.local_models_for('InventoryItem')
906
+ expect(local_models).to have_key('Asset')
907
+
908
+ mapping = local_models['Asset']
909
+ expect(mapping[:field_mappings]['item_id']).to eq('unique_id')
910
+ expect(mapping[:field_mappings]['manufacturer']).to eq('make')
911
+ end
912
+
913
+ it 'has correct transformations' do
914
+ local_models = NatsWave::ModelRegistry.local_models_for('InventoryItem')
915
+ mapping = local_models['Asset']
916
+
917
+ # Test transformation functions
918
+ make_transformer = mapping[:transformations]['make']
919
+ expect(make_transformer.call('ford')).to eq('FORD')
920
+ expect(make_transformer.call(' Caterpillar ')).to eq('CATERPILLAR')
1086
921
  end
922
+ end
1087
923
  end
1088
924
  ```
1089
925
 
1090
926
  ### Testing Message Processing
1091
927
 
1092
928
  ```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
1117
- end
929
+ # spec/services/nats_wave_integration_spec.rb
930
+ RSpec.describe 'NATS Wave Integration' do
931
+ let(:sample_message) do
932
+ {
933
+ 'model' => 'InventoryItem',
934
+ 'action' => 'create',
935
+ 'data' => {
936
+ 'item_id' => 'INV-001',
937
+ 'manufacturer' => 'caterpillar',
938
+ 'model_name' => '320D',
939
+ 'year_manufactured' => '2020',
940
+ 'estimated_value' => 125000.00
941
+ },
942
+ 'source' => { 'service' => 'inventory_service' }
943
+ }
944
+ end
945
+
946
+ describe 'message processing' do
947
+ it 'creates asset from inventory message' do
948
+ expect {
949
+ # Simulate message processing (this would normally happen automatically)
950
+ NatsWave::MessageHandler.process(sample_message)
951
+ }.to change(Asset, :count).by(1)
952
+
953
+ asset = Asset.last
954
+ expect(asset.unique_id).to eq('INV-001')
955
+ expect(asset.make).to eq('CATERPILLAR') # Uppercased by transformation
956
+ expect(asset.model).to eq('320D')
957
+ expect(asset.year).to eq(2020) # Converted to integer
958
+ expect(asset.estimated_hammer).to eq(12500000) # Converted to cents
1118
959
  end
960
+
961
+ it 'calls custom handler after auto-sync' do
962
+ expect(ValuationRequestJob).to receive(:perform_later).with('INV-001')
963
+
964
+ # This would trigger both auto-sync and custom handler
965
+ NatsWave::MessageHandler.process(sample_message)
966
+ end
967
+ end
968
+
969
+ describe 'publishing' do
970
+ it 'publishes asset updates with field mappings' do
971
+ asset = Asset.create!(
972
+ unique_id: 'TEST-001',
973
+ make: 'FORD',
974
+ model: 'F-150',
975
+ year: 2020
976
+ )
977
+
978
+ expect(NatsWave).to receive(:publish) do |args|
979
+ expect(args[:subject]).to match(/assets\.update/)
980
+ expect(args[:data]['asset_id']).to eq('TEST-001') # Mapped from unique_id
981
+ expect(args[:data]['manufacturer']).to eq('FORD') # Mapped from make
982
+ end
983
+
984
+ asset.update!(make: 'FORD-UPDATED')
985
+ end
986
+ end
987
+ end
988
+ ```
989
+
990
+ ### Testing Custom Handlers
991
+
992
+ ```ruby
993
+ # spec/models/asset_nats_handlers_spec.rb
994
+ RSpec.describe 'Asset NATS Handlers' do
995
+ describe 'subscription handler' do
996
+ let(:message) do
997
+ {
998
+ 'model' => 'InventoryItem',
999
+ 'action' => 'create',
1000
+ 'data' => { 'item_id' => 'TEST-001', 'estimated_value' => nil }
1001
+ }
1002
+ end
1003
+
1004
+ it 'queues valuation request for assets without estimates' do
1005
+ expect(ValuationRequestJob).to receive(:perform_later).with('TEST-001')
1006
+
1007
+ # Simulate the handler being called
1008
+ # In real usage, this happens automatically when messages are received
1009
+ Asset.new.instance_eval do
1010
+ # Call the handler block manually for testing
1011
+ processed_data = { unique_id: 'TEST-001', estimated_hammer: nil }
1012
+ # Your handler logic here...
1013
+ end
1014
+ end
1015
+ end
1016
+
1017
+ describe 'publishing handler' do
1018
+ it 'sends notifications after publishing valuation updates' do
1019
+ asset = Asset.create!(unique_id: 'TEST-001', workflow_status: 'pending')
1020
+
1021
+ expect(NotificationService).to receive(:notify_valuation_complete)
1022
+
1023
+ # This would trigger publishing + custom handler
1024
+ asset.update!(workflow_status: 'complete')
1025
+ end
1026
+ end
1119
1027
  end
1120
1028
  ```
1121
1029
 
@@ -1177,71 +1085,58 @@ spec:
1177
1085
  app: valuation-service
1178
1086
  spec:
1179
1087
  containers:
1180
- - name: app
1181
- image: valuation-service:latest
1182
- env:
1183
- - name: NATS_URL
1184
- value: "nats://nats-cluster:4222"
1185
- - name: NATS_SERVICE_NAME
1186
- value: "valuation-service"
1187
- ports:
1188
- - containerPort: 3000
1189
- livenessProbe:
1190
- httpGet:
1191
- path: /health/nats_wave
1192
- port: 3000
1193
- initialDelaySeconds: 30
1194
- periodSeconds: 10
1195
- readinessProbe:
1196
- httpGet:
1197
- path: /health/nats_wave
1198
- port: 3000
1199
- initialDelaySeconds: 5
1200
- periodSeconds: 5
1088
+ - name: app
1089
+ image: valuation-service:latest
1090
+ env:
1091
+ - name: NATS_URL
1092
+ value: "nats://nats-cluster:4222"
1093
+ - name: NATS_SERVICE_NAME
1094
+ value: "valuation-service"
1095
+ - name: NATS_AUTH_SECRET
1096
+ valueFrom:
1097
+ secretKeyRef:
1098
+ name: nats-wave-secrets
1099
+ key: auth-secret
1100
+ ports:
1101
+ - containerPort: 3000
1102
+ livenessProbe:
1103
+ httpGet:
1104
+ path: /health/nats_wave
1105
+ port: 3000
1106
+ initialDelaySeconds: 30
1107
+ periodSeconds: 10
1108
+ readinessProbe:
1109
+ httpGet:
1110
+ path: /health/nats_wave
1111
+ port: 3000
1112
+ initialDelaySeconds: 5
1113
+ periodSeconds: 5
1201
1114
  ```
1202
1115
 
1203
1116
  ## πŸ’‘ Examples
1204
1117
 
1205
- ### Complete Asset Valuation Integration
1118
+ ### Complete Asset Workflow Integration
1206
1119
 
1207
1120
  ```ruby
1208
1121
  # app/models/asset.rb
1209
1122
  class Asset < ApplicationRecord
1210
- include NatsWave::NatsPublishable
1211
1123
  include NatsWave::Concerns::Mappable
1212
1124
 
1213
1125
  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
1126
  has_many :estimates, dependent: :destroy
1218
1127
  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
1128
 
1232
- # Sync from external inventory management system
1233
- nats_wave_maps_from 'InventoryAsset',
1129
+ # Subscribe to inventory system - auto-sync with custom processing
1130
+ nats_wave_subscribes_to "inventory_service.assets.*", "warehouse_service.assets.*",
1234
1131
  field_mappings: {
1235
1132
  'inventory_id' => 'unique_id',
1236
1133
  'asset_description' => 'description',
1237
1134
  'manufacturer' => 'make',
1238
- 'model_number' => 'model',
1135
+ 'model_number' => 'model',
1239
1136
  'year_manufactured' => 'year',
1240
1137
  'serial_number' => 'serial',
1241
1138
  'vin_number' => 'vin',
1242
1139
  'location_name' => 'location',
1243
- 'inventory_tag' => 'inventory_tag',
1244
- 'sticker_number' => 'sticker',
1245
1140
  'estimated_value' => 'estimated_hammer'
1246
1141
  },
1247
1142
  transformations: {
@@ -1249,19 +1144,43 @@ class Asset < ApplicationRecord
1249
1144
  'model' => ->(model) { model&.titleize&.strip },
1250
1145
  'year' => ->(year) { year.to_i if year.present? },
1251
1146
  'estimated_hammer' => ->(value) { (value.to_f * 100).round }, # Convert to cents
1252
- 'asset_status' => ->(status) { status&.downcase || 'in_progress' }
1147
+ 'asset_status' => ->(status) { status&.downcase || 'active' }
1253
1148
  },
1254
1149
  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.*']
1262
-
1263
- # Sync from auction management system
1264
- nats_wave_maps_from 'AuctionLot',
1150
+ skip_fields: ['internal_inventory_code', 'warehouse_notes'] do |model_name, action, processed_data, message|
1151
+ Rails.logger.info "πŸ“¦ Inventory sync: #{action} for #{processed_data[:unique_id]}"
1152
+
1153
+ case action
1154
+ when 'create'
1155
+ # Auto-request valuation for new assets without estimates
1156
+ if processed_data[:estimated_hammer].blank?
1157
+ ValuationRequestJob.perform_later(processed_data[:unique_id])
1158
+ Rails.logger.info "πŸ” Queued valuation request for #{processed_data[:unique_id]}"
1159
+ end
1160
+
1161
+ # Download images if provided
1162
+ if message.dig('data', 'image_urls').present?
1163
+ ImageDownloadJob.perform_later(processed_data[:unique_id], message['data']['image_urls'])
1164
+ end
1165
+
1166
+ when 'update'
1167
+ # Track significant value changes
1168
+ original_value = Asset.find_by(unique_id: processed_data[:unique_id])&.estimated_hammer
1169
+ if original_value != processed_data[:estimated_hammer]
1170
+ AssetValueChangeNotificationJob.perform_later(processed_data[:unique_id])
1171
+ end
1172
+
1173
+ when 'delete', 'destroy'
1174
+ # Clean up related data
1175
+ CleanupAssetDataJob.perform_later(processed_data[:unique_id])
1176
+ end
1177
+
1178
+ # Always update search index
1179
+ SearchIndexUpdateJob.perform_later(processed_data[:unique_id], 'Asset')
1180
+ end
1181
+
1182
+ # Subscribe to auction results
1183
+ nats_wave_subscribes_to "auction_service.lots.*",
1265
1184
  field_mappings: {
1266
1185
  'lot_number' => 'lot_number',
1267
1186
  'hammer_price' => 'pw_contract_price',
@@ -1272,65 +1191,81 @@ class Asset < ApplicationRecord
1272
1191
  'pw_contract_price' => ->(price) { (price.to_f * 100).round }, # Convert to cents
1273
1192
  'pw_contract_date' => ->(date) { Time.parse(date.to_s) rescue nil }
1274
1193
  },
1275
- conditions: {
1276
- 'source.service' => ->(service) { service != ENV['NATS_SERVICE_NAME'] },
1277
- 'status' => 'sold'
1278
- },
1279
- subjects: ['auction_service.lots.*']
1280
-
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
1194
+ unique_fields: [:lot_number] do |model_name, action, processed_data, message|
1195
+ if action == 'sold'
1196
+ Rails.logger.info "πŸ”¨ Asset sold: #{processed_data[:lot_number]} for $#{processed_data[:pw_contract_price] / 100}"
1197
+
1198
+ # Create final estimate based on actual sale
1199
+ asset = Asset.find_by(lot_number: processed_data[:lot_number])
1200
+ if asset
1201
+ Estimate.create!(
1202
+ asset: asset,
1203
+ user: User.find_by(email: 'auction@system.com'),
1204
+ estimate_type: EstimateType.find_by(name: 'Auction Result'),
1205
+ status: 'current',
1206
+ value_category: 'final',
1207
+ value: processed_data[:pw_contract_price],
1208
+ version_number: 1,
1209
+ notes: "Actual auction sale price from #{processed_data[:pw_auction]}"
1210
+ )
1211
+
1212
+ # Notify stakeholders
1213
+ AuctionResultNotificationJob.perform_later(asset.id)
1214
+ end
1215
+ end
1216
+ end
1287
1217
 
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
1218
+ # Publish asset changes to other services
1219
+ nats_wave_publishes_to "#{ENV['NATS_SERVICE_NAME']}.assets.{action}",
1220
+ field_mappings: {
1221
+ 'unique_id' => 'asset_id',
1222
+ 'make' => 'manufacturer',
1223
+ 'model' => 'model_name',
1224
+ 'estimated_hammer' => 'estimated_value'
1225
+ },
1226
+ transformations: {
1227
+ 'created_at' => lambda(&:iso8601),
1228
+ 'updated_at' => lambda(&:iso8601),
1229
+ 'estimated_value' => ->(cents) { cents.to_f / 100 } # Convert back to dollars
1230
+ },
1231
+ skip_fields: %w[notes edge_data source_data package_id creator_id last_updater_id],
1232
+ actions: %i[create update] do |model_name, action, published_data, record|
1233
+ Rails.logger.info "πŸ“€ Published #{action} for asset: #{record.unique_id}"
1234
+
1235
+ # Custom processing after publishing
1236
+ if action == 'update' && record.estimated_hammer_changed?
1237
+ # Notify external pricing services
1238
+ ExternalPricingService.update_asset_value(record.unique_id, record.estimated_hammer)
1239
+ end
1240
+
1241
+ # Update external inventory systems
1242
+ if record.location_changed?
1243
+ ExternalInventoryService.update_location(record.unique_id, record.location)
1244
+ end
1245
+ end
1300
1246
  end
1247
+ ```
1301
1248
 
1302
- # app/models/package.rb
1249
+ ### Package Management Integration
1250
+
1251
+ ```ruby
1252
+ # app/models/package.rb
1303
1253
  class Package < ApplicationRecord
1304
- include NatsWave::NatsPublishable
1305
1254
  include NatsWave::Concerns::Mappable
1306
1255
 
1307
1256
  belongs_to :organization
1308
- belongs_to :contact_user, class_name: 'User', optional: true
1309
1257
  has_many :assets, dependent: :destroy
1310
- has_many :subpackages, dependent: :destroy
1311
1258
  has_many :valuation_requests, dependent: :destroy
1312
- has_many :ims_submissions, dependent: :destroy
1313
1259
 
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',
1260
+ # Subscribe to CRM project updates
1261
+ nats_wave_subscribes_to "crm_service.projects.*",
1325
1262
  field_mappings: {
1326
1263
  'project_id' => 'key',
1327
1264
  'client_name' => 'owner_name',
1328
1265
  'client_email' => 'owner_email',
1329
1266
  'project_location' => 'location',
1330
1267
  'project_status' => 'workflow_status',
1331
- 'project_notes' => 'notes',
1332
- 'opportunity_reference' => 'opportunity_id',
1333
- 'contact_person' => 'primary_contact'
1268
+ 'opportunity_reference' => 'opportunity_id'
1334
1269
  },
1335
1270
  transformations: {
1336
1271
  'workflow_status' => ->(status) {
@@ -1342,354 +1277,108 @@ class Package < ApplicationRecord
1342
1277
  end
1343
1278
  }
1344
1279
  },
1345
- unique_fields: [:key],
1346
- conditions: {
1347
- 'source.service' => ->(service) { service != ENV['NATS_SERVICE_NAME'] }
1348
- },
1349
- subjects: ['crm_service.projects.*']
1350
-
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
- )
1280
+ unique_fields: [:key] do |model_name, action, processed_data, message|
1281
+ Rails.logger.info "πŸ“‹ CRM project sync: #{action} for #{processed_data[:key]}"
1282
+
1283
+ if action == 'create'
1284
+ # Auto-create valuation request for new projects
1285
+ ValuationRequest.create!(
1286
+ package_id: Package.find_by(key: processed_data[:key])&.id,
1287
+ user: User.find_by(email: 'system@valuation.com'),
1288
+ notes: 'Auto-created from CRM project'
1289
+ )
1290
+ end
1291
+ end
1393
1292
 
1394
- # Sync from external valuation services
1395
- nats_wave_maps_from 'ExternalValuation',
1293
+ # Publish package workflow changes
1294
+ nats_wave_publishes_to "#{ENV['NATS_SERVICE_NAME']}.packages.{action}",
1396
1295
  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'
1296
+ 'key' => 'package_key',
1297
+ 'owner_name' => 'client_name',
1298
+ 'workflow_status' => 'status'
1403
1299
  },
1404
1300
  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 }
1301
+ 'created_at' => lambda(&:iso8601),
1302
+ 'updated_at' => lambda(&:iso8601)
1412
1303
  },
1413
- unique_fields: [:key],
1414
- conditions: {
1415
- 'source.service' => ->(service) { service != ENV['NATS_SERVICE_NAME'] }
1416
- },
1417
- subjects: ['valuation_service.estimates.*']
1304
+ skip_fields: %w[notes source_data],
1305
+ actions: %i[create update] do |model_name, action, published_data, record|
1306
+ Rails.logger.info "πŸ“€ Published package #{action}: #{record.key}"
1307
+
1308
+ # Notify CRM of workflow changes
1309
+ if action == 'update' && record.workflow_status_changed?
1310
+ CRMNotificationService.notify_status_change(record)
1311
+ end
1312
+ end
1418
1313
  end
1314
+ ```
1419
1315
 
1420
- # app/models/user.rb
1316
+ ### User & Organization Sync
1317
+
1318
+ ```ruby
1319
+ # app/models/user.rb
1421
1320
  class User < ApplicationRecord
1422
- include NatsWave::NatsPublishable
1423
1321
  include NatsWave::Concerns::Mappable
1424
1322
 
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',
1323
+ # Subscribe to authentication service
1324
+ nats_wave_subscribes_to "auth_service.users.*",
1441
1325
  field_mappings: {
1442
1326
  'user_id' => 'key',
1443
1327
  'email_address' => 'email',
1444
1328
  'full_name' => 'name',
1445
- 'is_active' => 'active',
1446
- 'organization_key' => 'current_organization_key'
1329
+ 'is_active' => 'active'
1447
1330
  },
1448
1331
  transformations: {
1449
1332
  'email' => ->(email) { email&.downcase&.strip },
1450
1333
  'name' => ->(name) { name&.strip },
1451
1334
  'active' => ->(active) { active == 'true' || active == true }
1452
1335
  },
1453
- unique_fields: [:key, :email],
1454
- conditions: {
1455
- 'source.service' => ->(service) { service != ENV['NATS_SERVICE_NAME'] }
1456
- },
1457
- subjects: ['auth_service.users.*']
1336
+ unique_fields: [:key, :email] do |model_name, action, processed_data, message|
1337
+ Rails.logger.info "πŸ‘€ User sync: #{action} for #{processed_data[:email]}"
1338
+
1339
+ if action == 'create'
1340
+ # Send welcome email
1341
+ UserMailer.welcome_email(processed_data[:email]).deliver_later
1342
+ end
1343
+ end
1344
+
1345
+ # Publish user activity for analytics
1346
+ nats_wave_publishes_to "#{ENV['NATS_SERVICE_NAME']}.users.{action}",
1347
+ skip_fields: %w[api_auth_key source_details],
1348
+ actions: %i[create update] do |model_name, action, published_data, record|
1349
+ Rails.logger.info "πŸ“€ Published user #{action}: #{record.email}"
1350
+ end
1458
1351
  end
1459
1352
 
1460
1353
  # app/models/organization.rb
1461
1354
  class Organization < ApplicationRecord
1462
- include NatsWave::NatsPublishable
1463
1355
  include NatsWave::Concerns::Mappable
1464
1356
 
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',
1357
+ # Subscribe to CRM account updates
1358
+ nats_wave_subscribes_to "crm_service.accounts.*",
1482
1359
  field_mappings: {
1483
1360
  'account_id' => 'key',
1484
1361
  'company_name' => 'name',
1485
1362
  'is_active' => 'active',
1486
1363
  'primary_contact_name' => 'contact',
1487
- 'primary_email' => 'email',
1488
- 'primary_phone' => 'phone',
1489
- 'account_group' => 'group_name'
1364
+ 'primary_email' => 'email'
1490
1365
  },
1491
1366
  transformations: {
1492
1367
  'name' => ->(name) { name&.strip },
1493
1368
  'email' => ->(email) { email&.downcase&.strip },
1494
1369
  'active' => ->(active) { active == 'true' || active == true }
1495
1370
  },
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
1543
- end
1544
- ```
1545
-
1546
- ### Cross-Service Workflow Example
1547
-
1548
- ```ruby
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
1371
+ unique_fields: [:key, :name] do |model_name, action, processed_data, message|
1372
+ Rails.logger.info "🏒 Organization sync: #{action} for #{processed_data[:name]}"
1579
1373
  end
1580
-
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
- )
1605
- end
1606
- end
1607
-
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
1626
- end
1627
- end
1628
- ```
1629
-
1630
- ### User & Organization Sync
1631
-
1632
- ```ruby
1633
- # app/models/user.rb
1634
- class User < ApplicationRecord
1635
- include NatsWave::NatsPublishable
1636
- include NatsWave::Concerns::Mappable
1637
-
1638
- nats_publishable(
1639
- skip_attributes: [:api_auth_key, :source_details],
1640
- subject_prefix: 'users'
1641
- )
1642
-
1643
- # Sync from authentication service
1644
- nats_wave_maps_from 'AuthUser',
1645
- field_mappings: {
1646
- 'user_id' => 'key',
1647
- 'email_address' => 'email',
1648
- 'full_name' => 'name',
1649
- 'is_active' => 'active',
1650
- 'organization_id' => 'current_organization_key'
1651
- },
1652
- transformations: {
1653
- 'email' => ->(email) { email&.downcase&.strip },
1654
- 'name' => ->(name) { name&.titleize&.strip },
1655
- 'active' => ->(active) { active == 'true' || active == true }
1656
- },
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)
1374
+
1375
+ # Publish organization changes
1376
+ nats_wave_publishes_to "#{ENV['NATS_SERVICE_NAME']}.organizations.{action}",
1377
+ skip_fields: %w[api_auth_key workflow_config],
1378
+ actions: %i[create update] do |model_name, action, published_data, record|
1379
+ Rails.logger.info "πŸ“€ Published organization #{action}: #{record.name}"
1663
1380
  end
1664
1381
  end
1665
-
1666
- # app/models/organization.rb
1667
- class Organization < ApplicationRecord
1668
- include NatsWave::NatsPublishable
1669
- include NatsWave::Concerns::Mappable
1670
-
1671
- nats_publishable(
1672
- skip_attributes: [:api_auth_key, :workflow_config],
1673
- subject_prefix: 'organizations'
1674
- )
1675
-
1676
- # Sync from CRM system
1677
- nats_wave_maps_from 'CRMOrganization',
1678
- field_mappings: {
1679
- 'organization_id' => 'key',
1680
- 'company_name' => 'name',
1681
- 'primary_contact' => 'contact',
1682
- 'email_address' => 'email',
1683
- 'phone_number' => 'phone',
1684
- 'is_active' => 'active'
1685
- },
1686
- transformations: {
1687
- 'name' => ->(name) { name&.strip },
1688
- 'email' => ->(email) { email&.downcase&.strip }
1689
- },
1690
- unique_fields: [:key, :name],
1691
- subjects: ['crm_service.organizations.*']
1692
- end
1693
1382
  ```
1694
1383
 
1695
1384
  ## 🀝 Contributing
@@ -1729,9 +1418,8 @@ This gem is available as open source under the terms of the [MIT License](LICENS
1729
1418
 
1730
1419
  ## 🏒 About Purple Wave
1731
1420
 
1732
- NatsWave is developed and maintained by [Purple Wave](https://purplewave.com) to enable seamless communication between
1733
- our microservices and teams in the asset valuation and auction industry.
1421
+ NatsWave is developed and maintained by [Purple Wave](https://purplewave.com) to enable seamless communication between our microservices and teams in the asset valuation and auction industry.
1734
1422
 
1735
1423
  ---
1736
1424
 
1737
- **Made with ❀️ by the Purple Wave Team**
1425
+ **Made with ❀️ by Jeffrey Dabo**