source_monitor 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/agents/rails-concern.md +464 -0
  3. data/.claude/agents/rails-controller.md +424 -0
  4. data/.claude/agents/rails-hotwire.md +446 -0
  5. data/.claude/agents/rails-implement.md +374 -0
  6. data/.claude/agents/rails-job.md +334 -0
  7. data/.claude/agents/rails-lint.md +294 -0
  8. data/.claude/agents/rails-mailer.md +371 -0
  9. data/.claude/agents/rails-migration.md +449 -0
  10. data/.claude/agents/rails-model.md +420 -0
  11. data/.claude/agents/rails-policy.md +443 -0
  12. data/.claude/agents/rails-presenter.md +427 -0
  13. data/.claude/agents/rails-query.md +412 -0
  14. data/.claude/agents/rails-review.md +490 -0
  15. data/.claude/agents/rails-service.md +458 -0
  16. data/.claude/agents/rails-state-records.md +465 -0
  17. data/.claude/agents/rails-tdd.md +314 -0
  18. data/.claude/agents/rails-test.md +441 -0
  19. data/.claude/agents/rails-view-component.md +418 -0
  20. data/.claude/hooks/block-secrets.sh +52 -0
  21. data/.claude/settings.json +85 -0
  22. data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
  23. data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
  24. data/.claude/skills/active-storage-setup/SKILL.md +311 -0
  25. data/.claude/skills/api-versioning/SKILL.md +294 -0
  26. data/.claude/skills/authentication-flow/SKILL.md +335 -0
  27. data/.claude/skills/authentication-flow/reference/current.md +248 -0
  28. data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
  29. data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
  30. data/.claude/skills/authorization-pundit/SKILL.md +462 -0
  31. data/.claude/skills/caching-strategies/SKILL.md +350 -0
  32. data/.claude/skills/database-migrations/SKILL.md +354 -0
  33. data/.claude/skills/form-object-patterns/SKILL.md +399 -0
  34. data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
  35. data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
  36. data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
  37. data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
  38. data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
  39. data/.claude/skills/i18n-patterns/SKILL.md +320 -0
  40. data/.claude/skills/install/SKILL.md +367 -0
  41. data/.claude/skills/performance-optimization/SKILL.md +311 -0
  42. data/.claude/skills/rails-architecture/SKILL.md +259 -0
  43. data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
  44. data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
  45. data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
  46. data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
  47. data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
  48. data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
  49. data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
  50. data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
  51. data/.claude/skills/rails-concern/SKILL.md +399 -0
  52. data/.claude/skills/rails-controller/SKILL.md +336 -0
  53. data/.claude/skills/rails-model-generator/SKILL.md +321 -0
  54. data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
  55. data/.claude/skills/rails-presenter/SKILL.md +274 -0
  56. data/.claude/skills/rails-query-object/SKILL.md +289 -0
  57. data/.claude/skills/rails-service-object/SKILL.md +349 -0
  58. data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
  59. data/.claude/skills/tdd-cycle/SKILL.md +359 -0
  60. data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
  61. data/.gitignore +1 -0
  62. data/.rubocop.yml +2 -0
  63. data/.ruby-version +1 -1
  64. data/.vbw-planning/.notification-log.jsonl +192 -0
  65. data/.vbw-planning/.session-log.jsonl +871 -0
  66. data/.vbw-planning/PROJECT.md +51 -0
  67. data/.vbw-planning/REQUIREMENTS.md +50 -0
  68. data/.vbw-planning/SHIPPED.md +28 -0
  69. data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
  70. data/.vbw-planning/codebase/CONCERNS.md +99 -0
  71. data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
  72. data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
  73. data/.vbw-planning/codebase/INDEX.md +86 -0
  74. data/.vbw-planning/codebase/META.md +42 -0
  75. data/.vbw-planning/codebase/PATTERNS.md +262 -0
  76. data/.vbw-planning/codebase/STACK.md +101 -0
  77. data/.vbw-planning/codebase/STRUCTURE.md +324 -0
  78. data/.vbw-planning/codebase/TESTING.md +154 -0
  79. data/.vbw-planning/config.json +12 -0
  80. data/.vbw-planning/discovery.json +24 -0
  81. data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
  82. data/.vbw-planning/milestones/default/STATE.md +83 -0
  83. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
  84. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
  85. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
  86. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
  87. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
  88. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
  89. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
  90. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
  91. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
  92. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
  93. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
  94. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
  95. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
  96. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
  97. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
  98. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
  99. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
  100. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
  101. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
  102. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
  103. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
  104. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
  105. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
  106. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
  107. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
  108. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
  109. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
  110. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
  111. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
  112. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
  113. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
  114. data/CHANGELOG.md +28 -0
  115. data/CLAUDE.md +179 -0
  116. data/Gemfile +8 -0
  117. data/Gemfile.lock +114 -101
  118. data/Rakefile +2 -0
  119. data/app/assets/builds/source_monitor/application.css +2076 -0
  120. data/app/assets/builds/source_monitor/application.js +2758 -0
  121. data/app/assets/builds/source_monitor/application.js.map +7 -0
  122. data/app/controllers/source_monitor/application_controller.rb +2 -0
  123. data/app/controllers/source_monitor/health_controller.rb +2 -0
  124. data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
  125. data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
  126. data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
  127. data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
  128. data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
  129. data/app/controllers/source_monitor/items_controller.rb +2 -0
  130. data/app/controllers/source_monitor/sources_controller.rb +0 -14
  131. data/app/helpers/source_monitor/application_helper.rb +4 -112
  132. data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
  133. data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
  134. data/app/jobs/source_monitor/application_job.rb +2 -0
  135. data/app/models/source_monitor/application_record.rb +2 -0
  136. data/app/models/source_monitor/log_entry.rb +0 -2
  137. data/config/coverage_baseline.json +217 -1862
  138. data/config/routes.rb +2 -0
  139. data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
  140. data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
  141. data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
  142. data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
  143. data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
  144. data/lib/source_monitor/assets/bundler.rb +2 -0
  145. data/lib/source_monitor/assets.rb +2 -0
  146. data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
  147. data/lib/source_monitor/configuration/events.rb +60 -0
  148. data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
  149. data/lib/source_monitor/configuration/health_settings.rb +27 -0
  150. data/lib/source_monitor/configuration/http_settings.rb +43 -0
  151. data/lib/source_monitor/configuration/model_definition.rb +108 -0
  152. data/lib/source_monitor/configuration/models.rb +36 -0
  153. data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
  154. data/lib/source_monitor/configuration/retention_settings.rb +45 -0
  155. data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
  156. data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
  157. data/lib/source_monitor/configuration/validation_definition.rb +32 -0
  158. data/lib/source_monitor/configuration.rb +12 -579
  159. data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
  160. data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
  161. data/lib/source_monitor/dashboard/queries.rb +2 -195
  162. data/lib/source_monitor/engine.rb +2 -0
  163. data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
  164. data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
  165. data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
  166. data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
  167. data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
  168. data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
  169. data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
  170. data/lib/source_monitor/items/item_creator.rb +28 -455
  171. data/lib/source_monitor/setup/bundle_installer.rb +2 -0
  172. data/lib/source_monitor/setup/cli.rb +2 -0
  173. data/lib/source_monitor/setup/dependency_checker.rb +2 -0
  174. data/lib/source_monitor/setup/detectors.rb +2 -0
  175. data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
  176. data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
  177. data/lib/source_monitor/setup/install_generator.rb +2 -0
  178. data/lib/source_monitor/setup/migration_installer.rb +2 -0
  179. data/lib/source_monitor/setup/node_installer.rb +2 -0
  180. data/lib/source_monitor/setup/prompter.rb +2 -0
  181. data/lib/source_monitor/setup/requirements.rb +2 -0
  182. data/lib/source_monitor/setup/shell_runner.rb +2 -0
  183. data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
  184. data/lib/source_monitor/setup/verification/printer.rb +2 -0
  185. data/lib/source_monitor/setup/verification/result.rb +2 -0
  186. data/lib/source_monitor/setup/verification/runner.rb +2 -0
  187. data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
  188. data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
  189. data/lib/source_monitor/setup/workflow.rb +2 -0
  190. data/lib/source_monitor/version.rb +3 -1
  191. data/lib/source_monitor.rb +140 -58
  192. data/lib/tasks/source_monitor_assets.rake +2 -0
  193. data/lib/tasks/source_monitor_setup.rake +2 -0
  194. data/lib/tasks/source_monitor_tasks.rake +2 -0
  195. data/source_monitor.gemspec +3 -1
  196. metadata +144 -4
@@ -0,0 +1,342 @@
1
+ # Query Object Patterns
2
+
3
+ ## Basic Query Structure
4
+
5
+ ```ruby
6
+ # app/queries/[name]_query.rb
7
+ class NameQuery
8
+ attr_reader :account
9
+
10
+ def initialize(account:)
11
+ @account = account
12
+ end
13
+
14
+ # @return [ActiveRecord::Relation<Model>]
15
+ def call
16
+ account.models
17
+ .where(conditions)
18
+ .order(created_at: :desc)
19
+ end
20
+ end
21
+ ```
22
+
23
+ ## Query Categories
24
+
25
+ ### 1. Filter Queries
26
+
27
+ Return filtered ActiveRecord relations:
28
+
29
+ ```ruby
30
+ # app/queries/active_events_query.rb
31
+ class ActiveEventsQuery
32
+ attr_reader :account
33
+
34
+ def initialize(account:)
35
+ @account = account
36
+ end
37
+
38
+ def call(date_range: nil)
39
+ scope = account.events.where(status: :active)
40
+ scope = scope.where(event_date: date_range) if date_range
41
+ scope.includes(:venue, :vendors).order(event_date: :asc)
42
+ end
43
+ end
44
+ ```
45
+
46
+ ### 2. Aggregation Queries
47
+
48
+ Return computed statistics:
49
+
50
+ ```ruby
51
+ # app/queries/revenue_stats_query.rb
52
+ class RevenueStatsQuery
53
+ attr_reader :account
54
+
55
+ def initialize(account:)
56
+ @account = account
57
+ end
58
+
59
+ def call(period: :month)
60
+ {
61
+ total: total_revenue,
62
+ by_period: revenue_by_period(period),
63
+ by_category: revenue_by_category,
64
+ growth_rate: calculate_growth
65
+ }
66
+ end
67
+
68
+ private
69
+
70
+ def total_revenue
71
+ account.orders.completed.sum(:total_cents)
72
+ end
73
+
74
+ def revenue_by_period(period)
75
+ group_clause = case period
76
+ when :day then "DATE(created_at)"
77
+ when :week then "DATE_TRUNC('week', created_at)"
78
+ when :month then "DATE_TRUNC('month', created_at)"
79
+ end
80
+
81
+ account.orders.completed
82
+ .group(Arel.sql(group_clause))
83
+ .sum(:total_cents)
84
+ end
85
+
86
+ def revenue_by_category
87
+ account.orders.completed
88
+ .joins(line_items: :product)
89
+ .group("products.category")
90
+ .sum(:total_cents)
91
+ end
92
+ end
93
+ ```
94
+
95
+ ### 3. Dashboard Queries
96
+
97
+ Multiple related metrics:
98
+
99
+ ```ruby
100
+ # app/queries/dashboard_stats_query.rb
101
+ class DashboardStatsQuery
102
+ attr_reader :user, :account
103
+
104
+ def initialize(user:)
105
+ @user = user
106
+ @account = user.account
107
+ end
108
+
109
+ def upcoming_events(limit: 5)
110
+ account.events
111
+ .where("event_date >= ?", Date.current)
112
+ .order(event_date: :asc)
113
+ .limit(limit)
114
+ end
115
+
116
+ def pending_tasks_count
117
+ account.tasks.pending.count
118
+ end
119
+
120
+ def leads_by_status
121
+ account.leads.group(:status).count
122
+ end
123
+
124
+ def recent_activity(limit: 10)
125
+ account.activities
126
+ .includes(:user, :trackable)
127
+ .order(created_at: :desc)
128
+ .limit(limit)
129
+ end
130
+ end
131
+ ```
132
+
133
+ ### 4. Search Queries
134
+
135
+ Full-text search with filters:
136
+
137
+ ```ruby
138
+ # app/queries/vendor_search_query.rb
139
+ class VendorSearchQuery
140
+ attr_reader :account
141
+
142
+ def initialize(account:)
143
+ @account = account
144
+ end
145
+
146
+ def call(term:, filters: {})
147
+ scope = account.vendors
148
+
149
+ scope = apply_search(scope, term) if term.present?
150
+ scope = apply_filters(scope, filters)
151
+ scope = apply_sorting(scope, filters[:sort])
152
+
153
+ scope.includes(:category, :reviews)
154
+ end
155
+
156
+ private
157
+
158
+ def apply_search(scope, term)
159
+ scope.where(
160
+ "name ILIKE :term OR description ILIKE :term",
161
+ term: "%#{sanitize_like(term)}%"
162
+ )
163
+ end
164
+
165
+ def apply_filters(scope, filters)
166
+ scope = scope.where(category_id: filters[:category]) if filters[:category]
167
+ scope = scope.where(active: true) if filters[:active_only]
168
+ scope = scope.where("rating >= ?", filters[:min_rating]) if filters[:min_rating]
169
+ scope
170
+ end
171
+
172
+ def apply_sorting(scope, sort)
173
+ case sort
174
+ when "name" then scope.order(name: :asc)
175
+ when "rating" then scope.order(rating: :desc)
176
+ when "recent" then scope.order(created_at: :desc)
177
+ else scope.order(name: :asc)
178
+ end
179
+ end
180
+
181
+ def sanitize_like(term)
182
+ term.gsub(/[%_]/) { |x| "\\#{x}" }
183
+ end
184
+ end
185
+ ```
186
+
187
+ ### 5. Report Queries
188
+
189
+ Complex data for exports:
190
+
191
+ ```ruby
192
+ # app/queries/event_report_query.rb
193
+ class EventReportQuery
194
+ attr_reader :account
195
+
196
+ def initialize(account:)
197
+ @account = account
198
+ end
199
+
200
+ def call(date_range:)
201
+ account.events
202
+ .where(event_date: date_range)
203
+ .includes(:venue, :vendors, :attendees)
204
+ .select(
205
+ "events.*",
206
+ "COUNT(DISTINCT attendees.id) as attendee_count",
207
+ "SUM(event_vendors.amount_cents) as total_vendor_cost"
208
+ )
209
+ .joins(:attendees, :event_vendors)
210
+ .group("events.id")
211
+ .order(event_date: :asc)
212
+ end
213
+ end
214
+ ```
215
+
216
+ ## Performance Patterns
217
+
218
+ ### Eager Loading
219
+
220
+ ```ruby
221
+ def call
222
+ account.events
223
+ .includes(:venue) # Belongs-to
224
+ .includes(:vendors) # Has-many through
225
+ .includes(attendees: :user) # Nested
226
+ .preload(:documents) # Separate query
227
+ .eager_load(:primary_contact) # LEFT JOIN
228
+ end
229
+ ```
230
+
231
+ ### Batch Processing
232
+
233
+ ```ruby
234
+ def process_all
235
+ account.events.find_each(batch_size: 100) do |event|
236
+ yield event
237
+ end
238
+ end
239
+ ```
240
+
241
+ ### Subquery Optimization
242
+
243
+ ```ruby
244
+ def call
245
+ # Use subquery instead of pluck for large datasets
246
+ active_vendor_ids = account.vendors.active.select(:id)
247
+
248
+ account.events
249
+ .where(vendor_id: active_vendor_ids)
250
+ .order(created_at: :desc)
251
+ end
252
+ ```
253
+
254
+ ## Multi-Tenancy Patterns
255
+
256
+ ### Always Scope Through Account
257
+
258
+ ```ruby
259
+ # GOOD
260
+ def call
261
+ account.events.where(status: :active)
262
+ end
263
+
264
+ # BAD - Security risk!
265
+ def call
266
+ Event.where(account_id: account.id, status: :active)
267
+ end
268
+ ```
269
+
270
+ ### Test Isolation
271
+
272
+ ```ruby
273
+ # test/queries/active_events_query_test.rb
274
+ require "test_helper"
275
+
276
+ class ActiveEventsQueryTest < ActiveSupport::TestCase
277
+ test "only returns events for the account" do
278
+ account = accounts(:one)
279
+ our_event = events(:one) # belongs to accounts(:one)
280
+ their_event = events(:other_account_event)
281
+
282
+ result = ActiveEventsQuery.new(account: account).call
283
+ assert_includes result, our_event
284
+ assert_not_includes result, their_event
285
+ end
286
+ end
287
+ ```
288
+
289
+ ## Composition Patterns
290
+
291
+ ### Query Chaining
292
+
293
+ ```ruby
294
+ # Queries return relations, enabling chaining
295
+ events = ActiveEventsQuery.new(account: account).call
296
+ upcoming = events.where("event_date > ?", Date.current)
297
+ paginated = upcoming.page(params[:page]).per(20)
298
+ ```
299
+
300
+ ### Query Composition
301
+
302
+ ```ruby
303
+ class ComplexReportQuery
304
+ def initialize(account:)
305
+ @events_query = ActiveEventsQuery.new(account: account)
306
+ @revenue_query = RevenueStatsQuery.new(account: account)
307
+ end
308
+
309
+ def call(date_range:)
310
+ {
311
+ events: @events_query.call(date_range: date_range),
312
+ revenue: @revenue_query.call
313
+ }
314
+ end
315
+ end
316
+ ```
317
+
318
+ ## Usage in Controllers
319
+
320
+ ```ruby
321
+ class EventsController < ApplicationController
322
+ def index
323
+ @events = ActiveEventsQuery.new(account: current_account)
324
+ .call
325
+ .page(params[:page])
326
+ end
327
+
328
+ def dashboard
329
+ @stats = DashboardStatsQuery.new(user: current_user)
330
+ end
331
+ end
332
+ ```
333
+
334
+ ## Checklist
335
+
336
+ - [ ] Constructor accepts `account:` or `user:`
337
+ - [ ] Always scoped through account (multi-tenant)
338
+ - [ ] Return type documented (`@return`)
339
+ - [ ] Uses `.includes()` to prevent N+1
340
+ - [ ] Search terms sanitized
341
+ - [ ] Spec tests tenant isolation
342
+ - [ ] Complex queries explain their purpose
@@ -0,0 +1,286 @@
1
+ # Service Object Patterns
2
+
3
+ ## Basic Service Structure
4
+
5
+ ```ruby
6
+ # app/services/[namespace]/[verb]_service.rb
7
+ module Namespace
8
+ class VerbService
9
+ def initialize(dependencies = {})
10
+ @dependency = dependencies[:dependency] || DefaultDependency.new
11
+ end
12
+
13
+ def call(params)
14
+ validate_input(params)
15
+ perform_operation(params)
16
+ success(result)
17
+ rescue StandardError => e
18
+ failure(e.message)
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :dependency
24
+
25
+ def success(data)
26
+ Result.new(success: true, data: data)
27
+ end
28
+
29
+ def failure(error, code = :unknown)
30
+ Result.new(success: false, error: error, code: code)
31
+ end
32
+ end
33
+ end
34
+ ```
35
+
36
+ ## Service Categories
37
+
38
+ ### 1. Command Services (Write Operations)
39
+
40
+ Single action that changes state:
41
+
42
+ ```ruby
43
+ # app/services/orders/create_service.rb
44
+ module Orders
45
+ class CreateService
46
+ def call(user:, items:)
47
+ order = nil
48
+
49
+ ActiveRecord::Base.transaction do
50
+ order = user.orders.create!(status: :pending)
51
+ create_line_items(order, items)
52
+ reserve_inventory(items)
53
+ end
54
+
55
+ OrderMailer.confirmation(order).deliver_later
56
+ success(order)
57
+ rescue ActiveRecord::RecordInvalid => e
58
+ failure(e.message, :validation_error)
59
+ end
60
+ end
61
+ end
62
+ ```
63
+
64
+ ### 2. Query Services (Read Operations)
65
+
66
+ Complex reads that don't fit in Query Objects:
67
+
68
+ ```ruby
69
+ # app/services/reports/generate_service.rb
70
+ module Reports
71
+ class GenerateService
72
+ def call(account:, date_range:, format:)
73
+ data = gather_data(account, date_range)
74
+ formatted = format_data(data, format)
75
+ success(formatted)
76
+ end
77
+
78
+ private
79
+
80
+ def gather_data(account, range)
81
+ {
82
+ events: EventStatsQuery.new(account: account).call(range),
83
+ revenue: RevenueQuery.new(account: account).call(range),
84
+ leads: LeadConversionQuery.new(account: account).call(range)
85
+ }
86
+ end
87
+ end
88
+ end
89
+ ```
90
+
91
+ ### 3. Integration Services (External APIs)
92
+
93
+ Wrap external service calls:
94
+
95
+ ```ruby
96
+ # app/services/payments/charge_service.rb
97
+ module Payments
98
+ class ChargeService
99
+ def initialize(gateway: StripeGateway.new)
100
+ @gateway = gateway
101
+ end
102
+
103
+ def call(order:, payment_method_id:)
104
+ charge = gateway.charge(
105
+ amount: order.total_cents,
106
+ currency: "eur",
107
+ payment_method_id: payment_method_id
108
+ )
109
+
110
+ order.update!(
111
+ payment_status: :paid,
112
+ payment_reference: charge.id
113
+ )
114
+
115
+ success(charge)
116
+ rescue PaymentGateway::CardDeclined => e
117
+ failure(e.message, :card_declined)
118
+ rescue PaymentGateway::Error => e
119
+ failure(e.message, :payment_error)
120
+ end
121
+
122
+ private
123
+
124
+ attr_reader :gateway
125
+ end
126
+ end
127
+ ```
128
+
129
+ ### 4. Orchestrator Services (Complex Workflows)
130
+
131
+ Coordinate multiple services:
132
+
133
+ ```ruby
134
+ # app/services/onboarding/complete_service.rb
135
+ module Onboarding
136
+ class CompleteService
137
+ def call(user:, params:)
138
+ results = []
139
+
140
+ results << Accounts::SetupService.new.call(user: user, params: params[:account])
141
+ return results.last if results.last.failure?
142
+
143
+ results << Preferences::ConfigureService.new.call(user: user, params: params[:preferences])
144
+ return results.last if results.last.failure?
145
+
146
+ results << Notifications::WelcomeService.new.call(user: user)
147
+
148
+ user.update!(onboarding_completed_at: Time.current)
149
+ success(user)
150
+ end
151
+ end
152
+ end
153
+ ```
154
+
155
+ ## Dependency Injection Patterns
156
+
157
+ ### Constructor Injection (Preferred)
158
+
159
+ ```ruby
160
+ class OrderService
161
+ def initialize(
162
+ inventory: InventoryService.new,
163
+ payment: PaymentService.new,
164
+ notifier: NotificationService.new
165
+ )
166
+ @inventory = inventory
167
+ @payment = payment
168
+ @notifier = notifier
169
+ end
170
+ end
171
+ ```
172
+
173
+ ### Testing with Mocks
174
+
175
+ ```ruby
176
+ # test/services/orders/create_service_test.rb
177
+ require "test_helper"
178
+
179
+ class Orders::CreateServiceTest < ActiveSupport::TestCase
180
+ setup do
181
+ @inventory = Minitest::Mock.new
182
+ @payment = Minitest::Mock.new
183
+ @service = Orders::CreateService.new(inventory: @inventory, payment: @payment)
184
+ @user = users(:one)
185
+ end
186
+
187
+ test "checks inventory before charging" do
188
+ @inventory.expect :available?, true, [Array]
189
+ @inventory.expect :reserve, true, [Array]
190
+ @payment.expect :charge, true, [Hash]
191
+
192
+ @service.call(user: @user, items: [{ product_id: products(:widget).id, quantity: 1 }])
193
+
194
+ @inventory.verify
195
+ @payment.verify
196
+ end
197
+ end
198
+ ```
199
+
200
+ ## Error Handling Patterns
201
+
202
+ ### Typed Error Codes
203
+
204
+ ```ruby
205
+ module Orders
206
+ class CreateService
207
+ ERROR_CODES = {
208
+ empty_cart: "No items in cart",
209
+ insufficient_inventory: "Item out of stock",
210
+ payment_failed: "Payment could not be processed",
211
+ validation_failed: "Invalid order data"
212
+ }.freeze
213
+
214
+ def call(params)
215
+ return failure(:empty_cart) if params[:items].empty?
216
+ return failure(:insufficient_inventory) unless inventory_available?(params[:items])
217
+
218
+ order = create_order(params)
219
+ success(order)
220
+ rescue PaymentError
221
+ failure(:payment_failed)
222
+ rescue ActiveRecord::RecordInvalid => e
223
+ failure(:validation_failed, e.message)
224
+ end
225
+
226
+ private
227
+
228
+ def failure(code, details = nil)
229
+ message = ERROR_CODES[code]
230
+ message = "#{message}: #{details}" if details
231
+ Result.new(success: false, error: message, code: code)
232
+ end
233
+ end
234
+ end
235
+ ```
236
+
237
+ ### Controller Error Handling
238
+
239
+ ```ruby
240
+ class OrdersController < ApplicationController
241
+ def create
242
+ result = Orders::CreateService.new.call(order_params)
243
+
244
+ if result.success?
245
+ redirect_to result.data, notice: t(".success")
246
+ else
247
+ handle_service_error(result)
248
+ end
249
+ end
250
+
251
+ private
252
+
253
+ def handle_service_error(result)
254
+ case result.code
255
+ when :empty_cart
256
+ redirect_to cart_path, alert: result.error
257
+ when :insufficient_inventory
258
+ flash.now[:alert] = result.error
259
+ render :new, status: :unprocessable_entity
260
+ when :payment_failed
261
+ redirect_to checkout_path, alert: result.error
262
+ else
263
+ flash.now[:alert] = result.error
264
+ render :new, status: :unprocessable_entity
265
+ end
266
+ end
267
+ end
268
+ ```
269
+
270
+ ## Service Naming Conventions
271
+
272
+ | Pattern | Example | Use Case |
273
+ |---------|---------|----------|
274
+ | `VerbNounService` | `CreateOrderService` | Single action |
275
+ | `Namespace::VerbService` | `Orders::CreateService` | Namespaced (preferred) |
276
+ | `NounVerbService` | `OrderCreatorService` | Alternative style |
277
+
278
+ ## Checklist
279
+
280
+ - [ ] Single public method (`#call`)
281
+ - [ ] Returns Result object
282
+ - [ ] Dependencies injected via constructor
283
+ - [ ] Errors caught and wrapped
284
+ - [ ] Transaction for multi-model writes
285
+ - [ ] Typed error codes for handling
286
+ - [ ] Spec covers success and failure paths