seams 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (414) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +335 -0
  3. data/LICENSE +21 -0
  4. data/README.md +104 -0
  5. data/lib/generators/seams/accounts/accounts_generator.rb +272 -0
  6. data/lib/generators/seams/accounts/templates/README.md.tt +219 -0
  7. data/lib/generators/seams/accounts/templates/app/models/account.rb.tt +124 -0
  8. data/lib/generators/seams/accounts/templates/app/models/application_record.rb.tt +7 -0
  9. data/lib/generators/seams/accounts/templates/app/models/current.rb.tt +38 -0
  10. data/lib/generators/seams/accounts/templates/app/models/membership.rb.tt +114 -0
  11. data/lib/generators/seams/accounts/templates/config/routes.rb.tt +8 -0
  12. data/lib/generators/seams/accounts/templates/db/migrate/create_accounts.rb.tt +29 -0
  13. data/lib/generators/seams/accounts/templates/db/migrate/create_accounts_memberships.rb.tt +49 -0
  14. data/lib/generators/seams/accounts/templates/lib/accounts.rb.tt +21 -0
  15. data/lib/generators/seams/accounts/templates/lib/concerns/account_scoped.rb.tt +97 -0
  16. data/lib/generators/seams/accounts/templates/lib/concerns/authorization.rb.tt +105 -0
  17. data/lib/generators/seams/accounts/templates/lib/configuration.rb.tt +26 -0
  18. data/lib/generators/seams/accounts/templates/lib/engine.rb.tt +55 -0
  19. data/lib/generators/seams/accounts/templates/spec/factories/accounts.rb.tt +49 -0
  20. data/lib/generators/seams/accounts/templates/spec/models/accounts/account_spec.rb.tt +64 -0
  21. data/lib/generators/seams/accounts/templates/spec/models/accounts/membership_spec.rb.tt +99 -0
  22. data/lib/generators/seams/accounts/templates/spec/runtime/accounts_boot_spec.rb.tt +181 -0
  23. data/lib/generators/seams/admin/admin_generator.rb +852 -0
  24. data/lib/generators/seams/admin/templates/README.md.tt +266 -0
  25. data/lib/generators/seams/admin/templates/app/controllers/admin/accounts_controller.rb.tt +16 -0
  26. data/lib/generators/seams/admin/templates/app/controllers/admin/accounts_memberships_controller.rb.tt +16 -0
  27. data/lib/generators/seams/admin/templates/app/controllers/admin/application_controller.rb.tt +282 -0
  28. data/lib/generators/seams/admin/templates/app/controllers/admin/identities_controller.rb.tt +26 -0
  29. data/lib/generators/seams/admin/templates/app/controllers/admin/invitations_controller.rb.tt +14 -0
  30. data/lib/generators/seams/admin/templates/app/controllers/admin/invoices_controller.rb.tt +14 -0
  31. data/lib/generators/seams/admin/templates/app/controllers/admin/lifetime_passes_controller.rb.tt +14 -0
  32. data/lib/generators/seams/admin/templates/app/controllers/admin/notification_preferences_controller.rb.tt +15 -0
  33. data/lib/generators/seams/admin/templates/app/controllers/admin/notifications_controller.rb.tt +18 -0
  34. data/lib/generators/seams/admin/templates/app/controllers/admin/plans_controller.rb.tt +14 -0
  35. data/lib/generators/seams/admin/templates/app/controllers/admin/subscriptions_controller.rb.tt +14 -0
  36. data/lib/generators/seams/admin/templates/app/controllers/admin/teams_controller.rb.tt +14 -0
  37. data/lib/generators/seams/admin/templates/app/controllers/admin/teams_memberships_controller.rb.tt +16 -0
  38. data/lib/generators/seams/admin/templates/app/dashboards/admin/account_dashboard.rb.tt +50 -0
  39. data/lib/generators/seams/admin/templates/app/dashboards/admin/accounts_membership_dashboard.rb.tt +58 -0
  40. data/lib/generators/seams/admin/templates/app/dashboards/admin/identity_dashboard.rb.tt +48 -0
  41. data/lib/generators/seams/admin/templates/app/dashboards/admin/invitation_dashboard.rb.tt +51 -0
  42. data/lib/generators/seams/admin/templates/app/dashboards/admin/invoice_dashboard.rb.tt +67 -0
  43. data/lib/generators/seams/admin/templates/app/dashboards/admin/lifetime_pass_dashboard.rb.tt +65 -0
  44. data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_dashboard.rb.tt +58 -0
  45. data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_preference_dashboard.rb.tt +43 -0
  46. data/lib/generators/seams/admin/templates/app/dashboards/admin/plan_dashboard.rb.tt +72 -0
  47. data/lib/generators/seams/admin/templates/app/dashboards/admin/subscription_dashboard.rb.tt +59 -0
  48. data/lib/generators/seams/admin/templates/app/dashboards/admin/team_dashboard.rb.tt +39 -0
  49. data/lib/generators/seams/admin/templates/app/dashboards/admin/teams_membership_dashboard.rb.tt +43 -0
  50. data/lib/generators/seams/admin/templates/app/policies/admin/platform/account_policy.rb.tt +10 -0
  51. data/lib/generators/seams/admin/templates/app/policies/admin/platform/accounts_membership_policy.rb.tt +10 -0
  52. data/lib/generators/seams/admin/templates/app/policies/admin/platform/application_policy.rb.tt +85 -0
  53. data/lib/generators/seams/admin/templates/app/policies/admin/platform/identity_policy.rb.tt +18 -0
  54. data/lib/generators/seams/admin/templates/app/policies/admin/platform/invitation_policy.rb.tt +9 -0
  55. data/lib/generators/seams/admin/templates/app/policies/admin/platform/invoice_policy.rb.tt +9 -0
  56. data/lib/generators/seams/admin/templates/app/policies/admin/platform/lifetime_pass_policy.rb.tt +9 -0
  57. data/lib/generators/seams/admin/templates/app/policies/admin/platform/notification_policy.rb.tt +9 -0
  58. data/lib/generators/seams/admin/templates/app/policies/admin/platform/notification_preference_policy.rb.tt +9 -0
  59. data/lib/generators/seams/admin/templates/app/policies/admin/platform/plan_policy.rb.tt +11 -0
  60. data/lib/generators/seams/admin/templates/app/policies/admin/platform/subscription_policy.rb.tt +9 -0
  61. data/lib/generators/seams/admin/templates/app/policies/admin/platform/team_policy.rb.tt +9 -0
  62. data/lib/generators/seams/admin/templates/app/policies/admin/platform/teams_membership_policy.rb.tt +9 -0
  63. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/account_policy.rb.tt +33 -0
  64. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/accounts_membership_policy.rb.tt +24 -0
  65. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/application_policy.rb.tt +169 -0
  66. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/identity_policy.rb.tt +67 -0
  67. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invitation_policy.rb.tt +24 -0
  68. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invoice_policy.rb.tt +21 -0
  69. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/lifetime_pass_policy.rb.tt +21 -0
  70. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_policy.rb.tt +25 -0
  71. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_preference_policy.rb.tt +23 -0
  72. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/plan_policy.rb.tt +47 -0
  73. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/subscription_policy.rb.tt +22 -0
  74. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/team_policy.rb.tt +28 -0
  75. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/teams_membership_policy.rb.tt +24 -0
  76. data/lib/generators/seams/admin/templates/config/routes.rb.tt +38 -0
  77. data/lib/generators/seams/admin/templates/lib/admin.rb.tt +36 -0
  78. data/lib/generators/seams/admin/templates/lib/concerns/authenticator.rb.tt +66 -0
  79. data/lib/generators/seams/admin/templates/lib/configuration.rb.tt +90 -0
  80. data/lib/generators/seams/admin/templates/lib/context.rb.tt +44 -0
  81. data/lib/generators/seams/admin/templates/lib/engine.rb.tt +68 -0
  82. data/lib/generators/seams/admin/templates/spec/factories/admin.rb.tt +10 -0
  83. data/lib/generators/seams/admin/templates/spec/runtime/admin_boot_spec.rb.tt +604 -0
  84. data/lib/generators/seams/auth/add_oauth_provider/add_oauth_provider_generator.rb +157 -0
  85. data/lib/generators/seams/auth/add_oauth_provider/templates/adapter.rb.tt +95 -0
  86. data/lib/generators/seams/auth/add_oauth_provider/templates/adapter_spec.rb.tt +58 -0
  87. data/lib/generators/seams/auth/auth_generator.rb +311 -0
  88. data/lib/generators/seams/auth/templates/README.md.tt +289 -0
  89. data/lib/generators/seams/auth/templates/app/controllers/oauth/callbacks_controller.rb.tt +80 -0
  90. data/lib/generators/seams/auth/templates/app/controllers/password_resets_controller.rb.tt +44 -0
  91. data/lib/generators/seams/auth/templates/app/controllers/registrations_controller.rb.tt +34 -0
  92. data/lib/generators/seams/auth/templates/app/controllers/sessions_controller.rb.tt +49 -0
  93. data/lib/generators/seams/auth/templates/app/jobs/application_job.rb.tt +7 -0
  94. data/lib/generators/seams/auth/templates/app/jobs/cleanup_expired_sessions_job.rb.tt +30 -0
  95. data/lib/generators/seams/auth/templates/app/mailers/passwords_mailer.rb.tt +15 -0
  96. data/lib/generators/seams/auth/templates/app/models/api_token.rb.tt +62 -0
  97. data/lib/generators/seams/auth/templates/app/models/application_record.rb.tt +7 -0
  98. data/lib/generators/seams/auth/templates/app/models/current.rb.tt +15 -0
  99. data/lib/generators/seams/auth/templates/app/models/identity.rb.tt +74 -0
  100. data/lib/generators/seams/auth/templates/app/models/oauth/provider.rb.tt +48 -0
  101. data/lib/generators/seams/auth/templates/app/models/session.rb.tt +28 -0
  102. data/lib/generators/seams/auth/templates/app/services/authenticate_identity.rb.tt +31 -0
  103. data/lib/generators/seams/auth/templates/app/services/generate_api_token.rb.tt +35 -0
  104. data/lib/generators/seams/auth/templates/app/services/oauth/authenticator.rb.tt +94 -0
  105. data/lib/generators/seams/auth/templates/app/services/register_identity.rb.tt +57 -0
  106. data/lib/generators/seams/auth/templates/app/services/reset_password.rb.tt +41 -0
  107. data/lib/generators/seams/auth/templates/app/services/revoke_api_token.rb.tt +38 -0
  108. data/lib/generators/seams/auth/templates/app/views/password_resets/edit.html.erb.tt +12 -0
  109. data/lib/generators/seams/auth/templates/app/views/password_resets/new.html.erb.tt +11 -0
  110. data/lib/generators/seams/auth/templates/app/views/passwords_mailer/reset_email.html.erb.tt +7 -0
  111. data/lib/generators/seams/auth/templates/app/views/registrations/new.html.erb.tt +26 -0
  112. data/lib/generators/seams/auth/templates/app/views/sessions/_oauth_buttons.html.erb.tt +18 -0
  113. data/lib/generators/seams/auth/templates/app/views/sessions/new.html.erb.tt +17 -0
  114. data/lib/generators/seams/auth/templates/config/routes.rb.tt +21 -0
  115. data/lib/generators/seams/auth/templates/db/migrate/create_auth_api_tokens.rb.tt +26 -0
  116. data/lib/generators/seams/auth/templates/db/migrate/create_auth_identities.rb.tt +29 -0
  117. data/lib/generators/seams/auth/templates/db/migrate/create_auth_oauth_providers.rb.tt +35 -0
  118. data/lib/generators/seams/auth/templates/db/migrate/create_auth_sessions.rb.tt +19 -0
  119. data/lib/generators/seams/auth/templates/lib/auth.rb.tt +39 -0
  120. data/lib/generators/seams/auth/templates/lib/concerns/api_authenticatable.rb.tt +58 -0
  121. data/lib/generators/seams/auth/templates/lib/concerns/authenticatable.rb.tt +32 -0
  122. data/lib/generators/seams/auth/templates/lib/concerns/authentication.rb.tt +60 -0
  123. data/lib/generators/seams/auth/templates/lib/configuration.rb.tt +45 -0
  124. data/lib/generators/seams/auth/templates/lib/engine.rb.tt +46 -0
  125. data/lib/generators/seams/auth/templates/lib/oauth/abstract.rb.tt +87 -0
  126. data/lib/generators/seams/auth/templates/lib/oauth/github.rb.tt +112 -0
  127. data/lib/generators/seams/auth/templates/lib/oauth/google.rb.tt +78 -0
  128. data/lib/generators/seams/auth/templates/lib/tasks/auth_pii.rake.tt +68 -0
  129. data/lib/generators/seams/auth/templates/spec/factories/auth.rb.tt +38 -0
  130. data/lib/generators/seams/auth/templates/spec/mailers/passwords_mailer_spec.rb.tt +37 -0
  131. data/lib/generators/seams/auth/templates/spec/models/api_token_spec.rb.tt +84 -0
  132. data/lib/generators/seams/auth/templates/spec/models/identity_spec.rb.tt +56 -0
  133. data/lib/generators/seams/auth/templates/spec/models/oauth/provider_spec.rb.tt +64 -0
  134. data/lib/generators/seams/auth/templates/spec/models/session_spec.rb.tt +34 -0
  135. data/lib/generators/seams/auth/templates/spec/runtime/boot_spec.rb.tt +30 -0
  136. data/lib/generators/seams/auth/templates/spec/runtime/event_payload_spec.rb.tt +29 -0
  137. data/lib/generators/seams/auth/templates/spec/runtime/login_flow_spec.rb.tt +45 -0
  138. data/lib/generators/seams/billing/billing_generator.rb +476 -0
  139. data/lib/generators/seams/billing/templates/README.md.tt +355 -0
  140. data/lib/generators/seams/billing/templates/app/controllers/admin/lifetime_passes_controller.rb.tt +84 -0
  141. data/lib/generators/seams/billing/templates/app/controllers/checkout_controller.rb.tt +92 -0
  142. data/lib/generators/seams/billing/templates/app/controllers/invoices_controller.rb.tt +63 -0
  143. data/lib/generators/seams/billing/templates/app/controllers/plans_controller.rb.tt +14 -0
  144. data/lib/generators/seams/billing/templates/app/controllers/portal_controller.rb.tt +45 -0
  145. data/lib/generators/seams/billing/templates/app/controllers/subscriptions_controller.rb.tt +119 -0
  146. data/lib/generators/seams/billing/templates/app/controllers/webhooks_controller.rb.tt +98 -0
  147. data/lib/generators/seams/billing/templates/app/helpers/currency_helper.rb.tt +44 -0
  148. data/lib/generators/seams/billing/templates/app/jobs/application_job.rb.tt +6 -0
  149. data/lib/generators/seams/billing/templates/app/jobs/cancel_subscription_job.rb.tt +39 -0
  150. data/lib/generators/seams/billing/templates/app/jobs/start_subscription_job.rb.tt +32 -0
  151. data/lib/generators/seams/billing/templates/app/jobs/webhooks/process_event_job.rb.tt +37 -0
  152. data/lib/generators/seams/billing/templates/app/models/application_record.rb.tt +7 -0
  153. data/lib/generators/seams/billing/templates/app/models/invoice.rb.tt +35 -0
  154. data/lib/generators/seams/billing/templates/app/models/lifetime_pass.rb.tt +60 -0
  155. data/lib/generators/seams/billing/templates/app/models/plan.rb.tt +95 -0
  156. data/lib/generators/seams/billing/templates/app/models/subscription.rb.tt +31 -0
  157. data/lib/generators/seams/billing/templates/app/models/webhook_event.rb.tt +13 -0
  158. data/lib/generators/seams/billing/templates/app/services/checkout_session_service.rb.tt +25 -0
  159. data/lib/generators/seams/billing/templates/app/services/customers/find_or_create_service.rb.tt +73 -0
  160. data/lib/generators/seams/billing/templates/app/services/invoices/sync_service.rb.tt +50 -0
  161. data/lib/generators/seams/billing/templates/app/services/lifetime/create_lifetime_session_service.rb.tt +82 -0
  162. data/lib/generators/seams/billing/templates/app/services/lifetime/create_pass_from_checkout_service.rb.tt +88 -0
  163. data/lib/generators/seams/billing/templates/app/services/lifetime/grant_pass_service.rb.tt +80 -0
  164. data/lib/generators/seams/billing/templates/app/services/lifetime/revoke_pass_service.rb.tt +59 -0
  165. data/lib/generators/seams/billing/templates/app/services/portal_session_service.rb.tt +23 -0
  166. data/lib/generators/seams/billing/templates/app/services/service_result.rb.tt +38 -0
  167. data/lib/generators/seams/billing/templates/app/services/stripe_service.rb.tt +67 -0
  168. data/lib/generators/seams/billing/templates/app/services/subscriptions/cancel_service.rb.tt +42 -0
  169. data/lib/generators/seams/billing/templates/app/services/subscriptions/change_plan_service.rb.tt +48 -0
  170. data/lib/generators/seams/billing/templates/app/services/subscriptions/reactivate_service.rb.tt +28 -0
  171. data/lib/generators/seams/billing/templates/app/services/webhooks/event_router.rb.tt +54 -0
  172. data/lib/generators/seams/billing/templates/app/services/webhooks/handler.rb.tt +93 -0
  173. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/charge_refunded_handler.rb.tt +18 -0
  174. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/checkout_session_completed_handler.rb.tt +58 -0
  175. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_created_handler.rb.tt +16 -0
  176. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_finalized_handler.rb.tt +14 -0
  177. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_handler_base.rb.tt +80 -0
  178. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_paid_handler.rb.tt +12 -0
  179. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_payment_failed_handler.rb.tt +12 -0
  180. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_voided_handler.rb.tt +12 -0
  181. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/payment_failed_handler.rb.tt +15 -0
  182. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/payment_succeeded_handler.rb.tt +19 -0
  183. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_created_handler.rb.tt +11 -0
  184. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_deleted_handler.rb.tt +15 -0
  185. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_handler_base.rb.tt +92 -0
  186. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_trial_will_end_handler.rb.tt +15 -0
  187. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_updated_handler.rb.tt +11 -0
  188. data/lib/generators/seams/billing/templates/app/views/admin/lifetime_passes/index.html.erb.tt +36 -0
  189. data/lib/generators/seams/billing/templates/app/views/admin/lifetime_passes/new.html.erb.tt +37 -0
  190. data/lib/generators/seams/billing/templates/app/views/checkout/success.html.erb.tt +5 -0
  191. data/lib/generators/seams/billing/templates/app/views/invoices/index.html.erb.tt +22 -0
  192. data/lib/generators/seams/billing/templates/app/views/invoices/show.html.erb.tt +14 -0
  193. data/lib/generators/seams/billing/templates/app/views/plans/index.html.erb.tt +51 -0
  194. data/lib/generators/seams/billing/templates/app/views/subscriptions/index.html.erb.tt +16 -0
  195. data/lib/generators/seams/billing/templates/app/views/subscriptions/show.html.erb.tt +25 -0
  196. data/lib/generators/seams/billing/templates/config/routes.rb.tt +39 -0
  197. data/lib/generators/seams/billing/templates/db/migrate/create_billing_invoices.rb.tt +32 -0
  198. data/lib/generators/seams/billing/templates/db/migrate/create_billing_lifetime_passes.rb.tt +43 -0
  199. data/lib/generators/seams/billing/templates/db/migrate/create_billing_plans.rb.tt +31 -0
  200. data/lib/generators/seams/billing/templates/db/migrate/create_billing_subscriptions.rb.tt +33 -0
  201. data/lib/generators/seams/billing/templates/db/migrate/create_billing_webhook_events.rb.tt +24 -0
  202. data/lib/generators/seams/billing/templates/lib/billing.rb.tt +34 -0
  203. data/lib/generators/seams/billing/templates/lib/concerns/billable.rb.tt +100 -0
  204. data/lib/generators/seams/billing/templates/lib/configuration.rb.tt +52 -0
  205. data/lib/generators/seams/billing/templates/lib/engine.rb.tt +72 -0
  206. data/lib/generators/seams/billing/templates/lib/gateways/abstract.rb.tt +65 -0
  207. data/lib/generators/seams/billing/templates/lib/gateways/adyen.rb.tt +16 -0
  208. data/lib/generators/seams/billing/templates/lib/gateways/paddle.rb.tt +22 -0
  209. data/lib/generators/seams/billing/templates/lib/gateways/stripe.rb.tt +155 -0
  210. data/lib/generators/seams/billing/templates/lib/stripe/client.rb.tt +101 -0
  211. data/lib/generators/seams/billing/templates/lib/stripe/webhook_signature.rb.tt +43 -0
  212. data/lib/generators/seams/billing/templates/lib/tasks/billing_check.rake.tt +34 -0
  213. data/lib/generators/seams/billing/templates/spec/factories/billing.rb.tt +65 -0
  214. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/charge_refunded.json.tt +19 -0
  215. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/checkout_session_completed.json.tt +17 -0
  216. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_created.json.tt +25 -0
  217. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_deleted.json.tt +17 -0
  218. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_trial_will_end.json.tt +17 -0
  219. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_updated.json.tt +28 -0
  220. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_created.json.tt +18 -0
  221. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_finalized.json.tt +18 -0
  222. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_paid.json.tt +19 -0
  223. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_payment_failed.json.tt +20 -0
  224. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_voided.json.tt +18 -0
  225. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_payment_failed.json.tt +21 -0
  226. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_succeeded.json.tt +17 -0
  227. data/lib/generators/seams/billing/templates/spec/gateways/contract_spec.rb.tt +11 -0
  228. data/lib/generators/seams/billing/templates/spec/gateways/stripe_spec.rb.tt +53 -0
  229. data/lib/generators/seams/billing/templates/spec/models/plan_spec.rb.tt +81 -0
  230. data/lib/generators/seams/billing/templates/spec/models/subscription_spec.rb.tt +43 -0
  231. data/lib/generators/seams/billing/templates/spec/runtime/boot_spec.rb.tt +38 -0
  232. data/lib/generators/seams/billing/templates/spec/runtime/webhook_handlers_spec.rb.tt +382 -0
  233. data/lib/generators/seams/billing/templates/spec/support/shared_examples/a_billing_gateway.rb.tt +100 -0
  234. data/lib/generators/seams/billing/templates/spec/support/stripe_helpers.rb.tt +59 -0
  235. data/lib/generators/seams/core/core_generator.rb +191 -0
  236. data/lib/generators/seams/core/templates/README.md.tt +45 -0
  237. data/lib/generators/seams/core/templates/app/controllers/concerns/has_current_attributes.rb.tt +71 -0
  238. data/lib/generators/seams/core/templates/app/models/application_record.rb.tt +7 -0
  239. data/lib/generators/seams/core/templates/app/models/audit_log.rb.tt +19 -0
  240. data/lib/generators/seams/core/templates/app/models/concerns/auditable.rb.tt +64 -0
  241. data/lib/generators/seams/core/templates/app/models/concerns/sluggable.rb.tt +53 -0
  242. data/lib/generators/seams/core/templates/app/models/concerns/soft_deletable.rb.tt +37 -0
  243. data/lib/generators/seams/core/templates/app/models/concerns/tenant_scoped.rb.tt +39 -0
  244. data/lib/generators/seams/core/templates/app/models/current.rb.tt +16 -0
  245. data/lib/generators/seams/core/templates/app/services/event_publisher.rb.tt +23 -0
  246. data/lib/generators/seams/core/templates/app/validators/email_format_validator.rb.tt +21 -0
  247. data/lib/generators/seams/core/templates/db/migrate/create_core_audit_logs.rb.tt +29 -0
  248. data/lib/generators/seams/core/templates/lib/core.rb.tt +8 -0
  249. data/lib/generators/seams/core/templates/lib/engine.rb.tt +28 -0
  250. data/lib/generators/seams/core/templates/spec/concerns/auditable_spec.rb.tt +39 -0
  251. data/lib/generators/seams/core/templates/spec/concerns/sluggable_spec.rb.tt +29 -0
  252. data/lib/generators/seams/core/templates/spec/models/audit_log_spec.rb.tt +22 -0
  253. data/lib/generators/seams/core/templates/spec/runtime/boot_spec.rb.tt +25 -0
  254. data/lib/generators/seams/core/templates/spec/validators/email_format_validator_spec.rb.tt +29 -0
  255. data/lib/generators/seams/engine/engine_generator.rb +165 -0
  256. data/lib/generators/seams/engine/templates/Gemfile.tt +19 -0
  257. data/lib/generators/seams/engine/templates/LICENSE.tt +21 -0
  258. data/lib/generators/seams/engine/templates/README.md.tt +40 -0
  259. data/lib/generators/seams/engine/templates/Rakefile.tt +14 -0
  260. data/lib/generators/seams/engine/templates/app/application_controller.rb.tt +6 -0
  261. data/lib/generators/seams/engine/templates/app/application_record.rb.tt +16 -0
  262. data/lib/generators/seams/engine/templates/config/locales/en.yml.tt +14 -0
  263. data/lib/generators/seams/engine/templates/config/routes.rb.tt +4 -0
  264. data/lib/generators/seams/engine/templates/gemspec.tt +20 -0
  265. data/lib/generators/seams/engine/templates/host_initializer.rb.tt +13 -0
  266. data/lib/generators/seams/engine/templates/lib/engine.rb.tt +27 -0
  267. data/lib/generators/seams/engine/templates/lib/root.rb.tt +7 -0
  268. data/lib/generators/seams/engine/templates/lib/version.rb.tt +5 -0
  269. data/lib/generators/seams/engine/templates/rubocop.yml.tt +55 -0
  270. data/lib/generators/seams/engine/templates/spec/example_spec.rb.tt +16 -0
  271. data/lib/generators/seams/engine/templates/spec/spec_helper.rb.tt +23 -0
  272. data/lib/generators/seams/install/install_generator.rb +211 -0
  273. data/lib/generators/seams/install/templates/Dockerfile.tt +52 -0
  274. data/lib/generators/seams/install/templates/Procfile.tt +14 -0
  275. data/lib/generators/seams/install/templates/bin_seams.tt +107 -0
  276. data/lib/generators/seams/install/templates/ci.yml.tt +123 -0
  277. data/lib/generators/seams/install/templates/deploy.yml.tt +63 -0
  278. data/lib/generators/seams/install/templates/doc/ARCHITECTURE.md.tt +86 -0
  279. data/lib/generators/seams/install/templates/docker-entrypoint.tt +27 -0
  280. data/lib/generators/seams/install/templates/rubocop.yml.tt +33 -0
  281. data/lib/generators/seams/install/templates/ruby-version.tt +1 -0
  282. data/lib/generators/seams/install/templates/script/collate_coverage.rb.tt +33 -0
  283. data/lib/generators/seams/install/templates/script/run_affected_tests.sh.tt +64 -0
  284. data/lib/generators/seams/install/templates/seams.rake.tt +65 -0
  285. data/lib/generators/seams/install/templates/seams.rb.tt +9 -0
  286. data/lib/generators/seams/install/templates/seams_engines.rb.tt +15 -0
  287. data/lib/generators/seams/notifications/notifications_generator.rb +395 -0
  288. data/lib/generators/seams/notifications/templates/README.md.tt +269 -0
  289. data/lib/generators/seams/notifications/templates/app/channels/notification_channel.rb.tt +36 -0
  290. data/lib/generators/seams/notifications/templates/app/controllers/notifications_controller.rb.tt +58 -0
  291. data/lib/generators/seams/notifications/templates/app/controllers/preferences_controller.rb.tt +54 -0
  292. data/lib/generators/seams/notifications/templates/app/javascript/controllers/notification_bell_controller.js.tt +34 -0
  293. data/lib/generators/seams/notifications/templates/app/jobs/application_job.rb.tt +6 -0
  294. data/lib/generators/seams/notifications/templates/app/jobs/create_notification_job.rb.tt +31 -0
  295. data/lib/generators/seams/notifications/templates/app/jobs/send_due_notifications_job.rb.tt +22 -0
  296. data/lib/generators/seams/notifications/templates/app/jobs/send_notification_job.rb.tt +13 -0
  297. data/lib/generators/seams/notifications/templates/app/mailers/application_mailer.rb.tt +12 -0
  298. data/lib/generators/seams/notifications/templates/app/mailers/notification_mailer.rb.tt +23 -0
  299. data/lib/generators/seams/notifications/templates/app/models/application_record.rb.tt +7 -0
  300. data/lib/generators/seams/notifications/templates/app/models/delivery.rb.tt +13 -0
  301. data/lib/generators/seams/notifications/templates/app/models/notification.rb.tt +218 -0
  302. data/lib/generators/seams/notifications/templates/app/models/notification_preference.rb.tt +29 -0
  303. data/lib/generators/seams/notifications/templates/app/models/strategies/email.rb.tt +38 -0
  304. data/lib/generators/seams/notifications/templates/app/models/strategies/in_app.rb.tt +26 -0
  305. data/lib/generators/seams/notifications/templates/app/models/strategies/sms.rb.tt +33 -0
  306. data/lib/generators/seams/notifications/templates/app/subscribers/auth_subscriber.rb.tt +71 -0
  307. data/lib/generators/seams/notifications/templates/app/subscribers/billing_subscriber.rb.tt +127 -0
  308. data/lib/generators/seams/notifications/templates/app/views/layouts/notifications/mailer.html.erb.tt +22 -0
  309. data/lib/generators/seams/notifications/templates/app/views/layouts/notifications/mailer.text.erb.tt +4 -0
  310. data/lib/generators/seams/notifications/templates/app/views/notifications/_bell.html.erb.tt +15 -0
  311. data/lib/generators/seams/notifications/templates/app/views/notifications/index.html.erb.tt +15 -0
  312. data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_failed.html.erb.tt +4 -0
  313. data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_failed.text.erb.tt +4 -0
  314. data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_paid.html.erb.tt +3 -0
  315. data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_paid.text.erb.tt +3 -0
  316. data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_granted.html.erb.tt +5 -0
  317. data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_granted.text.erb.tt +5 -0
  318. data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_purchased.html.erb.tt +5 -0
  319. data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_purchased.text.erb.tt +5 -0
  320. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_canceled.html.erb.tt +4 -0
  321. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_canceled.text.erb.tt +4 -0
  322. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_started.html.erb.tt +4 -0
  323. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_started.text.erb.tt +5 -0
  324. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_updated.html.erb.tt +3 -0
  325. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_updated.text.erb.tt +3 -0
  326. data/lib/generators/seams/notifications/templates/app/views/templates/default.html.erb.tt +10 -0
  327. data/lib/generators/seams/notifications/templates/app/views/templates/default.text.erb.tt +11 -0
  328. data/lib/generators/seams/notifications/templates/app/views/templates/welcome.html.erb.tt +6 -0
  329. data/lib/generators/seams/notifications/templates/app/views/templates/welcome.text.erb.tt +6 -0
  330. data/lib/generators/seams/notifications/templates/config/initializers/notifications.rb.tt +58 -0
  331. data/lib/generators/seams/notifications/templates/config/routes.rb.tt +17 -0
  332. data/lib/generators/seams/notifications/templates/db/migrate/create_notification_deliveries.rb.tt +16 -0
  333. data/lib/generators/seams/notifications/templates/db/migrate/create_notification_preferences.rb.tt +25 -0
  334. data/lib/generators/seams/notifications/templates/db/migrate/create_notifications.rb.tt +35 -0
  335. data/lib/generators/seams/notifications/templates/lib/adapters/abstract.rb.tt +20 -0
  336. data/lib/generators/seams/notifications/templates/lib/adapters/action_mailer.rb.tt +17 -0
  337. data/lib/generators/seams/notifications/templates/lib/adapters/null_sms.rb.tt +23 -0
  338. data/lib/generators/seams/notifications/templates/lib/concerns/notifiable.rb.tt +135 -0
  339. data/lib/generators/seams/notifications/templates/lib/configuration.rb.tt +24 -0
  340. data/lib/generators/seams/notifications/templates/lib/engine.rb.tt +35 -0
  341. data/lib/generators/seams/notifications/templates/lib/notifications.rb.tt +75 -0
  342. data/lib/generators/seams/notifications/templates/lib/type_registry.rb.tt +74 -0
  343. data/lib/generators/seams/notifications/templates/spec/factories/notifications.rb.tt +53 -0
  344. data/lib/generators/seams/notifications/templates/spec/models/delivery_spec.rb.tt +28 -0
  345. data/lib/generators/seams/notifications/templates/spec/models/notification_preference_spec.rb.tt +46 -0
  346. data/lib/generators/seams/notifications/templates/spec/models/notification_spec.rb.tt +60 -0
  347. data/lib/generators/seams/notifications/templates/spec/runtime/bell_broadcast_spec.rb.tt +59 -0
  348. data/lib/generators/seams/notifications/templates/spec/runtime/billing_subscriber_skip_spec.rb.tt +87 -0
  349. data/lib/generators/seams/notifications/templates/spec/runtime/boot_spec.rb.tt +39 -0
  350. data/lib/generators/seams/notifications/templates/spec/runtime/schedule_round_trip_spec.rb.tt +55 -0
  351. data/lib/generators/seams/remove/remove_generator.rb +259 -0
  352. data/lib/generators/seams/teams/teams_generator.rb +298 -0
  353. data/lib/generators/seams/teams/templates/README.md.tt +88 -0
  354. data/lib/generators/seams/teams/templates/app/controllers/invitations_controller.rb.tt +102 -0
  355. data/lib/generators/seams/teams/templates/app/controllers/memberships_controller.rb.tt +54 -0
  356. data/lib/generators/seams/teams/templates/app/controllers/teams_controller.rb.tt +68 -0
  357. data/lib/generators/seams/teams/templates/app/jobs/application_job.rb.tt +6 -0
  358. data/lib/generators/seams/teams/templates/app/mailers/invitation_mailer.rb.tt +34 -0
  359. data/lib/generators/seams/teams/templates/app/models/application_record.rb.tt +7 -0
  360. data/lib/generators/seams/teams/templates/app/models/current.rb.tt +30 -0
  361. data/lib/generators/seams/teams/templates/app/models/invitation.rb.tt +36 -0
  362. data/lib/generators/seams/teams/templates/app/models/membership.rb.tt +36 -0
  363. data/lib/generators/seams/teams/templates/app/models/team.rb.tt +32 -0
  364. data/lib/generators/seams/teams/templates/app/subscribers/invitation_subscriber.rb.tt +36 -0
  365. data/lib/generators/seams/teams/templates/app/views/invitation_mailer/invite.text.erb.tt +8 -0
  366. data/lib/generators/seams/teams/templates/app/views/invitations/index.html.erb.tt +44 -0
  367. data/lib/generators/seams/teams/templates/app/views/memberships/index.html.erb.tt +32 -0
  368. data/lib/generators/seams/teams/templates/app/views/teams/edit.html.erb.tt +28 -0
  369. data/lib/generators/seams/teams/templates/app/views/teams/index.html.erb.tt +15 -0
  370. data/lib/generators/seams/teams/templates/app/views/teams/new.html.erb.tt +24 -0
  371. data/lib/generators/seams/teams/templates/app/views/teams/show.html.erb.tt +17 -0
  372. data/lib/generators/seams/teams/templates/config/routes.rb.tt +19 -0
  373. data/lib/generators/seams/teams/templates/db/migrate/create_team_invitations.rb.tt +24 -0
  374. data/lib/generators/seams/teams/templates/db/migrate/create_team_memberships.rb.tt +25 -0
  375. data/lib/generators/seams/teams/templates/db/migrate/create_teams.rb.tt +18 -0
  376. data/lib/generators/seams/teams/templates/lib/concerns/account_scoped.rb.tt +79 -0
  377. data/lib/generators/seams/teams/templates/lib/concerns/authorization.rb.tt +55 -0
  378. data/lib/generators/seams/teams/templates/lib/configuration.rb.tt +45 -0
  379. data/lib/generators/seams/teams/templates/lib/engine.rb.tt +51 -0
  380. data/lib/generators/seams/teams/templates/lib/teams.rb.tt +22 -0
  381. data/lib/generators/seams/teams/templates/spec/factories/teams.rb.tt +47 -0
  382. data/lib/generators/seams/teams/templates/spec/models/invitation_spec.rb.tt +25 -0
  383. data/lib/generators/seams/teams/templates/spec/models/membership_spec.rb.tt +29 -0
  384. data/lib/generators/seams/teams/templates/spec/models/team_spec.rb.tt +23 -0
  385. data/lib/generators/seams/teams/templates/spec/runtime/boot_spec.rb.tt +32 -0
  386. data/lib/seams/cli/list.rb +111 -0
  387. data/lib/seams/cli/quality.rb +99 -0
  388. data/lib/seams/cli/resolve.rb +276 -0
  389. data/lib/seams/cli/test_changed.rb +116 -0
  390. data/lib/seams/cli.rb +48 -0
  391. data/lib/seams/configuration.rb +19 -0
  392. data/lib/seams/cops/known_queue_names.rb +42 -0
  393. data/lib/seams/cops/migration_comments.rb +68 -0
  394. data/lib/seams/cops/no_cross_engine_dependency.rb +58 -0
  395. data/lib/seams/cops/no_cross_engine_model_access.rb +153 -0
  396. data/lib/seams/cops.rb +18 -0
  397. data/lib/seams/event_registry.rb +49 -0
  398. data/lib/seams/events/adapter.rb +24 -0
  399. data/lib/seams/events/adapters/active_support.rb +31 -0
  400. data/lib/seams/events/publisher.rb +178 -0
  401. data/lib/seams/events.rb +39 -0
  402. data/lib/seams/generators/dummy_app_writer.rb +424 -0
  403. data/lib/seams/generators/eject_aware.rb +102 -0
  404. data/lib/seams/generators/follow_up_generator.rb +148 -0
  405. data/lib/seams/generators/host_injector.rb +124 -0
  406. data/lib/seams/generators/sibling_rubocop_writer.rb +77 -0
  407. data/lib/seams/generators/splicer.rb +217 -0
  408. data/lib/seams/observability/adapter.rb +33 -0
  409. data/lib/seams/observability/adapters/rails_logger.rb +59 -0
  410. data/lib/seams/observability.rb +34 -0
  411. data/lib/seams/runtime.rb +23 -0
  412. data/lib/seams/version.rb +5 -0
  413. data/lib/seams.rb +23 -0
  414. metadata +493 -0
@@ -0,0 +1,269 @@
1
+ # Notifications
2
+
3
+ > In-app + email + SMS notifications, scheduled or recurring, for a
4
+ > Seams-powered host. STI delivery strategies, ice_cube schedules,
5
+ > swappable adapters.
6
+
7
+ **Requires:** soft requirements only.
8
+ - The `auth` engine: `Notifications::AuthSubscriber.owner_class_name`
9
+ resolves the recipient via `Auth::Identity` by default.
10
+ - The `accounts` and/or `billing` engines: the `BillingSubscriber`
11
+ resolves the recipient via `Billing.configuration.billable_class`
12
+ (default `"Accounts::Account"`). Without billing the subscriber
13
+ attaches no handlers; without accounts, set `billable_class` to
14
+ whatever class the host uses for the billing recipient.
15
+
16
+ ## Model
17
+
18
+ `Notifications::Notification` is an STI base. Three concrete
19
+ subclasses, each implementing its own `#dispatch!`:
20
+
21
+ | Class | Channel | Dispatches via |
22
+ | --- | --- | --- |
23
+ | `Notifications::Strategies::InApp` | in-app | ActionCable broadcast to the per-recipient channel |
24
+ | `Notifications::Strategies::Email` | email | `Notifications.email_adapter.deliver(notification:)` |
25
+ | `Notifications::Strategies::Sms` | sms | `Notifications.sms_adapter.deliver(notification:)` |
26
+
27
+ Every Notification belongs to an `owner` (polymorphic), names a
28
+ `template` (an ERB filename), and carries an ice_cube schedule
29
+ serialised to a `schedule_data` jsonb column. `next_delivery_at` is
30
+ the indexed cache the recurring sweeper reads from.
31
+
32
+ ### Polymorphic owner
33
+
34
+ The Notification's `owner` is polymorphic — an `owner_type` /
35
+ `owner_id` pair. After Wave 9 the canonical "human" is
36
+ `Auth::Identity`, but **any** ActiveRecord model can own
37
+ notifications: an Account (the tenant), a Membership (Identity in
38
+ Account), or a host-defined model. Hosts mix and match without any
39
+ schema changes:
40
+
41
+ ```ruby
42
+ Notifications::Notification.create!(owner: identity, template: "welcome")
43
+ Notifications::Notification.create!(owner: account, template: "billing/invoice_paid")
44
+ Notifications::Notification.create!(owner: membership, template: "team/role_changed")
45
+ Notifications::Notification.create!(owner: project, template: "project/deadline")
46
+ ```
47
+
48
+ ## Scheduling
49
+
50
+ The examples below assume the host has included
51
+ `Notifications::Notifiable` on `Auth::Identity` (or another model)
52
+ to pick up the `#notify` helper — see "Notifiable is optional"
53
+ below. Hosts that skip the concern call
54
+ `Notifications::Notification.create!(owner: ..., template: ...)`
55
+ directly.
56
+
57
+ ```ruby
58
+ # Send right now (default if you skip schedule_config)
59
+ identity.notify(strategy: :email, template: "welcome")
60
+
61
+ # Send in 24h
62
+ identity.notify(
63
+ strategy: :email,
64
+ template: "trial_ending",
65
+ schedule_config: { starts_at: 1.day.from_now, frequency: "once" }
66
+ )
67
+
68
+ # Weekly digest
69
+ identity.notify(
70
+ strategy: :email,
71
+ template: "weekly_digest",
72
+ schedule_config: {
73
+ starts_at: Time.current.next_week,
74
+ frequency: "weekly",
75
+ interval: 1
76
+ }
77
+ )
78
+
79
+ # Monthly, capped at 12 occurrences
80
+ identity.notify(
81
+ strategy: :email,
82
+ template: "anniversary",
83
+ schedule_config: {
84
+ starts_at: Time.current,
85
+ frequency: "monthly",
86
+ count: 12
87
+ }
88
+ )
89
+ ```
90
+
91
+ Or assemble the schedule yourself for richer rules (exception dates,
92
+ "first Tuesday of the month", etc.):
93
+
94
+ ```ruby
95
+ sched = IceCube::Schedule.new(Time.current)
96
+ sched.add_recurrence_rule(
97
+ IceCube::Rule.weekly.day(:monday).hour_of_day(9)
98
+ )
99
+ notification = identity.notifications.create!(
100
+ type: "Notifications::Strategies::Email",
101
+ template: "monday_morning"
102
+ )
103
+ notification.schedule = sched
104
+ notification.save!
105
+ ```
106
+
107
+ ## Sweeper
108
+
109
+ `Notifications::SendDueNotificationsJob` is a plain `ApplicationJob`
110
+ that finds every `Notification.due` and enqueues a per-row
111
+ `SendNotificationJob`. Wire it into your queue's recurring
112
+ scheduler. With Rails 8's Solid Queue:
113
+
114
+ ```yaml
115
+ # config/recurring.yml
116
+ production:
117
+ notifications_dispatcher:
118
+ class: Notifications::SendDueNotificationsJob
119
+ schedule: every minute
120
+ ```
121
+
122
+ ## Events emitted
123
+
124
+ | Event name | Payload | Emitted when |
125
+ | --- | --- | --- |
126
+ | `notification.queued.notifications` | `{ id:, type:, owner_type:, owner_id: }` | `Notification#send!` begins |
127
+ | `notification.delivered.notifications` | `{ id:, type:, owner_type:, owner_id: }` | `dispatch!` succeeded + Delivery recorded |
128
+ | `notification.failed.notifications` | `{ id:, type:, error: }` | `dispatch!` raised |
129
+
130
+ Owner reference uses `owner_type` + `owner_id` (the polymorphic
131
+ columns) — Notifications are addressed at any model, so subscribers
132
+ need both halves to resolve a recipient.
133
+
134
+ ## Events consumed
135
+
136
+ | Event name | Subscriber | What it does |
137
+ | --- | --- | --- |
138
+ | `identity.signed_up.auth` | `Notifications::AuthSubscriber` | Creates an InApp + Email welcome notification owned by the Auth::Identity (subject to NotificationPreference). |
139
+ | `subscription.created.billing` | `Notifications::BillingSubscriber` | InApp notification, template `billing/subscription_started`. |
140
+ | `subscription.updated.billing` | `Notifications::BillingSubscriber` | InApp notification, template `billing/subscription_updated`. |
141
+ | `subscription.canceled.billing` | `Notifications::BillingSubscriber` | InApp notification, template `billing/subscription_canceled`. |
142
+ | `invoice.paid.billing` | `Notifications::BillingSubscriber` | InApp notification, template `billing/invoice_paid`. |
143
+ | `invoice.failed.billing` | `Notifications::BillingSubscriber` | InApp notification, template `billing/invoice_failed`. |
144
+ | `lifetime.granted.billing` | `Notifications::BillingSubscriber` | InApp notification, template `billing/lifetime_granted`. |
145
+ | `lifetime.purchased.billing` | `Notifications::BillingSubscriber` | InApp notification, template `billing/lifetime_purchased`. |
146
+
147
+ The Billing subscriber resolves the recipient by reading
148
+ `Billing.configuration.billable_class` (default `Accounts::Account`)
149
+ and looking up the row by `account_id` carried on the billing
150
+ event payload. It only attaches when `Billing::Engine` is loaded.
151
+ Hosts that want a different recipient (e.g. a domain User on top
152
+ of `Auth::Identity`) override `Billing.configuration.billable_class`
153
+ in their initializer.
154
+
155
+ ### Cross-engine dependencies
156
+
157
+ The subscribers in this engine resolve owners by reaching into other
158
+ engines' models. This dependency direction is intentional but worth
159
+ documenting explicitly:
160
+
161
+ | Subscriber | Reaches into | Why |
162
+ | --- | --- | --- |
163
+ | `Notifications::AuthSubscriber` | `Auth::Identity` | resolve owner from `identity_id` payload (the human who just signed up) |
164
+ | `Notifications::BillingSubscriber` | `Billing.configuration.billable_class` (default `Accounts::Account`) | resolve owner from `account_id` payload — the tenant the subscription / invoice / lifetime grant belongs to |
165
+ | `Notifications::CreateNotificationJob` | any AR class | resolve owner via `owner_class.constantize.find_by(id: owner_id)` |
166
+
167
+ If you want a different owner for the welcome notification (an
168
+ Account, a Membership, a host User), copy
169
+ `engines/notifications/app/subscribers/notifications/auth_subscriber.rb`
170
+ into your host, change `OWNER_CLASS_NAME`, then re-attach in
171
+ `config/initializers/notifications.rb`. The Notification table is
172
+ fully polymorphic — no schema changes needed.
173
+
174
+ ### Subscribers enqueue, never block the publisher
175
+
176
+ `Seams::Events::Publisher` runs subscribers synchronously in the
177
+ publisher's thread. Every subscriber here therefore enqueues
178
+ `Notifications::CreateNotificationJob` (which does the actual DB
179
+ write + send) rather than calling `Notification.create!` inline.
180
+ Publishing should never wait on the bus.
181
+
182
+ ## Exposed concerns
183
+
184
+ | Concern | Purpose |
185
+ | --- | --- |
186
+ | `Notifications::Notifiable` | OPTIONAL. Mix into any model that should expose `notifications` + `#notify(strategy:, template:, schedule_config:)` sugar. |
187
+
188
+ ### Notifiable is optional
189
+
190
+ The `Notifications::Notification` row is polymorphic — every record
191
+ has an `owner_type` / `owner_id` pair, and `create!(owner: anything, template: ...)`
192
+ works with any ActiveRecord model. The `Notifiable` concern is just
193
+ sugar for the receiving side; you don't need it.
194
+
195
+ Three include patterns hosts can pick from:
196
+
197
+ 1. **Wire onto `Auth::Identity`** (canonical post-Wave-9 host —
198
+ the "human" is `Auth::Identity`, no host User):
199
+
200
+ ```ruby
201
+ # config/initializers/notifications.rb
202
+ Rails.application.config.to_prepare do
203
+ Auth::Identity.include(Notifications::Notifiable)
204
+ end
205
+ ```
206
+
207
+ 2. **Wire onto a host User class** (hosts that keep their own User
208
+ alongside `Auth::Identity`):
209
+
210
+ ```ruby
211
+ class User < ApplicationRecord
212
+ include Notifications::Notifiable
213
+ end
214
+ ```
215
+
216
+ 3. **Don't include the concern at all.** Use
217
+ `Notifications::Notification.create!(owner: ..., template: ...)`
218
+ directly — the polymorphic owner column accepts any AR record.
219
+
220
+ Hosts including the concern on a non-Identity class (an Account, a
221
+ host User keyed by a different `identity_id`, etc.) should override
222
+ `#notification_preference_identity_id` so preference lookups key off
223
+ the right Identity.
224
+
225
+ ## Adapters
226
+
227
+ | Interface | Default | Override via |
228
+ | --- | --- | --- |
229
+ | Email | `Notifications::Adapters::ActionMailer` | `Notifications.configure { \|c\| c.email_adapter = "MyApp::MailgunAdapter" }` |
230
+ | SMS | `Notifications::Adapters::NullSms` | `Notifications.configure { \|c\| c.sms_adapter = "MyApp::TwilioAdapter" }` |
231
+
232
+ To add a new adapter, subclass `Notifications::Adapters::Abstract`
233
+ and implement `#deliver(notification:)`. The adapter receives the
234
+ full Notification so it can read `recipient`, `template`,
235
+ `rendered_content`, `owner` — whatever the gateway needs.
236
+
237
+ ## Templates
238
+
239
+ Notifications are rendered via ERB files looked up in this order:
240
+
241
+ 1. `app/views/notifications/templates/<name>.erb` in the host
242
+ 2. `app/views/notifications/templates/<name>.erb` in the engine
243
+
244
+ Drop a file in your host to override. The notification is exposed in
245
+ the template via the local variable `notification` — use
246
+ `notification.owner`, `notification.recipient`, etc.
247
+
248
+ ## Preferences
249
+
250
+ `Notifications::NotificationPreference` lets an Identity opt out by
251
+ channel + notification_type. The table keys off `identity_id` (not
252
+ the polymorphic Notification owner) — channel preferences live with
253
+ the human, not with whatever model a notification happens to be
254
+ addressed at:
255
+
256
+ ```ruby
257
+ Notifications::NotificationPreference.enabled?(
258
+ identity_id: 42, channel: "email", notification_type: "weekly_digest"
259
+ ) # => true (default) | false (if a row says enabled: false)
260
+ ```
261
+
262
+ The shipped `AuthSubscriber` consults this before creating the
263
+ Email Notification at signup.
264
+
265
+ ## Running the specs
266
+
267
+ ```bash
268
+ bin/rails seams:test[notifications]
269
+ ```
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notifications
4
+ # Per-recipient ActionCable channel. Server-side code can broadcast
5
+ # a Turbo Stream to this channel when a new notification is created,
6
+ # so the bell icon updates in real time.
7
+ #
8
+ # Notifications::NotificationChannel.broadcast_to(
9
+ # identity, { unread_count: identity.notifications.unread.count }
10
+ # )
11
+ #
12
+ # Post-Wave-9 the canonical recipient is `Auth::Current.identity`.
13
+ # Hosts that keep a domain User on top of Auth::Identity can
14
+ # override `current_recipient` to point at that User instead.
15
+ class NotificationChannel < ActionCable::Channel::Base
16
+ def subscribed
17
+ return reject unless current_recipient
18
+
19
+ stream_for current_recipient
20
+ end
21
+
22
+ private
23
+
24
+ # Resolves the recipient for the WebSocket connection. Tries
25
+ # `connection.current_identity` (the Wave-9 default exposed by
26
+ # `Auth::Authentication`) first, then falls back to
27
+ # `connection.current_user` for hosts that maintain a User model
28
+ # on top of Auth::Identity.
29
+ def current_recipient
30
+ return connection.current_identity if connection.respond_to?(:current_identity)
31
+ return connection.current_user if connection.respond_to?(:current_user)
32
+
33
+ nil
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notifications
4
+ class NotificationsController < ApplicationController
5
+ before_action :set_notification, only: %i[show mark_as_read]
6
+
7
+ def index
8
+ @notifications = current_recipient
9
+ &.notifications
10
+ &.where(type: "Notifications::Strategies::InApp")
11
+ &.recent
12
+ &.limit(50) || []
13
+ end
14
+
15
+ def show
16
+ @notification.mark_as_read!
17
+ end
18
+
19
+ def mark_as_read
20
+ @notification.mark_as_read!
21
+ respond_to do |format|
22
+ format.html { redirect_to notifications_path }
23
+ format.turbo_stream
24
+ end
25
+ end
26
+
27
+ def mark_all_as_read
28
+ current_recipient
29
+ &.notifications
30
+ &.where(type: "Notifications::Strategies::InApp")
31
+ &.unread
32
+ &.update_all(read_at: Time.current)
33
+ redirect_to notifications_path, notice: "All caught up"
34
+ end
35
+
36
+ private
37
+
38
+ def set_notification
39
+ @notification = current_recipient&.notifications&.find(params[:id])
40
+ end
41
+
42
+ # Resolves the recipient whose notifications this controller
43
+ # exposes. Post-Wave-9 the canonical recipient is
44
+ # `Auth::Current.identity` (the signed-in human). Hosts that keep
45
+ # a domain User on top of Auth::Identity can override
46
+ # `current_recipient` (or expose `current_user` from their auth
47
+ # concern) to point at that User instead — the legacy
48
+ # `respond_to?(:current_user)` fallback below preserves Wave-8
49
+ # behaviour for hosts that haven't migrated.
50
+ def current_recipient
51
+ if defined?(Auth::Current) && Auth::Current.respond_to?(:identity) && Auth::Current.identity
52
+ return Auth::Current.identity
53
+ end
54
+
55
+ respond_to?(:current_user) ? current_user : nil
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notifications
4
+ class PreferencesController < ApplicationController
5
+ def show
6
+ @preferences = Notifications::NotificationPreference.where(identity_id: current_identity_id)
7
+ end
8
+
9
+ def update
10
+ # Whitelist the keys we accept. The form posts
11
+ # `preferences[<channel>:<type>] = "1" | "0"`. Allowed channels
12
+ # are the canonical CHANNELS list; type is either "any" (→ nil)
13
+ # or a NotificationPreference::TYPE-style identifier ([a-z0-9_]).
14
+ preference_params.each do |key, enabled|
15
+ channel, type = key.split(":", 2)
16
+ next unless Notifications::NotificationPreference::CHANNELS.include?(channel)
17
+ next unless type.nil? || type == "any" || type.match?(/\A[a-z0-9_]+\z/)
18
+
19
+ type = nil if type == "any"
20
+ pref = Notifications::NotificationPreference.find_or_initialize_by(
21
+ identity_id: current_identity_id,
22
+ channel: channel,
23
+ notification_type: type
24
+ )
25
+ pref.update!(enabled: enabled.to_s == "1")
26
+ end
27
+ redirect_to preferences_path, notice: "Preferences saved"
28
+ end
29
+
30
+ private
31
+
32
+ # `params[:preferences].to_h` raises ActionController::UnfilteredParameters
33
+ # in Rails default config. Use permit! after filtering to the
34
+ # `:preferences` key — the per-key channel/type validation above
35
+ # is the real safety net.
36
+ def preference_params
37
+ raw = params.require(:preferences)
38
+ raw.respond_to?(:permit!) ? raw.permit!.to_h : raw.to_h
39
+ end
40
+
41
+ # Resolves the signed-in identity's id from `Auth::Current.identity`
42
+ # (the Auth engine's per-request namespace). Gated on
43
+ # `defined?(Auth::Current)` so the controller is safe in hosts
44
+ # that don't ship the auth engine. Override in your host if you
45
+ # wire authentication differently.
46
+ def current_identity_id
47
+ if defined?(Auth::Current) && Auth::Current.respond_to?(:identity) && Auth::Current.identity
48
+ return Auth::Current.identity.id
49
+ end
50
+
51
+ nil
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,34 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Notification bell. Subscribes to the per-user ActionCable channel
4
+ // (if available) and updates the unread count badge in real time.
5
+ //
6
+ // Drop-in: add `data-controller="notification-bell"` to the bell
7
+ // element. Pair with `data-notification-bell-target="count"` on the
8
+ // element that shows the unread number.
9
+ export default class extends Controller {
10
+ static targets = ["count"]
11
+ static values = { count: Number }
12
+
13
+ connect() {
14
+ if (typeof window.consumer === "undefined") return
15
+
16
+ this.subscription = window.consumer.subscriptions.create(
17
+ { channel: "Notifications::NotificationChannel" },
18
+ { received: (data) => this.update(data) }
19
+ )
20
+ }
21
+
22
+ disconnect() {
23
+ this.subscription?.unsubscribe()
24
+ }
25
+
26
+ update({ unread_count }) {
27
+ if (typeof unread_count !== "number") return
28
+ this.countValue = unread_count
29
+ if (this.hasCountTarget) {
30
+ this.countTarget.textContent = unread_count
31
+ this.countTarget.style.display = unread_count > 0 ? "" : "none"
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notifications
4
+ class ApplicationJob < ::ApplicationJob
5
+ end
6
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notifications
4
+ # Creates a Notification row and enqueues its send. Subscribers
5
+ # enqueue this job rather than doing the DB write inline — see
6
+ # Seams::Events::Publisher (subscribers run synchronously in the
7
+ # publisher's thread; never block the publisher).
8
+ class CreateNotificationJob < ApplicationJob
9
+ queue_as :notifications
10
+
11
+ STRATEGIES = {
12
+ "in_app" => "Notifications::Strategies::InApp",
13
+ "email" => "Notifications::Strategies::Email",
14
+ "sms" => "Notifications::Strategies::Sms"
15
+ }.freeze
16
+
17
+ def perform(owner_class:, owner_id:, template:, strategy:)
18
+ klass_name = STRATEGIES.fetch(strategy.to_s) do
19
+ raise ArgumentError, "Unknown strategy: #{strategy.inspect}"
20
+ end
21
+
22
+ owner = owner_class.constantize.find_by(id: owner_id)
23
+ return unless owner
24
+
25
+ notif = klass_name.constantize.new(owner: owner, template: template)
26
+ notif.schedule = IceCube::Schedule.new(Time.current)
27
+ notif.save!
28
+ notif.send_async
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notifications
4
+ # Recurring sweeper. Finds every Notification whose +next_delivery_at+
5
+ # has passed and enqueues a +SendNotificationJob+ for each. Per-row
6
+ # jobs let each notification retry / rate-limit independently.
7
+ #
8
+ # Wire into your queue's recurring scheduler. With Rails 8's Solid
9
+ # Queue, add to config/recurring.yml:
10
+ #
11
+ # production:
12
+ # notifications_dispatcher:
13
+ # class: Notifications::SendDueNotificationsJob
14
+ # schedule: every minute
15
+ class SendDueNotificationsJob < ApplicationJob
16
+ queue_as :notifications
17
+
18
+ def perform
19
+ Notifications::Notification.due.find_each(&:send_async)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notifications
4
+ # Sends a single Notification by id. Uses find_by so a row deleted
5
+ # between enqueue and execution silently no-ops instead of raising.
6
+ class SendNotificationJob < ApplicationJob
7
+ queue_as :notifications
8
+
9
+ def perform(notification_id)
10
+ Notifications::Notification.find_by(id: notification_id)&.send!
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notifications
4
+ # Engine-scoped mailer parent so NotificationMailer doesn't reach
5
+ # into the host's ::ApplicationMailer at autoload time. Hosts that
6
+ # want a layout add `layout "mailer"` here (or per mailer); we don't
7
+ # ship one because the dummy app has no `app/views/layouts/mailer.*`
8
+ # to render and forcing one would crash dummy specs.
9
+ class ApplicationMailer < ::ApplicationMailer
10
+ default from: -> { Notifications.configuration.default_from }
11
+ end
12
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notifications
4
+ # Single mailer used by Notifications::Adapters::ActionMailer.
5
+ # Renders multipart/alternative (text + HTML) when both formats are
6
+ # present in the host/engine template lookup chain. Falls back to a
7
+ # single-part text email when only the text template exists.
8
+ class NotificationMailer < ApplicationMailer
9
+ def notify(notification)
10
+ @notification = notification
11
+ text_body = notification.rendered_content(format: :text)
12
+ html_body =
13
+ if notification.template_exists?(format: :html)
14
+ notification.rendered_content(format: :html)
15
+ end
16
+
17
+ mail(to: notification.recipient, subject: notification.template.to_s.titleize) do |format|
18
+ format.text { render plain: text_body, layout: "notifications/mailer" }
19
+ format.html { render html: html_body.html_safe, layout: "notifications/mailer" } if html_body
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notifications
4
+ class ApplicationRecord < ::ApplicationRecord
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notifications
4
+ # One row per successful +Notification#send!+. Useful audit trail
5
+ # and the basis for "when did we last send?" queries that don't
6
+ # need to load the gateway.
7
+ class Delivery < ApplicationRecord
8
+ self.table_name = "notification_deliveries"
9
+
10
+ belongs_to :notification, class_name: "Notifications::Notification",
11
+ inverse_of: :deliveries
12
+ end
13
+ end