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,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Accounts
4
+ # Joins an Auth::Identity to an Accounts::Account with a role.
5
+ #
6
+ # `identity_id` is **nullable**: when set, the row represents a real
7
+ # human in this Account (an owner, admin, or member). When NULL, the
8
+ # row is a system actor — the audit-log writer for changes that
9
+ # don't have a human behind them (background jobs, webhook
10
+ # ingestion, scheduled tasks). Every Account ships with exactly one
11
+ # `role: :system, identity_id: nil` row, created by
12
+ # `Account.create_with_owner`.
13
+ #
14
+ # An Identity can have at most one Membership per Account
15
+ # (enforced by the unique compound index in the migration); the
16
+ # role lives on the Membership, not the Identity.
17
+ class Membership < ApplicationRecord
18
+ self.table_name = "accounts_memberships"
19
+ self.primary_key = "id"
20
+ self.implicit_order_column = "created_at"
21
+
22
+ ROLES = %w[owner admin member system].freeze
23
+
24
+ belongs_to :account, class_name: "Accounts::Account"
25
+
26
+ validates :name, presence: true
27
+ validates :role, inclusion: { in: ROLES }
28
+ validates :identity_id,
29
+ uniqueness: { scope: :account_id, allow_nil: true,
30
+ message: "already has a membership in this account" }
31
+
32
+ scope :owner, -> { where(role: "owner") }
33
+ scope :admin, -> { where(role: %w[owner admin]) }
34
+ scope :member, -> { where(role: "member") }
35
+ # `:active` excludes the system actor — callers that want every
36
+ # actor (including system) should query without the scope.
37
+ scope :active, -> { where(active: true).where.not(role: "system") }
38
+ scope :system_actors, -> { where(role: "system") }
39
+
40
+ after_create_commit :publish_membership_created
41
+ after_update_commit :publish_role_changed, if: :saved_change_to_role?
42
+ after_destroy_commit :publish_membership_removed
43
+
44
+ def owner?
45
+ role == "owner"
46
+ end
47
+
48
+ def admin?
49
+ %w[owner admin].include?(role)
50
+ end
51
+
52
+ def system?
53
+ role == "system"
54
+ end
55
+
56
+ # Can `self` perform admin-level changes against `other` (another
57
+ # Membership)? Owners can change anyone, admins can change anyone
58
+ # except an owner, members and the system actor cannot change
59
+ # anyone. Pairs with `can_change?` for the slightly more
60
+ # permissive change-anything-non-destructive rule.
61
+ def can_administer?(other)
62
+ return false if system?
63
+ return false unless admin?
64
+ return true if owner?
65
+
66
+ !other.owner?
67
+ end
68
+
69
+ # Like `can_administer?` but allows admins to change other admins
70
+ # (just not owners). Used for invite/remove flows where two
71
+ # admins should be able to manage each other.
72
+ def can_change?(other)
73
+ return false if system?
74
+ return false unless admin?
75
+ return true if owner?
76
+
77
+ !other.owner?
78
+ end
79
+
80
+ private
81
+
82
+ def publish_membership_created
83
+ Seams::Events::Publisher.publish(
84
+ "membership.created.accounts",
85
+ account_id: account_id,
86
+ membership_id: id,
87
+ identity_id: identity_id,
88
+ role: role
89
+ )
90
+ end
91
+
92
+ def publish_role_changed
93
+ from_role, to_role = saved_change_to_role
94
+ Seams::Events::Publisher.publish(
95
+ "membership.role_changed.accounts",
96
+ account_id: account_id,
97
+ membership_id: id,
98
+ from_role: from_role,
99
+ to_role: to_role,
100
+ changed_by_identity_id: Accounts::Current.membership&.identity_id
101
+ )
102
+ end
103
+
104
+ def publish_membership_removed
105
+ Seams::Events::Publisher.publish(
106
+ "membership.removed.accounts",
107
+ account_id: account_id,
108
+ membership_id: id,
109
+ identity_id: identity_id,
110
+ removed_by_identity_id: Accounts::Current.membership&.identity_id
111
+ )
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Intentionally empty. The Accounts engine ships no controllers in
4
+ # Wave 9 — hosts wire their own account-creation flows; the engine
5
+ # provides models + concerns. Future waves may add a thin controller
6
+ # surface.
7
+ Accounts::Engine.routes.draw do
8
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # What: creates the accounts table for the Accounts engine.
4
+ # Why: Accounts::Account is the tenant boundary. Every other engine
5
+ # that scopes its data to a tenant binds to this row.
6
+ # Risk: empty table on creation — no data migration needed.
7
+ #
8
+ # UUID primary key (not bigint): account ids appear in shareable URLs
9
+ # and we don't want to leak row counts. external_account_id is a
10
+ # bigint kept alongside for slug encoding so URL slugs stay short.
11
+ class CreateAccounts < ActiveRecord::Migration[7.1]
12
+ def change
13
+ enable_extension "pgcrypto" unless extension_enabled?("pgcrypto")
14
+
15
+ create_table :accounts, id: :uuid do |t|
16
+ t.string :name, null: false
17
+ t.bigint :external_account_id, null: false
18
+ # Soft cancel: account is unusable but data is still recoverable.
19
+ t.datetime :cancelled_at
20
+ # Hard delete grace marker: when set, the host's incinerator job
21
+ # permanently destroys the row + cascade.
22
+ t.datetime :incinerated_at
23
+ t.timestamps
24
+ end
25
+
26
+ add_index :accounts, :external_account_id, unique: true
27
+ add_index :accounts, :cancelled_at
28
+ end
29
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # What: creates the accounts_memberships join table.
4
+ # Why: joins Auth::Identity to Accounts::Account with a role.
5
+ # identity_id is NULLABLE: rows with NULL identity_id are
6
+ # system actors used for audit-log writes that don't have a
7
+ # human behind them (background jobs, webhook ingestion).
8
+ # Risk: append-mostly. Unique compound index on (account_id,
9
+ # identity_id) ensures an Identity has at most one Membership
10
+ # per Account; system rows (NULL identity_id) bypass that
11
+ # constraint by virtue of the NULL.
12
+ class CreateAccountsMemberships < ActiveRecord::Migration[7.1]
13
+ def change
14
+ enable_extension "pgcrypto" unless extension_enabled?("pgcrypto")
15
+
16
+ create_table :accounts_memberships, id: :uuid do |t|
17
+ t.references :account, type: :uuid, null: false,
18
+ foreign_key: { to_table: :accounts }, index: false
19
+ # NULL = system actor. Otherwise references Auth::Identity. No
20
+ # FK to auth_identities because the auth and accounts engines
21
+ # may live in different schemas / databases in production —
22
+ # cross-engine integrity is enforced at the application layer.
23
+ # bigint to match auth_identities' default integer PK (the same
24
+ # convention every other engine — billing, teams — follows).
25
+ t.bigint :identity_id, null: true
26
+ # Denormalised display name so the Identity (and its email)
27
+ # can be soft-deleted / anonymised without breaking the
28
+ # account's audit trail.
29
+ t.string :name, null: false
30
+ t.string :role, null: false, default: "member"
31
+ t.boolean :active, null: false, default: true
32
+ t.datetime :verified_at
33
+ t.timestamps
34
+ end
35
+
36
+ add_index :accounts_memberships, %i[account_id identity_id], unique: true,
37
+ name: "index_accounts_memberships_unique"
38
+ add_index :accounts_memberships, %i[account_id role]
39
+ add_index :accounts_memberships, :identity_id
40
+ # Postgres treats NULLs as distinct in unique indexes, so the
41
+ # compound index above does NOT prevent two `(account_id, NULL)`
42
+ # rows. Wave 9 invariant: every Account has EXACTLY ONE system
43
+ # actor (created by `Account.create_with_owner`). Enforce it at
44
+ # the DB level with a partial unique index over the system rows.
45
+ add_index :accounts_memberships, :account_id, unique: true,
46
+ where: "role = 'system'",
47
+ name: "index_accounts_memberships_one_system_per_account"
48
+ end
49
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "accounts/version"
4
+ require "accounts/configuration"
5
+ require "accounts/engine"
6
+ require "accounts/concerns/account_scoped"
7
+ require "accounts/concerns/authorization"
8
+
9
+ module Accounts
10
+ class Error < StandardError; end
11
+
12
+ class << self
13
+ def configuration
14
+ @configuration ||= Configuration.new
15
+ end
16
+
17
+ def configure
18
+ yield configuration
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Accounts
6
+ # Mix into any model whose rows belong to a single Account. Adds:
7
+ #
8
+ # - `belongs_to :account, class_name: "Accounts::Account"`
9
+ # - a default_scope filtered to `Accounts::Current.account`. When
10
+ # `Accounts::Current.account` is unset, the scope returns `none`
11
+ # (an empty relation) — see "fail-closed" below.
12
+ # - presence validation on `account`
13
+ #
14
+ # class AuditEntry < ApplicationRecord
15
+ # include Accounts::AccountScoped
16
+ # end
17
+ #
18
+ # Accounts::Current.account = account
19
+ # AuditEntry.create!(action: "...") # account_id auto-assigned
20
+ # AuditEntry.all # only this account's rows
21
+ #
22
+ # ### Fail-closed default
23
+ #
24
+ # When `Accounts::Current.account` is nil, the default_scope returns
25
+ # `none` rather than `all`. Rationale: the alternative (return all
26
+ # rows across all tenants when no account is bound) is the canonical
27
+ # multi-tenant data-leak bug. A forgotten `Current.account=` in a
28
+ # background job, an admin tool, or a Rails console session would
29
+ # otherwise hand the operator another tenant's data without warning.
30
+ # Fail-closed surfaces "I have no rows" — a noisy symptom the
31
+ # developer fixes immediately — instead of a silent leak.
32
+ #
33
+ # Opt-out paths (use deliberately):
34
+ # * `.unscoped` — full Active Record bypass; loses every default
35
+ # scope including soft-delete + sluggable; reserved for
36
+ # platform-admin / impersonation flows gated on
37
+ # `Auth::Current.identity.staff?`.
38
+ # * `.with_no_account_scope` — returns the relation as if the
39
+ # model were not account-scoped; preserves any other
40
+ # default_scopes the model declares; intended for seed scripts,
41
+ # migrations, and platform tooling.
42
+ #
43
+ # Background jobs MUST set `Accounts::Current.account =` before
44
+ # querying, or use `.with_no_account_scope` explicitly. The shape:
45
+ #
46
+ # class IngestionJob < ApplicationJob
47
+ # def perform(account_id, payload)
48
+ # Accounts::Current.account = Accounts::Account.find(account_id)
49
+ # AuditEntry.create!(...) # auto-scoped
50
+ # end
51
+ # end
52
+ module AccountScoped
53
+ extend ActiveSupport::Concern
54
+
55
+ included do
56
+ belongs_to :account, class_name: "Accounts::Account"
57
+
58
+ # The default_scope ALWAYS applies a where(:account_id) clause —
59
+ # either filtered to the current account or to NULL. The model
60
+ # validates `account` presence and the `belongs_to` adds a NOT
61
+ # NULL guard at the application layer, so `where(account_id: nil)`
62
+ # is a never-matching sentinel — fail-closed without using
63
+ # `none`. The shape is deliberate: `unscope(where: :account_id)`
64
+ # in `with_no_account_scope` cleanly reverses this. Using
65
+ # `none` would create a NullRelation that `unscope` cannot un-do,
66
+ # which would defeat the documented opt-out path.
67
+ default_scope -> {
68
+ account = Accounts::Current.account
69
+ if account
70
+ where(account_id: account.id)
71
+ else
72
+ where(account_id: nil)
73
+ end
74
+ }
75
+
76
+ validates :account, presence: true
77
+
78
+ before_validation :assign_current_account, on: :create
79
+ end
80
+
81
+ class_methods do
82
+ # Returns the relation as it would be without the AccountScoped
83
+ # default_scope, preserving any other default_scopes the model
84
+ # declares. Use deliberately — name-it-loud opt-out for seed
85
+ # scripts, migrations, and platform tooling.
86
+ def with_no_account_scope
87
+ unscope(where: :account_id)
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def assign_current_account
94
+ self.account_id ||= Accounts::Current.account&.id if Accounts::Current.account
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Accounts
6
+ # Controller concern that enforces tenant access on every action by
7
+ # default. Mix into the host's ApplicationController (or a base
8
+ # account-scoped controller) and use the class-method helpers to
9
+ # opt out per-controller.
10
+ #
11
+ # class ApplicationController < ActionController::Base
12
+ # include Accounts::Authorization
13
+ # end
14
+ #
15
+ # class PublicPagesController < ApplicationController
16
+ # disallow_account_scope # no membership check
17
+ # end
18
+ #
19
+ # class OnboardingController < ApplicationController
20
+ # require_access_without_membership
21
+ # end
22
+ #
23
+ # Default-on `before_action :ensure_account_access` checks that the
24
+ # current Account is active AND the current Membership is active.
25
+ # Otherwise: 403 (or redirects on HTML).
26
+ #
27
+ # `ensure_admin` and `ensure_staff` are opt-in helpers controllers
28
+ # call from their own `before_action` — they don't run by default.
29
+ # `ensure_admin` checks the per-Account role (owner/admin);
30
+ # `ensure_staff` checks the platform-admin flag on Identity (set
31
+ # via `Auth::Identity#staff?`). Two different powers — keep them
32
+ # distinct.
33
+ module Authorization
34
+ extend ActiveSupport::Concern
35
+
36
+ included do
37
+ before_action :ensure_account_access if respond_to?(:before_action)
38
+ end
39
+
40
+ class_methods do
41
+ # Skip the default tenant access check for this controller. Use
42
+ # for public pages (sign-in, marketing, OAuth callbacks).
43
+ def disallow_account_scope(**options)
44
+ skip_before_action :ensure_account_access, **options
45
+ end
46
+
47
+ # Sign-up + onboarding flows: the Identity is signed in but
48
+ # doesn't have a Membership yet. Skip the access check, but
49
+ # redirect away if a Membership IS present (otherwise the user
50
+ # bounces between sign-up and the dashboard).
51
+ def require_access_without_membership(**options)
52
+ skip_before_action :ensure_account_access, **options
53
+ before_action :redirect_existing_membership, **options
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def ensure_account_access
60
+ return if account_access_allowed?
61
+
62
+ respond_to do |format|
63
+ format.html { redirect_to(authorization_redirect_path) }
64
+ format.json { head :forbidden }
65
+ format.any { head :forbidden }
66
+ end
67
+ end
68
+
69
+ def account_access_allowed?
70
+ return false unless Accounts::Current.account&.active?
71
+
72
+ membership = Accounts::Current.membership
73
+ membership.present? && membership.active && !membership.system?
74
+ end
75
+
76
+ # Per-account admin (owner OR admin). Use as a controller
77
+ # `before_action :ensure_admin` for in-account admin tooling
78
+ # (member management, billing settings, etc.).
79
+ def ensure_admin
80
+ head :forbidden unless Accounts::Current.membership&.admin?
81
+ end
82
+
83
+ # Platform admin (Identity#staff?). Use as a controller
84
+ # `before_action :ensure_staff` for support tooling that has to
85
+ # bypass the per-account scope (e.g. impersonation, cross-account
86
+ # search). Distinct from `ensure_admin`.
87
+ def ensure_staff
88
+ identity = defined?(Auth::Current) ? Auth::Current.identity : nil
89
+ head :forbidden unless identity&.staff?
90
+ end
91
+
92
+ def redirect_existing_membership
93
+ return unless Accounts::Current.membership
94
+
95
+ respond_to do |format|
96
+ format.html { redirect_to(Accounts.configuration.after_account_create_url) }
97
+ format.any { head :found }
98
+ end
99
+ end
100
+
101
+ def authorization_redirect_path
102
+ Accounts.configuration.after_account_create_url
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Accounts
4
+ # Engine-scoped configuration. Override in
5
+ # config/initializers/accounts.rb of the host application:
6
+ #
7
+ # Accounts.configure do |c|
8
+ # c.incineration_grace_period = 30.days
9
+ # c.after_account_create_url = "/dashboard"
10
+ # end
11
+ class Configuration
12
+ attr_accessor :incineration_grace_period,
13
+ :after_account_create_url
14
+ # Follow-up generators that add knobs (account_owner_role, default_account_locale) declare their attr_accessor here.
15
+ # seams:insertion-point accounts.configuration.attributes
16
+
17
+ def initialize
18
+ # How long a cancelled account lingers before incineration (hard
19
+ # delete). Hosts use this to power dunning + grace-period UX.
20
+ @incineration_grace_period = 30 * 24 * 60 * 60 # 30 days
21
+ @after_account_create_url = "/"
22
+ # Follow-up generators that add defaults for new attributes (matching accounts.configuration.attributes) splice them here.
23
+ # seams:insertion-point accounts.configuration.defaults
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Accounts
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Accounts
6
+
7
+ config.generators do |g|
8
+ g.test_framework :rspec
9
+ end
10
+
11
+ initializer "accounts.register_events" do
12
+ Seams::EventRegistry.register("account.created.accounts", emitted_by: "Accounts")
13
+ Seams::EventRegistry.register("account.cancelled.accounts", emitted_by: "Accounts")
14
+ Seams::EventRegistry.register("membership.created.accounts", emitted_by: "Accounts")
15
+ Seams::EventRegistry.register("membership.role_changed.accounts", emitted_by: "Accounts")
16
+ Seams::EventRegistry.register("membership.removed.accounts", emitted_by: "Accounts")
17
+ # Follow-up generators that emit new accounts events (account.upgraded.accounts, etc.) register them here.
18
+ # seams:insertion-point accounts.engine.events
19
+ end
20
+
21
+ initializer "accounts.append_migrations" do |app|
22
+ unless app.root == root
23
+ config.paths["db/migrate"].expanded.each do |expanded_path|
24
+ app.config.paths["db/migrate"] << expanded_path
25
+ end
26
+ end
27
+ end
28
+
29
+ # Follow-up generators that need their own initializer block declare it here, ahead of the cross-engine dependency check.
30
+ # seams:insertion-point accounts.engine.initializers
31
+
32
+ # Boot-time dependency assertion. Accounts requires Auth: an
33
+ # Accounts::Membership with an `identity_id` is meaningless
34
+ # without an `auth_identities` row to point at. We enforce this
35
+ # at app boot rather than at first failed query so the operator
36
+ # gets a clear "install seams:auth" error instead of a deep stack
37
+ # trace mid-request.
38
+ config.after_initialize do
39
+ missing = []
40
+ missing << "Auth::Identity (run: bin/rails generate seams:auth)" unless defined?(::Auth::Identity)
41
+
42
+ if missing.any?
43
+ raise <<~MSG
44
+ [seams accounts] missing required cross-engine dependency:
45
+ #{missing.join("\n ")}
46
+
47
+ The accounts engine joins Auth::Identity rows to
48
+ Accounts::Account rows; it cannot run without auth. Generate
49
+ the auth engine and re-run db:migrate, or remove accounts
50
+ with `bin/rails generate seams:remove accounts --force`.
51
+ MSG
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Factories for Accounts engine specs. Sequences keep names unique
4
+ # across the full spec run. The :membership factory associates with
5
+ # the auth engine's :auth_identity factory — ensure both engines
6
+ # are loaded in the dummy app or include the auth_identities table
7
+ # in this engine's dummy schema.
8
+ FactoryBot.define do
9
+ factory :account, class: "Accounts::Account" do
10
+ sequence(:name) { |n| "Account #{n}" }
11
+ sequence(:external_account_id) { |n| 1_000_000 + n }
12
+ end
13
+
14
+ factory :membership, class: "Accounts::Membership" do
15
+ association :account
16
+ association :identity, factory: :auth_identity
17
+ sequence(:name) { |n| "Member #{n}" }
18
+ role { "member" }
19
+ active { true }
20
+
21
+ # Denormalises identity_id from the association rather than
22
+ # relying on belongs_to magic — Membership has no `belongs_to
23
+ # :identity` because the auth engine and accounts engine are
24
+ # peer engines (no cross-engine model access at the
25
+ # ActiveRecord level).
26
+ after(:build) do |membership, evaluator|
27
+ membership.identity_id = evaluator.identity&.id
28
+ end
29
+
30
+ transient do
31
+ identity { nil }
32
+ end
33
+
34
+ factory :owner_membership do
35
+ role { "owner" }
36
+ end
37
+
38
+ factory :admin_membership do
39
+ role { "admin" }
40
+ end
41
+
42
+ factory :system_membership do
43
+ role { "system" }
44
+ identity { nil }
45
+ identity_id { nil }
46
+ name { "System" }
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_helper"
4
+
5
+ RSpec.describe Accounts::Account do
6
+ describe "validations" do
7
+ it "requires a name" do
8
+ account = described_class.new
9
+ expect(account).not_to be_valid
10
+ expect(account.errors[:name]).to include("can't be blank")
11
+ end
12
+ end
13
+
14
+ describe "scopes" do
15
+ it ".active excludes cancelled accounts" do
16
+ live = described_class.create!(name: "Live")
17
+ cancelled = described_class.create!(name: "Cancelled", cancelled_at: 1.day.ago)
18
+
19
+ expect(described_class.active).to include(live)
20
+ expect(described_class.active).not_to include(cancelled)
21
+ end
22
+ end
23
+
24
+ describe "#active?" do
25
+ it "is true when not cancelled and not incinerated" do
26
+ expect(described_class.new).to be_active
27
+ end
28
+
29
+ it "is false when cancelled_at is set" do
30
+ expect(described_class.new(cancelled_at: 1.day.ago)).not_to be_active
31
+ end
32
+
33
+ it "is false when incinerated_at is set" do
34
+ expect(described_class.new(incinerated_at: 1.day.ago)).not_to be_active
35
+ end
36
+ end
37
+
38
+ describe "#assign_external_account_id" do
39
+ it "auto-assigns external_account_id on create when blank" do
40
+ account = described_class.create!(name: "Acme")
41
+ expect(account.external_account_id).to be_a(Integer)
42
+ expect(account.external_account_id).to be > 0
43
+ end
44
+ end
45
+
46
+ describe ".create_with_owner" do
47
+ let(:identity) { Auth::Identity.create!(email: "owner-#{SecureRandom.hex(4)}@example.com", password: "verysecret") }
48
+ let(:owner) { Struct.new(:identity, :name).new(identity, "Ada") }
49
+
50
+ it "creates an account, a system membership, and an owner membership" do
51
+ account = described_class.create_with_owner(account: { name: "Acme" }, owner: owner)
52
+ expect(account.memberships.pluck(:role)).to match_array(%w[system owner])
53
+ end
54
+
55
+ it "rolls back the account when the owner membership fails to save" do
56
+ bad_owner = Struct.new(:identity, :name).new(identity, nil)
57
+
58
+ expect {
59
+ described_class.create_with_owner(account: { name: "Bad" }, owner: bad_owner)
60
+ }.to raise_error(ActiveRecord::RecordInvalid)
61
+ .and(change(described_class, :count).by(0))
62
+ end
63
+ end
64
+ end