nats_wave 1.1.5 β†’ 1.1.8

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