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,272 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "rails/generators"
5
+ require "seams"
6
+ require "generators/seams/engine/engine_generator"
7
+ require "seams/generators/host_injector"
8
+ require "seams/generators/eject_aware"
9
+ require "seams/generators/dummy_app_writer"
10
+
11
+ module Seams
12
+ module Generators
13
+ # Generates a canonical Accounts engine on top of the generic engine
14
+ # scaffold. Adds:
15
+ #
16
+ # - Accounts::Account model. Tenant boundary. UUID PK.
17
+ # - Accounts::Membership model. Joins Auth::Identity to Account
18
+ # with a role enum (owner/admin/member/system); identity_id is
19
+ # nullable for system actors used by audit-log writes.
20
+ # - Accounts::Current per-request namespace.
21
+ # - Accounts::AccountScoped model concern (default_scope to
22
+ # Current.account, opt-out via .unscoped).
23
+ # - Accounts::Authorization controller concern (default-on
24
+ # ensure_account_access; opt out via disallow_account_scope or
25
+ # require_access_without_membership; helpers ensure_admin /
26
+ # ensure_staff).
27
+ # - Migrations for accounts + accounts_memberships (pgcrypto).
28
+ # - lib/accounts/engine.rb registers the canonical events:
29
+ # account.created.accounts, account.cancelled.accounts,
30
+ # membership.created.accounts, membership.role_changed.accounts,
31
+ # membership.removed.accounts.
32
+ #
33
+ # The engine ships NO controllers in Wave 9 — hosts drive their
34
+ # own account-creation flows; this engine is the model + concern
35
+ # layer.
36
+ #
37
+ # Run with: bin/rails generate seams:accounts
38
+ class AccountsGenerator < Rails::Generators::Base
39
+ include Seams::Generators::HostInjector
40
+ include Seams::Generators::EjectAware
41
+
42
+ source_root File.expand_path("templates", __dir__)
43
+
44
+ ENGINE_NAME = "accounts"
45
+
46
+ def create_engine_skeleton
47
+ EngineGenerator.start([ENGINE_NAME], destination_root: destination_root)
48
+ end
49
+
50
+ def overwrite_engine_entry_point
51
+ # engine.rb / lib/accounts.rb stay framework-managed.
52
+ template "lib/engine.rb.tt", engine_path("lib/accounts/engine.rb"), force: true
53
+ template "lib/accounts.rb.tt", engine_path("lib/accounts.rb"), force: true
54
+ template_unless_ejected "lib/configuration.rb.tt",
55
+ engine_path("lib/accounts/configuration.rb")
56
+ end
57
+
58
+ def overwrite_routes
59
+ template_unless_ejected "config/routes.rb.tt", engine_path("config/routes.rb"), force: true
60
+ end
61
+
62
+ def create_models
63
+ template_unless_ejected "app/models/application_record.rb.tt",
64
+ engine_path("app/models/accounts/application_record.rb")
65
+ template_unless_ejected "app/models/account.rb.tt",
66
+ engine_path("app/models/accounts/account.rb")
67
+ template_unless_ejected "app/models/membership.rb.tt",
68
+ engine_path("app/models/accounts/membership.rb")
69
+ template_unless_ejected "app/models/current.rb.tt",
70
+ engine_path("app/models/accounts/current.rb")
71
+ end
72
+
73
+ def create_concerns
74
+ template_unless_ejected "lib/concerns/account_scoped.rb.tt",
75
+ engine_path("lib/accounts/concerns/account_scoped.rb")
76
+ template_unless_ejected "lib/concerns/authorization.rb.tt",
77
+ engine_path("lib/accounts/concerns/authorization.rb")
78
+ end
79
+
80
+ def create_migrations
81
+ template "db/migrate/create_accounts.rb.tt",
82
+ engine_path("db/migrate/#{timestamp(0)}_create_accounts.rb")
83
+ template "db/migrate/create_accounts_memberships.rb.tt",
84
+ engine_path("db/migrate/#{timestamp(1)}_create_accounts_memberships.rb")
85
+ end
86
+
87
+ def create_factories
88
+ template_unless_ejected "spec/factories/accounts.rb.tt",
89
+ engine_path("spec/factories/accounts.rb")
90
+ end
91
+
92
+ def create_unit_specs
93
+ template_unless_ejected "spec/models/accounts/account_spec.rb.tt",
94
+ engine_path("spec/models/accounts/account_spec.rb")
95
+ template_unless_ejected "spec/models/accounts/membership_spec.rb.tt",
96
+ engine_path("spec/models/accounts/membership_spec.rb")
97
+ end
98
+
99
+ def overwrite_readme
100
+ template "README.md.tt", engine_path("README.md"), force: true
101
+ end
102
+
103
+ def update_exposed_concerns
104
+ rubocop_path = engine_path(".rubocop.yml")
105
+ return unless File.exist?(rubocop_path)
106
+
107
+ contents = File.read(rubocop_path)
108
+ replacement = " ExposedConcerns:\n - Accounts::AccountScoped\n - Accounts::Authorization"
109
+ contents.sub!(/^ ExposedConcerns: \[\]$/, replacement)
110
+ File.write(rubocop_path, contents)
111
+ end
112
+
113
+ def create_dummy_app
114
+ # Post Wave 9: the dummy app does NOT ship a host User model.
115
+ # Auth::Identity is the canonical human; accounts membership
116
+ # is the per-tenant role row. The accounts engine specs DO
117
+ # exercise Auth::Identity directly (a Membership without an
118
+ # Identity to point at is meaningless), so we ship a slim
119
+ # stub at app/models/auth/identity.rb the same way the
120
+ # notifications engine does.
121
+ Seams::Generators::DummyAppWriter.write!(
122
+ engine_path: File.join(destination_root, "engines", ENGINE_NAME),
123
+ engine_module: "Accounts",
124
+ mount_at: "/accounts",
125
+ schema: dummy_schema,
126
+ host_user: dummy_host_identity,
127
+ host_user_path: "app/models/auth/identity.rb"
128
+ )
129
+ write_auth_current_stub
130
+ template "spec/runtime/accounts_boot_spec.rb.tt",
131
+ engine_path("spec/runtime/accounts_boot_spec.rb")
132
+ end
133
+
134
+ # Write a tiny `Auth::Current` stub so the accounts engine specs
135
+ # (which read Current.identity) can run without pulling in the
136
+ # full auth engine.
137
+ def write_auth_current_stub
138
+ path = File.join(destination_root, "engines", ENGINE_NAME,
139
+ "spec/dummy/app/models/auth/current.rb")
140
+ FileUtils.mkdir_p(File.dirname(path))
141
+ File.write(path, <<~RB)
142
+ # frozen_string_literal: true
143
+ # Slim Auth::Current stub for the accounts dummy app. Stands in
144
+ # for the real Auth::Current (which lives in the auth engine,
145
+ # not loaded by the dummy) so accounts specs can wire
146
+ # `Current.identity = identity` against the same surface area
147
+ # the canonical seams host uses.
148
+ module Auth
149
+ class Current < ActiveSupport::CurrentAttributes
150
+ attribute :identity
151
+ end
152
+ end
153
+ RB
154
+ end
155
+
156
+ def create_runtime_specs
157
+ # Currently a single runtime boot spec covers events, schema,
158
+ # create_with_owner, and Accounts::Current. Split into multiple
159
+ # files in a later phase if the file grows past ~120 lines.
160
+ end
161
+
162
+ def wire_into_host
163
+ # factory_bot_rails powers spec/factories/accounts.rb. Lives
164
+ # in the host's test group only.
165
+ host_inject_gem("factory_bot_rails", "~> 6.4", group: :test)
166
+ host_inject_mount(engine_class: "Accounts::Engine", at: "/accounts")
167
+ # NB: no host_inject_include_in_user — the host User is going
168
+ # away in Wave 9. Hosts that DO keep a User model wire it up
169
+ # themselves.
170
+ end
171
+
172
+ def report_summary
173
+ say ""
174
+ say " Accounts engine generated at engines/accounts/", :green
175
+ say ""
176
+ say " Next steps:", :yellow
177
+ say " 1. bin/rails db:migrate"
178
+ say " 2. Include Accounts::Authorization in your ApplicationController"
179
+ say " 3. Wire Accounts::Current.account in a before_action"
180
+ say " 4. Run the engine specs: bin/rails seams:test[accounts]"
181
+ say ""
182
+ end
183
+
184
+ private
185
+
186
+ def engine_path(relative)
187
+ File.join(destination_root, "engines", ENGINE_NAME, relative)
188
+ end
189
+
190
+ def timestamp(offset)
191
+ # Microsecond-resolution timestamp so migrations generated
192
+ # back-to-back don't collide. Offset by +50 so accounts'
193
+ # `accounts` + `accounts_memberships` tables migrate AFTER
194
+ # auth's `auth_identities` (+0..+3, since memberships address
195
+ # an Identity at the application layer) but BEFORE engines
196
+ # whose schemas depend on `accounts.id` semantically:
197
+ # notifications +100, billing +200, teams +300. Without this,
198
+ # billing's `subscriptions.account_id` would migrate before
199
+ # the `accounts` table existed — no DB-level FK so it's
200
+ # silent, but ordering matters if a host ever tightens to a
201
+ # real foreign-key constraint.
202
+ base = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
203
+ (base + 50 + offset).to_s
204
+ end
205
+
206
+ # Slim Auth::Identity stub for the dummy app. Stands in for the
207
+ # real Auth::Identity (which lives in the auth engine, not loaded
208
+ # by the dummy) so accounts specs can build an Identity for the
209
+ # owner-membership join. Includes has_secure_password so spec
210
+ # fixtures can pass `password:` like the real Identity accepts.
211
+ def dummy_host_identity
212
+ <<~RB
213
+ # frozen_string_literal: true
214
+ module Auth
215
+ class Identity < ApplicationRecord
216
+ self.table_name = "auth_identities"
217
+ has_secure_password
218
+ end
219
+ end
220
+ RB
221
+ end
222
+
223
+ def dummy_schema
224
+ # Includes auth_identities so factories that link memberships
225
+ # to an Identity can `create(:auth_identity)` against a real
226
+ # row. Match the auth engine's schema for that table so
227
+ # cross-engine specs don't drift.
228
+ <<~SCHEMA
229
+ enable_extension "pgcrypto"
230
+
231
+ create_table :auth_identities do |t|
232
+ t.text :email, null: false
233
+ t.string :password_digest, null: false
234
+ t.boolean :staff, null: false, default: false
235
+ t.timestamps
236
+ end
237
+ add_index :auth_identities, :email, unique: true
238
+ add_index :auth_identities, :staff, where: "staff = true"
239
+
240
+ create_table :accounts, id: :uuid do |t|
241
+ t.string :name, null: false
242
+ t.bigint :external_account_id, null: false
243
+ t.datetime :cancelled_at
244
+ t.datetime :incinerated_at
245
+ t.timestamps
246
+ end
247
+ add_index :accounts, :external_account_id, unique: true
248
+ add_index :accounts, :cancelled_at
249
+
250
+ create_table :accounts_memberships, id: :uuid do |t|
251
+ t.references :account, type: :uuid, null: false,
252
+ foreign_key: { to_table: :accounts }, index: false
253
+ t.bigint :identity_id, null: true
254
+ t.string :name, null: false
255
+ t.string :role, null: false, default: "member"
256
+ t.boolean :active, null: false, default: true
257
+ t.datetime :verified_at
258
+ t.timestamps
259
+ end
260
+ add_index :accounts_memberships, %i[account_id identity_id], unique: true,
261
+ name: "index_accounts_memberships_unique"
262
+ add_index :accounts_memberships, %i[account_id role]
263
+ add_index :accounts_memberships, :identity_id
264
+ # Wave 9 invariant: exactly one system actor per Account.
265
+ add_index :accounts_memberships, :account_id, unique: true,
266
+ where: "role = 'system'",
267
+ name: "index_accounts_memberships_one_system_per_account"
268
+ SCHEMA
269
+ end
270
+ end
271
+ end
272
+ end
@@ -0,0 +1,219 @@
1
+ # Accounts
2
+
3
+ > Tenant boundary for a Seams-powered host application. Owns the
4
+ > `Accounts::Account` (the workspace), `Accounts::Membership`
5
+ > (Identity ↔ Account, with role), per-request `Accounts::Current`,
6
+ > and the two concerns hosts need to scope their own data and
7
+ > controllers to a tenant. Identity stays in the auth engine — this
8
+ > engine never owns credentials.
9
+
10
+ **Requires:** the `auth` engine. `Accounts::Membership.identity_id`
11
+ joins to `auth_identities`; `Accounts::Current.account=` reads
12
+ `Auth::Current.identity` to derive the matching Membership. Install
13
+ auth before accounts.
14
+
15
+ ## Identity, Account, and Membership
16
+
17
+ Three peer concepts, three tables, one clear responsibility each:
18
+
19
+ | Model | Owns | Lives in |
20
+ | --- | --- | --- |
21
+ | `Auth::Identity` | The human. Credentials, sessions, OAuth, API tokens. | `auth_identities` (auth engine) |
22
+ | `Accounts::Account`| The workspace / tenant. Name + soft-cancel state. | `accounts` (this engine) |
23
+ | `Accounts::Membership` | The join. Says "Identity X is a Y in Account Z". | `accounts_memberships` (this engine) |
24
+
25
+ The same `Auth::Identity` can have memberships in multiple Accounts
26
+ with different roles. A Membership belongs to exactly one Account.
27
+
28
+ ## Why is `identity_id` nullable on Membership?
29
+
30
+ System actors. Every Account ships with a `role: "system"` row whose
31
+ `identity_id` is NULL. That row is the audit-log writer for changes
32
+ that don't have a human behind them — background jobs syncing data
33
+ from a webhook, scheduled tasks expiring trials, billing-engine
34
+ hooks reconciling a subscription, etc.
35
+
36
+ Without a system actor, every audit entry would either need a
37
+ nullable `actor_id` (which is what the system row neatly avoids) or
38
+ a fake "robot" Identity that lives nowhere else and confuses
39
+ everything from email lookup to billing customer mapping.
40
+
41
+ ## Roles
42
+
43
+ | Role | identity_id | Powers |
44
+ | --- | --- | --- |
45
+ | `owner` | required | Full admin. Can manage members, billing, cancel the account. |
46
+ | `admin` | required | Manages members and most settings; cannot remove an owner. |
47
+ | `member` | required | Default role. Read + write within the account's data scope. |
48
+ | `system` | NULL | Audit-log actor only. Never logs in. One per account. |
49
+
50
+ Every Account has exactly one system row (created automatically by
51
+ `Account.create_with_owner`); humans get `owner` / `admin` /
52
+ `member` rows.
53
+
54
+ ## `staff` vs `admin` — platform admin vs in-account admin
55
+
56
+ These are different powers:
57
+
58
+ - `Auth::Identity#staff?` — a boolean on the Identity row. Platform-level
59
+ super-user. Bypasses account scoping for support tooling
60
+ (impersonation, cross-account search). Set by an out-of-band admin
61
+ process; never via sign-up params.
62
+ - `Accounts::Membership#admin?` — true when role is `owner` OR `admin`.
63
+ In-account admin. Manages the workspace's members and settings
64
+ but has no power outside this account.
65
+
66
+ Use `ensure_staff` in controllers that span accounts.
67
+ Use `ensure_admin` in controllers that manage one account.
68
+
69
+ ## Events emitted
70
+
71
+ | Event name | Payload |
72
+ | --- | --- |
73
+ | `account.created.accounts` | `{ account_id:, owner_identity_id: }` |
74
+ | `account.cancelled.accounts` | `{ account_id:, cancelled_by_identity_id: }` |
75
+ | `membership.created.accounts` | `{ account_id:, membership_id:, identity_id:, role: }` |
76
+ | `membership.role_changed.accounts` | `{ account_id:, membership_id:, from_role:, to_role:, changed_by_identity_id: }` |
77
+ | `membership.removed.accounts` | `{ account_id:, membership_id:, identity_id:, removed_by_identity_id: }` |
78
+
79
+ `identity_id` is the row id in the auth engine's `auth_identities`
80
+ table. Subscribers (notifications, billing, etc.) resolve the human
81
+ via Identity, never via a host User.
82
+
83
+ ## Events consumed
84
+
85
+ This engine does not subscribe to any other engine's events.
86
+ Downstream engines (notifications, billing, teams) publish and
87
+ subscribe via `account_id` / `identity_id` carried on their event
88
+ payloads.
89
+
90
+ ## Exposed concerns
91
+
92
+ ### `Accounts::AccountScoped` — model concern
93
+
94
+ Mix into any model whose rows belong to a single Account:
95
+
96
+ ```ruby
97
+ class AuditEntry < ApplicationRecord
98
+ include Accounts::AccountScoped
99
+ end
100
+
101
+ Accounts::Current.account = account
102
+ AuditEntry.create!(action: "...") # account_id auto-assigned
103
+ AuditEntry.all # only this account's rows (default_scope)
104
+ AuditEntry.unscoped.all # opt out for cross-tenant queries
105
+ ```
106
+
107
+ The default_scope is a no-op when `Accounts::Current.account` is
108
+ unset, so background jobs that haven't bound a Current account see
109
+ every row — wire `Accounts::Current.account =` into your job's
110
+ `#perform` if you need scoping there.
111
+
112
+ ### `Accounts::Authorization` — controller concern
113
+
114
+ Mix into the host's `ApplicationController`:
115
+
116
+ ```ruby
117
+ class ApplicationController < ActionController::Base
118
+ include Accounts::Authorization
119
+ # default-on `before_action :ensure_account_access`
120
+ end
121
+ ```
122
+
123
+ Then opt out per-controller:
124
+
125
+ ```ruby
126
+ class PublicPagesController < ApplicationController
127
+ disallow_account_scope # public marketing
128
+ end
129
+
130
+ class OnboardingController < ApplicationController
131
+ require_access_without_membership # signed in, no membership yet
132
+ end
133
+ ```
134
+
135
+ Plus two opt-in helpers any controller can call from its own `before_action`:
136
+
137
+ - `ensure_admin` — checks `Accounts::Current.membership&.admin?`
138
+ (in-account owner OR admin)
139
+ - `ensure_staff` — checks `Auth::Current.identity&.staff?`
140
+ (platform admin)
141
+
142
+ ## Per-request state — `Accounts::Current`
143
+
144
+ The host wires `Accounts::Current.account` in a controller
145
+ before_action. Setting `account=` automatically derives the
146
+ matching `Membership` for the currently signed-in identity (read
147
+ from `Auth::Current.identity`):
148
+
149
+ ```ruby
150
+ class ApplicationController < ActionController::Base
151
+ before_action :resolve_current_account
152
+
153
+ private
154
+
155
+ def resolve_current_account
156
+ Accounts::Current.account = current_account_from_url_or_session
157
+ end
158
+ end
159
+
160
+ # Anywhere downstream:
161
+ Accounts::Current.account # => Accounts::Account
162
+ Accounts::Current.membership # => Accounts::Membership for the signed-in human
163
+ ```
164
+
165
+ ## Account creation
166
+
167
+ ```ruby
168
+ identity = Auth::Identity.create!(email: "ada@example.com", password: "...")
169
+ owner = Struct.new(:identity, :name).new(identity, "Ada Lovelace")
170
+
171
+ account = Accounts::Account.create_with_owner(
172
+ account: { name: "Acme Corp" },
173
+ owner: owner
174
+ )
175
+
176
+ account.memberships.pluck(:role) # => ["system", "owner"]
177
+ ```
178
+
179
+ The system membership is mandatory — it's the audit-log actor for
180
+ non-human changes. The owner membership is the human creating the
181
+ account. Wrapped in a transaction; if any of the three inserts
182
+ fail, all roll back.
183
+
184
+ ## Migration / setup steps
185
+
186
+ 1. Run the generator:
187
+ ```bash
188
+ bin/rails generate seams:accounts
189
+ ```
190
+ 2. Bundle install (no new gems, but locks the engine into the host).
191
+ 3. Migrate:
192
+ ```bash
193
+ bin/rails db:migrate
194
+ ```
195
+ 4. Mount in `config/routes.rb` (the generator does this automatically):
196
+ ```ruby
197
+ mount Accounts::Engine, at: "/accounts"
198
+ ```
199
+ 5. Wire `Accounts::Authorization` into your `ApplicationController`.
200
+
201
+ ## Mounting
202
+
203
+ ```ruby
204
+ # config/routes.rb (host application)
205
+ Rails.application.routes.draw do
206
+ mount Accounts::Engine, at: "/accounts"
207
+ end
208
+ ```
209
+
210
+ This engine ships **no controllers** intentionally. Hosts drive
211
+ account-creation flows themselves (the shape of the sign-up wizard
212
+ varies too much to template). Use `Accounts::Account.create_with_owner`
213
+ from your own controller.
214
+
215
+ ## Running the specs
216
+
217
+ ```bash
218
+ bin/rails seams:test[accounts]
219
+ ```
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Accounts
4
+ # The tenant boundary. One Account per "workspace" / "company" /
5
+ # "tenant" — every other engine that wants to scope its data to a
6
+ # tenant binds to this row.
7
+ #
8
+ # Identity (Auth::Identity) is the human; Account is the workspace;
9
+ # Membership joins them. The same human (Identity) can have
10
+ # memberships in multiple Accounts.
11
+ #
12
+ # UUID primary key (not bigint) so account ids can appear in
13
+ # shareable URLs without leaking row counts. The host User is gone
14
+ # post-Wave-9; downstream engines reference Identity (`identity_id`)
15
+ # or Account (`account_id`).
16
+ class Account < ApplicationRecord
17
+ self.table_name = "accounts"
18
+ self.primary_key = "id"
19
+ self.implicit_order_column = "created_at"
20
+
21
+ has_many :memberships, class_name: "Accounts::Membership",
22
+ foreign_key: :account_id, dependent: :destroy
23
+
24
+ validates :name, presence: true
25
+
26
+ scope :active, -> { where(cancelled_at: nil) }
27
+ scope :cancelled, -> { where.not(cancelled_at: nil) }
28
+
29
+ before_create :assign_external_account_id
30
+
31
+ after_create_commit :publish_account_created
32
+ after_update_commit :publish_account_cancelled, if: :saved_change_to_cancelled_at?
33
+
34
+ # Returns self so polymorphic helpers that expect `record.account`
35
+ # work uniformly across an Account row and any account-scoped row.
36
+ def account
37
+ self
38
+ end
39
+
40
+ def system_membership
41
+ memberships.find_by(role: "system")
42
+ end
43
+
44
+ def active?
45
+ cancelled_at.nil? && incinerated_at.nil?
46
+ end
47
+
48
+ def cancelled?
49
+ cancelled_at.present?
50
+ end
51
+
52
+ # Hard-delete grace period: a cancelled account becomes
53
+ # incinerated when the host job decides the grace window is up.
54
+ def incinerated?
55
+ incinerated_at.present?
56
+ end
57
+
58
+ # Wraps Account creation + System + Owner membership in a single
59
+ # transaction so the audit trail always has a System actor and the
60
+ # human creating the account has an Owner row.
61
+ #
62
+ # Accounts::Account.create_with_owner(
63
+ # account: { name: "Acme" },
64
+ # owner: identity_or_owner_struct
65
+ # )
66
+ #
67
+ # `owner` must respond to `identity` and `name`. The Identity link
68
+ # is the human's row in `auth_identities`.
69
+ def self.create_with_owner(account:, owner:)
70
+ transaction do
71
+ record = create!(**account)
72
+ record.memberships.create!(
73
+ role: "system",
74
+ name: "System",
75
+ identity_id: nil,
76
+ active: true
77
+ )
78
+ record.memberships.create!(
79
+ role: "owner",
80
+ name: owner.name,
81
+ identity_id: owner.identity.id,
82
+ verified_at: Time.current,
83
+ active: true
84
+ )
85
+ record
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def assign_external_account_id
92
+ self.external_account_id ||= self.class.next_external_account_id
93
+ end
94
+
95
+ # Per-class sequence so external_account_id stays unique without a
96
+ # DB-side sequence (UUID PKs make per-row IDs cheap, but
97
+ # external_account_id is a slug-friendly bigint). Hosts that want
98
+ # a different sequence can override this method.
99
+ def self.next_external_account_id
100
+ loop do
101
+ candidate = SecureRandom.random_number(2**62)
102
+ break candidate unless exists?(external_account_id: candidate)
103
+ end
104
+ end
105
+
106
+ def publish_account_created
107
+ Seams::Events::Publisher.publish(
108
+ "account.created.accounts",
109
+ account_id: id,
110
+ owner_identity_id: memberships.where(role: "owner").pick(:identity_id)
111
+ )
112
+ end
113
+
114
+ def publish_account_cancelled
115
+ return if cancelled_at.nil?
116
+
117
+ Seams::Events::Publisher.publish(
118
+ "account.cancelled.accounts",
119
+ account_id: id,
120
+ cancelled_by_identity_id: Accounts::Current.membership&.identity_id
121
+ )
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Accounts
4
+ class ApplicationRecord < ::ApplicationRecord
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Accounts
4
+ # ActiveSupport::CurrentAttributes namespace for the Accounts engine.
5
+ # Set once per request by the host (typically a before_action that
6
+ # resolves the account from the URL or session); readable from
7
+ # anywhere downstream without explicit threading.
8
+ #
9
+ # Setting `account=` automatically derives the matching Membership
10
+ # for the currently signed-in identity (read from `Auth::Current`).
11
+ # The cross-engine read into `Auth::Current.identity` is intentional:
12
+ # the auth and accounts engines each own a per-request namespace;
13
+ # the accounts engine reads identity to figure out which membership
14
+ # is "current" but never writes to `Auth::Current`.
15
+ #
16
+ # Contract: `Auth::Current.identity` MUST be set before
17
+ # `Accounts::Current.account =`. Hosts wire this in their
18
+ # ApplicationController after their authentication concern runs.
19
+ class Current < ActiveSupport::CurrentAttributes
20
+ attribute :account, :membership
21
+
22
+ def account=(value)
23
+ super(value)
24
+ self.membership =
25
+ if value && defined?(Auth::Current) && Auth::Current.identity
26
+ value.memberships.find_by(identity_id: Auth::Current.identity.id)
27
+ end
28
+ end
29
+
30
+ def with_account(value, &)
31
+ with(account: value, &)
32
+ end
33
+
34
+ def without_account(&)
35
+ with(account: nil, membership: nil, &)
36
+ end
37
+ end
38
+ end