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,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # What: creates the team_memberships join table.
4
+ # Why: many-to-many between teams and Auth::Identity rows, plus a
5
+ # role column (owner / admin / member) for cheap RBAC. Teams
6
+ # are peers to Accounts post-Wave-9, so the join is to Identity
7
+ # directly — not to an Accounts::Membership.
8
+ # Risk: append-mostly. Unique index on (team_id, identity_id) prevents
9
+ # duplicate memberships. No FK to auth_identities because the
10
+ # auth and teams engines may live in different schemas /
11
+ # databases in production — cross-engine integrity is enforced
12
+ # at the application layer.
13
+ class CreateTeamMemberships < ActiveRecord::Migration[7.1]
14
+ def change
15
+ create_table :team_memberships do |t|
16
+ t.references :team, null: false, foreign_key: { to_table: :teams }, index: true
17
+ t.bigint :identity_id, null: false
18
+ t.string :role, null: false, default: "member"
19
+ t.timestamps
20
+ end
21
+
22
+ add_index :team_memberships, %i[team_id identity_id], unique: true
23
+ add_index :team_memberships, :identity_id
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # What: creates the teams table for the Teams engine.
4
+ # Why: the Team is the unit of multi-tenant ownership (billing
5
+ # customer, project scope, RBAC root) — every host that mounts
6
+ # Teams::Engine needs it.
7
+ # Risk: empty on creation, append-mostly thereafter.
8
+ class CreateTeams < ActiveRecord::Migration[7.1]
9
+ def change
10
+ create_table :teams do |t|
11
+ t.string :name, null: false
12
+ t.string :slug, null: false
13
+ t.timestamps
14
+ end
15
+
16
+ add_index :teams, :slug, unique: true
17
+ end
18
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Teams
6
+ # Mix into any model whose rows belong to a single team. Sets up
7
+ # the belongs_to + a default_scope that filters to
8
+ # `Teams::Current.team`. When `Teams::Current.team` is unset the
9
+ # scope returns `none` (an empty relation) — see "fail-closed"
10
+ # below.
11
+ #
12
+ # class Project < ApplicationRecord
13
+ # include Teams::AccountScoped
14
+ # end
15
+ #
16
+ # Teams::Current.team = team
17
+ # Project.create!(name: "...") # team_id auto-assigned to team.id
18
+ # Project.all # only team's projects
19
+ #
20
+ # Pairs with Core's `TenantScoped` — same shape, different ownership
21
+ # model: TenantScoped binds to the host's tenant abstraction;
22
+ # AccountScoped binds specifically to a Teams::Team.
23
+ #
24
+ # ### Fail-closed default
25
+ #
26
+ # When `Teams::Current.team` is nil, the default_scope returns
27
+ # `none` rather than `all`. Same rationale as Accounts::AccountScoped:
28
+ # the alternative (return all rows across all teams when no team is
29
+ # bound) is the canonical multi-team data-leak bug. Fail-closed
30
+ # surfaces "I have no rows" — a noisy symptom the developer fixes
31
+ # immediately — instead of a silent leak.
32
+ #
33
+ # Opt-out paths (use deliberately):
34
+ # * `.unscoped` — full Active Record bypass.
35
+ # * `.with_no_team_scope` — drops the team_id filter only,
36
+ # preserves any other default_scopes.
37
+ #
38
+ # Background jobs MUST set `Teams::Current.team =` before querying,
39
+ # or call `.with_no_team_scope` explicitly.
40
+ module AccountScoped
41
+ extend ActiveSupport::Concern
42
+
43
+ included do
44
+ belongs_to :team, class_name: "Teams::Team"
45
+
46
+ # The default_scope ALWAYS applies a where(:team_id) clause —
47
+ # either filtered to the current team, or filtered to a
48
+ # never-matching sentinel (`team_id IS NULL`). This shape lets
49
+ # `unscope(where: :team_id)` in `with_no_team_scope` reverse the
50
+ # filter cleanly; using `none` here would create a NullRelation
51
+ # that `unscope` cannot un-do.
52
+ default_scope -> {
53
+ team = Teams::Current.team
54
+ if team
55
+ where(team_id: team.id)
56
+ else
57
+ where(team_id: nil)
58
+ end
59
+ }
60
+ before_validation :assign_current_team, on: :create
61
+ end
62
+
63
+ class_methods do
64
+ # Returns the relation as it would be without the team_id
65
+ # filter, preserving any other default_scopes the model
66
+ # declares. Use deliberately — name-it-loud opt-out for seed
67
+ # scripts, migrations, and platform tooling.
68
+ def with_no_team_scope
69
+ unscope(where: :team_id)
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def assign_current_team
76
+ self.team_id ||= Teams::Current.team&.id if Teams::Current.team
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Teams
6
+ # Helpers for controllers that need to enforce team-membership
7
+ # checks. Mix in by default in TeamsController/MembershipsController/
8
+ # InvitationsController so the engine ships with safe defaults.
9
+ # Hosts can override the predicates by re-opening the concern.
10
+ #
11
+ # Resolves the current Identity via `current_identity_id`, which by
12
+ # default reads `Auth::Current.identity` (the Auth engine's
13
+ # per-request namespace). Hosts can override `current_identity_id`
14
+ # to plug in a different resolver.
15
+ module Authorization
16
+ extend ActiveSupport::Concern
17
+
18
+ private
19
+
20
+ def require_team_member!
21
+ head :forbidden and return false unless team_member?
22
+ true
23
+ end
24
+
25
+ def require_team_admin!
26
+ head :forbidden and return false unless team_admin?
27
+ true
28
+ end
29
+
30
+ def team_member?
31
+ return false unless current_identity_id && @team
32
+
33
+ Teams::Membership.exists?(team_id: @team.id, identity_id: current_identity_id)
34
+ end
35
+
36
+ def team_admin?
37
+ return false unless current_identity_id && @team
38
+
39
+ Teams::Membership.where(team_id: @team.id, identity_id: current_identity_id, role: %w[owner admin]).exists?
40
+ end
41
+
42
+ # Resolves the signed-in human's id from `Auth::Current.identity`
43
+ # (the Auth engine's per-request namespace). Gated on
44
+ # `defined?(Auth::Current)` so the concern remains include-safe in
45
+ # hosts that don't ship the auth engine. Hosts that wire auth
46
+ # differently override this method.
47
+ def current_identity_id
48
+ if defined?(Auth::Current) && Auth::Current.respond_to?(:identity) && Auth::Current.identity
49
+ return Auth::Current.identity.id
50
+ end
51
+
52
+ nil
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teams
4
+ # Engine-scoped configuration. Override in
5
+ # config/initializers/teams.rb of the host application.
6
+ class Configuration
7
+ attr_accessor :invitation_ttl, :max_members_per_team
8
+ attr_writer :host_url, :invitation_mailer_from
9
+ # Follow-up generators that add knobs (slug_generator, archive_grace_period) declare their attr_accessor here.
10
+ # seams:insertion-point teams.configuration.attributes
11
+
12
+ def initialize
13
+ @invitation_ttl = 7 * 24 * 60 * 60 # 7 days, in seconds
14
+ @max_members_per_team = nil # nil = unlimited
15
+ @invitation_mailer_from = nil
16
+ @host_url = nil
17
+ end
18
+
19
+ # `host_url` is used to build the invitation accept link. Without
20
+ # it, invitation emails would carry a link to nowhere, so we raise
21
+ # at the first read rather than ship a broken email. Set in
22
+ # config/initializers/teams.rb.
23
+ def host_url
24
+ return @host_url if @host_url
25
+
26
+ raise Teams::ConfigurationError,
27
+ "Teams.configuration.host_url is not set. Add\n" \
28
+ " Teams.configure { |c| c.host_url = \"https://your-app.com\" }\n" \
29
+ "to config/initializers/teams.rb so invitation emails can " \
30
+ "build the accept URL."
31
+ end
32
+
33
+ # `invitation_mailer_from` is the From: header on invitation emails.
34
+ # Same shape as `host_url` — nil-default, raises on read so the
35
+ # host can't accidentally ship mail from "no-reply@example.com".
36
+ def invitation_mailer_from
37
+ return @invitation_mailer_from if @invitation_mailer_from
38
+
39
+ raise Teams::ConfigurationError,
40
+ "Teams.configuration.invitation_mailer_from is not set. Add\n" \
41
+ " Teams.configure { |c| c.invitation_mailer_from = \"team@your-app.com\" }\n" \
42
+ "to config/initializers/teams.rb."
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teams
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Teams
6
+
7
+ config.generators do |g|
8
+ g.test_framework :rspec
9
+ end
10
+
11
+ initializer "teams.register_events" do
12
+ Seams::EventRegistry.register("team.created.teams", emitted_by: "Teams")
13
+ Seams::EventRegistry.register("team.member_joined.teams", emitted_by: "Teams")
14
+ Seams::EventRegistry.register("team.member_left.teams", emitted_by: "Teams")
15
+ Seams::EventRegistry.register("invitation.sent.teams", emitted_by: "Teams")
16
+ Seams::EventRegistry.register("invitation.accepted.teams", emitted_by: "Teams")
17
+ # Follow-up generators that emit new teams events (team.archived.teams, invitation.revoked.teams) register them here.
18
+ # seams:insertion-point teams.engine.events
19
+ end
20
+
21
+ initializer "teams.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
+ config.after_initialize do
30
+ # Boot-time dependency assertion. Teams::Membership.identity_id
31
+ # references auth_identities; without auth the team membership
32
+ # rows are dangling. We enforce at boot so the operator gets a
33
+ # clear "install seams:auth" error rather than a NULL identity_id
34
+ # surprise on first query.
35
+ unless defined?(::Auth::Identity)
36
+ raise <<~MSG
37
+ [seams teams] missing required cross-engine dependency:
38
+ Auth::Identity (run: bin/rails generate seams:auth)
39
+
40
+ The teams engine joins identity_id to auth_identities; it
41
+ cannot run without auth. Generate the auth engine, or
42
+ remove teams with `bin/rails generate seams:remove teams --force`.
43
+ MSG
44
+ end
45
+
46
+ Teams::InvitationSubscriber.attach!
47
+ # Follow-up generators that ship subscribers (Teams::AccountSubscriber.attach!, etc.) splice their attach! calls here.
48
+ # seams:insertion-point teams.engine.subscribers
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "teams/version"
4
+ require "teams/configuration"
5
+ require "teams/engine"
6
+ require "teams/concerns/authorization"
7
+
8
+ module Teams
9
+ class Error < StandardError; end
10
+ class AuthorizationError < Error; end
11
+ class ConfigurationError < Error; end
12
+
13
+ class << self
14
+ def configuration
15
+ @configuration ||= Configuration.new
16
+ end
17
+
18
+ def configure
19
+ yield configuration
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Factories for Teams engine specs. Sequences keep names unique
4
+ # across the spec run so uniqueness validations don't trip.
5
+ #
6
+ # `:team_membership` associates with the auth engine's
7
+ # `:auth_identity` factory — ensure the auth_identities table is
8
+ # present in the engine's dummy schema (it is, see
9
+ # `TeamsGenerator#dummy_schema`).
10
+ FactoryBot.define do
11
+ factory :team, class: "Teams::Team" do
12
+ sequence(:name) { |n| "Team #{n}" }
13
+ sequence(:slug) { |n| "team-#{n}" }
14
+ end
15
+
16
+ factory :team_membership, class: "Teams::Membership" do
17
+ association :team
18
+ role { "member" }
19
+
20
+ # Denormalises identity_id from a built/created :auth_identity
21
+ # rather than relying on belongs_to magic — Membership has no
22
+ # `belongs_to :identity` because Auth::Identity lives in a peer
23
+ # engine (no cross-engine model access at the ActiveRecord level,
24
+ # per the Seams/NoCrossEngineModelAccess cop). Hosts call
25
+ # `create(:team_membership, identity: existing_identity)` or pass
26
+ # `identity_id:` directly.
27
+ transient do
28
+ identity { nil }
29
+ end
30
+
31
+ after(:build) do |membership, evaluator|
32
+ membership.identity_id ||= evaluator.identity&.id || FactoryBot.create(:auth_identity).id
33
+ end
34
+
35
+ factory :team_admin_membership do
36
+ role { "admin" }
37
+ end
38
+ end
39
+
40
+ factory :team_invitation, class: "Teams::Invitation" do
41
+ association :team
42
+ sequence(:email) { |n| "invitee-#{n}@example.com" }
43
+ role { "member" }
44
+ token { SecureRandom.hex(16) }
45
+ expires_at { 7.days.from_now }
46
+ end
47
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_helper"
4
+
5
+ RSpec.describe Teams::Invitation do
6
+ let(:team) { Teams::Team.create!(name: "Acme") }
7
+
8
+ it "auto-assigns a token and expiry" do
9
+ inv = team.invitations.create!(email: "x@y.com", role: "member")
10
+ expect(inv.token).to be_present
11
+ expect(inv.expires_at).to be > Time.current
12
+ end
13
+
14
+ it "is expired once expires_at passes" do
15
+ inv = team.invitations.create!(email: "x@y.com", role: "member", expires_at: 1.minute.ago)
16
+ expect(inv).to be_expired
17
+ end
18
+
19
+ it "rejects two pending invitations to the same email for the same team" do
20
+ team.invitations.create!(email: "x@y.com", role: "member")
21
+ expect do
22
+ team.invitations.create!(email: "x@y.com", role: "admin")
23
+ end.to raise_error(ActiveRecord::RecordNotUnique)
24
+ end
25
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_helper"
4
+
5
+ RSpec.describe Teams::Membership do
6
+ let(:team) { Teams::Team.create!(name: "Acme") }
7
+
8
+ it "rejects unknown roles" do
9
+ m = described_class.new(team: team, identity_id: 1, role: "made_up")
10
+ expect(m).not_to be_valid
11
+ end
12
+
13
+ it "is unique on (team_id, identity_id)" do
14
+ described_class.create!(team: team, identity_id: 1, role: "member")
15
+ dup = described_class.new(team: team, identity_id: 1, role: "admin")
16
+ expect(dup).not_to be_valid
17
+ end
18
+
19
+ describe "#admin?" do
20
+ it "is true for owner and admin roles" do
21
+ expect(described_class.new(role: "owner")).to be_admin
22
+ expect(described_class.new(role: "admin")).to be_admin
23
+ end
24
+
25
+ it "is false for member" do
26
+ expect(described_class.new(role: "member")).not_to be_admin
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails_helper"
4
+
5
+ RSpec.describe Teams::Team do
6
+ describe "validations" do
7
+ it "requires a name" do
8
+ expect(described_class.new(name: "")).not_to be_valid
9
+ end
10
+
11
+ it "auto-assigns slug from name" do
12
+ team = described_class.new(name: "My Team!")
13
+ team.valid?
14
+ expect(team.slug).to eq("my-team")
15
+ end
16
+
17
+ it "rejects duplicate slugs" do
18
+ described_class.create!(name: "Acme")
19
+ dup = described_class.new(name: "Acme")
20
+ expect(dup).not_to be_valid
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../rails_helper"
4
+
5
+ RSpec.describe "Teams engine boot", type: :integration do
6
+ it "loads the engine" do
7
+ expect(defined?(Teams::Engine)).to eq("constant")
8
+ end
9
+
10
+ it "registers the five canonical team events" do
11
+ %w[
12
+ team.created.teams
13
+ team.member_joined.teams
14
+ team.member_left.teams
15
+ invitation.sent.teams
16
+ invitation.accepted.teams
17
+ ].each do |event|
18
+ expect(Seams::EventRegistry.registered?(event)).to be(true)
19
+ end
20
+ end
21
+
22
+ it "creates the team tables from the dummy schema" do
23
+ %i[teams team_memberships team_invitations].each do |t|
24
+ expect(ActiveRecord::Base.connection.table_exists?(t)).to be(true), "missing #{t}"
25
+ end
26
+ end
27
+
28
+ it "creates a Team and auto-assigns a slug" do
29
+ team = Teams::Team.create!(name: "Acme Corp")
30
+ expect(team.slug).to eq("acme-corp")
31
+ end
32
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "seams"
4
+
5
+ module Seams
6
+ module CLI
7
+ # Implementation behind `bin/rails seams:list` — discovers every
8
+ # engine under engines/, looks up its registered events in
9
+ # Seams::EventRegistry, and prints a summary.
10
+ class List
11
+ DEFAULT_ENGINES_ROOT = "engines"
12
+
13
+ MODULE_DECLARATION = /\bmodule\s+([A-Z][A-Za-z0-9_]*)\b/
14
+
15
+ def initialize(engines_root: DEFAULT_ENGINES_ROOT, output: $stdout)
16
+ @engines_root = engines_root
17
+ @output = output
18
+ end
19
+
20
+ def call
21
+ engines = discover_engines
22
+ @output.puts("seams: #{engines.size} engine(s) installed")
23
+
24
+ if engines.empty?
25
+ @output.puts(" (no engines — generate one with `bin/rails generate seams:engine <name>`)")
26
+ return
27
+ end
28
+
29
+ engines.each { |engine| print_engine(engine) }
30
+ end
31
+
32
+ private
33
+
34
+ def discover_engines
35
+ return [] unless Dir.exist?(@engines_root)
36
+
37
+ Dir.children(@engines_root)
38
+ .select { |child| File.directory?(File.join(@engines_root, child)) }
39
+ .reject { |child| child.start_with?(".") }
40
+ .sort
41
+ end
42
+
43
+ def print_engine(name)
44
+ @output.puts(" - #{name}")
45
+ events_for(name).each { |event| @output.puts(" emits: #{event}") }
46
+ subscriptions_for(name).each { |event| @output.puts(" subscribes: #{event}") }
47
+ depends_on(name).each { |dep| @output.puts(" depends on: #{dep}") }
48
+ end
49
+
50
+ def events_for(name)
51
+ module_name = module_name_for(name)
52
+ events = Seams::EventRegistry.all.select { |_, owner| owner.to_s == module_name }.keys
53
+ events.empty? ? ["(no events)"] : events
54
+ end
55
+
56
+ # Returns the event names this engine subscribes to. Looks in
57
+ # both the engine.rb itself AND in `app/subscribers/**/*.rb` —
58
+ # the canonical seams convention, where each subscriber class
59
+ # calls `Publisher.attach_class(KEY, "event.name", ...)` (or the
60
+ # legacy `attach_once`) from an `attach!` class method that the
61
+ # engine's `config.after_initialize` block invokes. Without
62
+ # scanning `app/subscribers/` this method silently reported zero
63
+ # subscribers for every generated engine, hiding the
64
+ # cross-engine dependency graph this command exists to surface.
65
+ def subscriptions_for(name)
66
+ subscription_sources_for(name)
67
+ .filter_map { |path| File.read(path) if File.exist?(path) }
68
+ .flat_map do |content|
69
+ content.scan(/Publisher\.(?:subscribe|attach_once|attach_class)\([^"']*["']([^"']+)["']/m)
70
+ .flatten
71
+ end
72
+ .uniq
73
+ end
74
+
75
+ def subscription_sources_for(name)
76
+ engine_rb = File.join(@engines_root, name, "lib", name, "engine.rb")
77
+ subscriber_glob = File.join(@engines_root, name, "app", "subscribers", "**", "*.rb")
78
+
79
+ # Dir.glob is already sorted on every supported Ruby version.
80
+ [engine_rb, *Dir.glob(subscriber_glob)]
81
+ end
82
+
83
+ # Walks the subscribe-list and resolves each event back to the
84
+ # engine that emits it (via the canonical "name.action.<engine>"
85
+ # naming convention). Returns the set of distinct engine names
86
+ # this one depends on.
87
+ def depends_on(name)
88
+ subscriptions_for(name)
89
+ .map { |event| event.split(".").last }
90
+ .reject { |dep| dep.nil? || dep == name }
91
+ .uniq
92
+ .sort
93
+ end
94
+
95
+ # Tries to find the engine's actual Ruby module name by reading
96
+ # its lib/<name>.rb. Falls back to a CamelCase conversion of the
97
+ # directory name (so `oauth2` becomes `Oauth2` rather than
98
+ # crashing) when the file is missing or doesn't declare a module.
99
+ def module_name_for(name)
100
+ root_file = File.join(@engines_root, name, "lib", "#{name}.rb")
101
+
102
+ if File.exist?(root_file)
103
+ match = File.read(root_file).match(MODULE_DECLARATION)
104
+ return match[1] if match
105
+ end
106
+
107
+ name.split("_").map(&:capitalize).join
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "seams"
4
+
5
+ module Seams
6
+ module CLI
7
+ # Runs the four quality gates the host's `bin/audit` script also
8
+ # runs (RuboCop, Brakeman, bundler-audit, SimpleCov collation), in
9
+ # one shot, and reports a unified summary. Each tool that is not
10
+ # installed in the host's bundle is skipped with a note rather
11
+ # than failing — hosts opt in by adding the gem.
12
+ #
13
+ # bin/rails seams:quality:all
14
+ #
15
+ # Returns true when every gate that ran passed; false otherwise.
16
+ class Quality
17
+ DEFAULT_ENGINES_ROOT = "engines"
18
+
19
+ def initialize(engines_root: DEFAULT_ENGINES_ROOT, output: $stdout)
20
+ @engines_root = engines_root
21
+ @output = output
22
+ @results = {}
23
+ end
24
+
25
+ def call
26
+ run_rubocop
27
+ run_brakeman
28
+ run_bundler_audit
29
+ run_simplecov_collation
30
+
31
+ print_summary
32
+ @results.values.all? { |status| %i[pass skipped].include?(status) }
33
+ end
34
+
35
+ private
36
+
37
+ def run_rubocop
38
+ if gem_installed?("rubocop")
39
+ @output.puts("=== rubocop --parallel ===")
40
+ @results[:rubocop] = system("bundle", "exec", "rubocop", "--parallel") ? :pass : :fail
41
+ else
42
+ @output.puts("rubocop not installed — skipping.")
43
+ @results[:rubocop] = :skipped
44
+ end
45
+ end
46
+
47
+ def run_brakeman
48
+ if gem_installed?("brakeman")
49
+ @output.puts("=== brakeman ===")
50
+ @results[:brakeman] =
51
+ system("bundle", "exec", "brakeman", "--no-pager", "--no-progress", "--quiet") ? :pass : :fail
52
+ else
53
+ @output.puts("brakeman not installed — skipping.")
54
+ @results[:brakeman] = :skipped
55
+ end
56
+ end
57
+
58
+ def run_bundler_audit
59
+ if gem_installed?("bundler-audit")
60
+ @output.puts("=== bundle-audit check --update ===")
61
+ @results[:bundler_audit] = system("bundle", "exec", "bundle-audit", "check", "--update") ? :pass : :fail
62
+ else
63
+ @output.puts("bundler-audit not installed — skipping.")
64
+ @results[:bundler_audit] = :skipped
65
+ end
66
+ end
67
+
68
+ # Calls the host's script/collate_coverage.rb (shipped by
69
+ # Phase 1.5). Skipped if the script is not present (older host)
70
+ # or no per-engine resultset files exist yet.
71
+ def run_simplecov_collation
72
+ script = "script/collate_coverage.rb"
73
+ unless File.exist?(script)
74
+ @output.puts("script/collate_coverage.rb not present — skipping coverage collation.")
75
+ @results[:coverage] = :skipped
76
+ return
77
+ end
78
+
79
+ @output.puts("=== ruby script/collate_coverage.rb ===")
80
+ @results[:coverage] = system("ruby", script) ? :pass : :fail
81
+ end
82
+
83
+ def gem_installed?(gem_name)
84
+ Gem::Specification.find_by_name(gem_name)
85
+ true
86
+ rescue Gem::LoadError
87
+ false
88
+ end
89
+
90
+ def print_summary
91
+ @output.puts("")
92
+ @output.puts("seams:quality summary")
93
+ @results.each do |gate, status|
94
+ @output.puts(" #{gate.to_s.ljust(15)} #{status}")
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end