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,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "rails/generators"
5
+ require "seams"
6
+ require "seams/generators/sibling_rubocop_writer"
7
+ require "seams/generators/host_injector"
8
+
9
+ module Seams
10
+ module Generators
11
+ # Removes an engine generated by `seams:engine`. Prompts for
12
+ # confirmation unless --force is passed. Cleans up the surviving
13
+ # engines' .rubocop.yml so OtherEngines no longer references the
14
+ # engine that was just removed. Reverses host file edits made by
15
+ # the canonical generators (mount line, includes) — leaves the
16
+ # Gemfile alone since other engines may share gem deps.
17
+ #
18
+ # Run with: bin/rails generate seams:remove billing [--force]
19
+ class RemoveGenerator < Rails::Generators::NamedBase
20
+ include Seams::Generators::HostInjector
21
+
22
+ # Same constraints as EngineGenerator — keeps `seams:remove ../../etc`
23
+ # from being interpreted as a relative path the destructive
24
+ # FileUtils.rm_rf at line 80 would happily follow with --force.
25
+ NAME_PATTERN = /\A[a-z][a-z0-9_]*\z/
26
+
27
+ class_option :force, type: :boolean, default: false,
28
+ desc: "Skip the confirmation prompt"
29
+
30
+ def validate_name
31
+ return if NAME_PATTERN.match?(name)
32
+
33
+ raise Seams::GeneratorError,
34
+ "Engine name #{name.inspect} must be lowercase letters, digits, " \
35
+ "and underscores, starting with a letter."
36
+ end
37
+
38
+ # Maps engine name -> { mount: <Class>, includes: { user: [...], application_controller: [...] } }
39
+ # Lets remove know what to undo for the canonical engines. Generic
40
+ # engines aren't in this table and just get the directory deleted.
41
+ CANONICAL_HOST_EDITS = {
42
+ "auth" => {
43
+ mount: "Auth::Engine",
44
+ user_includes: %w[Auth::Authenticatable],
45
+ application_controller_includes: %w[Auth::Authentication]
46
+ },
47
+ "notifications" => {
48
+ mount: "Notifications::Engine",
49
+ user_includes: %w[Notifications::Notifiable]
50
+ },
51
+ # Post-Wave-9: billing no longer injects Billing::Billable
52
+ # into the host User — the engine includes it into the
53
+ # configured tenant class (default Accounts::Account) at
54
+ # boot via Billing.configuration.billable_class. The only
55
+ # host-side edit billing makes is the mount line, so unmount
56
+ # is the only reverse-edit we need.
57
+ "billing" => {
58
+ mount: "Billing::Engine",
59
+ user_includes: %w[]
60
+ },
61
+ "teams" => {
62
+ # Wave 9 removed Teams::Teamable — there's no canonical host
63
+ # User concern to remove. The `mount Teams::Engine` line is
64
+ # the only host edit the teams generator makes, so unmount
65
+ # is the only reverse-edit we need here.
66
+ mount: "Teams::Engine",
67
+ user_includes: %w[]
68
+ },
69
+ # Wave 9 — accounts engine. Like billing/teams post-Wave-9, the
70
+ # accounts generator does NOT inject anything into the host User
71
+ # (the canonical demo doesn't have one). Mount is the only
72
+ # host-side edit, so unmount is the only reverse-edit we need.
73
+ "accounts" => {
74
+ mount: "Accounts::Engine",
75
+ user_includes: %w[]
76
+ },
77
+ # Wave 11A — admin engine. Mounts under Seams::Admin::Engine.
78
+ # The generator injects `gem "administrate"` and `gem "pundit"`
79
+ # into the host Gemfile but the remover does NOT prune them
80
+ # (the host may have other dashboards depending on either).
81
+ # Unmounting + dropping the engine dir is enough.
82
+ "admin" => {
83
+ mount: "Seams::Admin::Engine",
84
+ user_includes: %w[]
85
+ }
86
+ }.freeze
87
+
88
+ # Read the engine's create_table calls before its directory is
89
+ # deleted. Used by write_drop_table_migration to generate the
90
+ # reversal. Pattern-matches the literal ActiveRecord::Migration
91
+ # call so we drop exactly what the engine created — never
92
+ # host-side tables that happen to share the prefix.
93
+ def capture_engine_tables
94
+ engine_path = File.join(destination_root, "engines", name)
95
+ return @engine_tables = [] unless File.directory?(engine_path)
96
+
97
+ migrate_dir = File.join(engine_path, "db/migrate")
98
+ return @engine_tables = [] unless File.directory?(migrate_dir)
99
+
100
+ @engine_tables = Dir.glob(File.join(migrate_dir, "*.rb")).flat_map do |file|
101
+ # Strip line comments before scanning so a stray
102
+ # `# create_table :backup_table do |t|` in a migration
103
+ # doesn't end up in the drop-table list.
104
+ source = File.read(file).gsub(/^\s*#.*$/, "")
105
+ source.scan(/^\s*create_table\s+:(\w+)/).flatten
106
+ end.uniq
107
+ end
108
+
109
+ def remove_engine_directory
110
+ engine_path = File.join(destination_root, "engines", name)
111
+
112
+ unless File.directory?(engine_path)
113
+ @engine_was_present = false
114
+ say " skip engines/#{name}/ (not found)", :yellow
115
+ return
116
+ end
117
+
118
+ return unless options[:force] || confirm_removal?
119
+
120
+ FileUtils.rm_rf(engine_path)
121
+ @engine_was_present = true
122
+ say " remove engines/#{name}/", :red
123
+ end
124
+
125
+ # Phase 1.7 — generate a drop-table migration in the host's
126
+ # db/migrate so the host can run `bin/rails db:migrate` to
127
+ # reclaim the engine's tables. Idempotent if-table-exists
128
+ # check so re-running doesn't blow up on already-dropped tables.
129
+ def write_drop_table_migration
130
+ return unless @engine_was_present
131
+ return if @engine_tables.nil? || @engine_tables.empty?
132
+
133
+ migrate_dir = File.join(destination_root, "db/migrate")
134
+ FileUtils.mkdir_p(migrate_dir)
135
+
136
+ filename = "#{drop_migration_timestamp}_drop_#{name}_tables.rb"
137
+ migration_path = File.join(migrate_dir, filename)
138
+ File.write(migration_path, drop_table_migration_body)
139
+
140
+ say " create db/migrate/#{filename}", :green
141
+ say " → run `bin/rails db:migrate` to drop #{@engine_tables.size} table(s).", :yellow
142
+ end
143
+
144
+ # Phase 1.7 — re-run `bundle install` so the host's lockfile no
145
+ # longer references the removed engine's gem deps (the canonical
146
+ # generators each inject their own — bcrypt, faraday, etc; the
147
+ # remover doesn't touch the Gemfile because deps are usually
148
+ # shared, but a fresh `bundle install` keeps lockfile + Gemfile
149
+ # in sync if the host did prune anything by hand). Skipped when
150
+ # the engine was never present (no-op remove) or when there's
151
+ # no Gemfile to bundle against.
152
+ def run_bundle_install
153
+ return unless @engine_was_present
154
+ return unless File.exist?(File.join(destination_root, "Gemfile"))
155
+
156
+ say " run bundle install (post-remove sync)", :green
157
+ Dir.chdir(destination_root) do
158
+ system("bundle", "install", "--quiet") || say(" → bundle install failed; resolve manually.", :red)
159
+ end
160
+ end
161
+
162
+ def update_sibling_engines
163
+ return unless @engine_was_present
164
+
165
+ engines_root = File.join(destination_root, "engines")
166
+ return unless Dir.exist?(engines_root)
167
+
168
+ survivors = Dir.children(engines_root)
169
+ .select { |c| File.directory?(File.join(engines_root, c)) }
170
+ .reject { |c| c.start_with?(".") }
171
+ .sort
172
+
173
+ return if survivors.empty?
174
+
175
+ Seams::Generators::SiblingRubocopWriter.rewrite!(engines_root: engines_root, dirs: survivors)
176
+ say " update .rubocop.yml of #{survivors.size} sibling engine(s)", :green
177
+ end
178
+
179
+ def unwire_from_host
180
+ return unless @engine_was_present
181
+
182
+ unwire_generic_host_edits
183
+ unwire_canonical_host_edits(CANONICAL_HOST_EDITS[name])
184
+ end
185
+
186
+ private
187
+
188
+ def unwire_generic_host_edits
189
+ # Generic uninject — applies to every engine. The mount line +
190
+ # initializer were created by the generic engine generator.
191
+ host_uninject_mount(engine_class: "#{generic_module_name}::Engine")
192
+ host_remove_initializer
193
+ end
194
+
195
+ def unwire_canonical_host_edits(edits)
196
+ return unless edits
197
+
198
+ host_uninject_mount(engine_class: edits[:mount]) if edits[:mount]
199
+ Array(edits[:user_includes]).each do |concern|
200
+ host_uninject_include("app/models/user.rb", concern)
201
+ end
202
+ Array(edits[:application_controller_includes]).each do |concern|
203
+ host_uninject_include("app/controllers/application_controller.rb", concern)
204
+ end
205
+ end
206
+
207
+ def confirm_removal?
208
+ yes?("Remove engine `#{name}` and everything under engines/#{name}? [y/N]")
209
+ end
210
+
211
+ def generic_module_name
212
+ name.split("_").map(&:capitalize).join
213
+ end
214
+
215
+ def host_remove_initializer
216
+ path = File.join(destination_root, "config/initializers/#{name}.rb")
217
+ return unless File.exist?(path)
218
+
219
+ FileUtils.rm(path)
220
+ say " remove config/initializers/#{name}.rb", :red
221
+ end
222
+
223
+ def drop_migration_timestamp
224
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
225
+ end
226
+
227
+ def drop_table_migration_body
228
+ drops = @engine_tables.map do |table|
229
+ %( drop_table :#{table}, force: :cascade if table_exists?(:#{table}))
230
+ end.join("\n")
231
+
232
+ rails_version = Rails::VERSION::STRING.split(".")[0, 2].join(".") if defined?(Rails::VERSION::STRING)
233
+ rails_version ||= "7.1"
234
+
235
+ <<~RB
236
+ # frozen_string_literal: true
237
+ #
238
+ # Drops every table created by the #{name} engine. Generated by
239
+ # `bin/rails generate seams:remove #{name}`. Run with:
240
+ #
241
+ # bin/rails db:migrate
242
+ #
243
+ # The if-table-exists guards make re-running safe.
244
+ class Drop#{name.classify}Tables < ActiveRecord::Migration[#{rails_version}]
245
+ def up
246
+ #{drops}
247
+ end
248
+
249
+ def down
250
+ raise ActiveRecord::IrreversibleMigration,
251
+ "Cannot recreate #{name} engine tables — re-run " \\
252
+ "`bin/rails generate seams:#{name}` to recreate the engine."
253
+ end
254
+ end
255
+ RB
256
+ end
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,298 @@
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 Teams engine on top of the generic engine
14
+ # scaffold. Models cover Team, Membership, Invitation; controllers
15
+ # cover team CRUD + membership management + invitation send/accept.
16
+ #
17
+ # Wave 9 model: Teams is a peer to Accounts (not nested). A
18
+ # Teams::Membership joins Auth::Identity directly to a Teams::Team.
19
+ # The host-User Teamable concern is gone — Wave 9 dropped the
20
+ # canonical demo's host User, so there's nowhere to mix it into.
21
+ #
22
+ # Run with: bin/rails generate seams:teams
23
+ class TeamsGenerator < Rails::Generators::Base
24
+ include Seams::Generators::HostInjector
25
+ include Seams::Generators::EjectAware
26
+
27
+ source_root File.expand_path("templates", __dir__)
28
+
29
+ ENGINE_NAME = "teams"
30
+ DEFAULT_FEATURES = %w[invitations roles].freeze
31
+
32
+ class_option :with, type: :string, default: "all",
33
+ desc: "Comma-separated features to enable: invitations,roles (or 'all')"
34
+
35
+ def create_base_engine
36
+ EngineGenerator.start([ENGINE_NAME], destination_root: destination_root)
37
+ end
38
+
39
+ def overwrite_engine_entry_point
40
+ # engine.rb / lib/teams.rb stay framework-managed.
41
+ template "lib/engine.rb.tt", engine_path("lib/teams/engine.rb"), force: true
42
+ template "lib/teams.rb.tt", engine_path("lib/teams.rb"), force: true
43
+ template_unless_ejected "lib/configuration.rb.tt",
44
+ engine_path("lib/teams/configuration.rb")
45
+ end
46
+
47
+ def overwrite_routes
48
+ template_unless_ejected "config/routes.rb.tt", engine_path("config/routes.rb"), force: true
49
+ end
50
+
51
+ def create_models
52
+ template_unless_ejected "app/models/application_record.rb.tt",
53
+ engine_path("app/models/teams/application_record.rb")
54
+ template_unless_ejected "app/models/team.rb.tt",
55
+ engine_path("app/models/teams/team.rb")
56
+ template_unless_ejected "app/models/membership.rb.tt",
57
+ engine_path("app/models/teams/membership.rb")
58
+ template_unless_ejected "app/models/current.rb.tt",
59
+ engine_path("app/models/teams/current.rb")
60
+ return unless features.include?("invitations")
61
+
62
+ template_unless_ejected "app/models/invitation.rb.tt",
63
+ engine_path("app/models/teams/invitation.rb")
64
+ end
65
+
66
+ def create_controllers
67
+ template_unless_ejected "app/controllers/teams_controller.rb.tt",
68
+ engine_path("app/controllers/teams/teams_controller.rb")
69
+ template_unless_ejected "app/controllers/memberships_controller.rb.tt",
70
+ engine_path("app/controllers/teams/memberships_controller.rb")
71
+ return unless features.include?("invitations")
72
+
73
+ template_unless_ejected "app/controllers/invitations_controller.rb.tt",
74
+ engine_path("app/controllers/teams/invitations_controller.rb")
75
+ end
76
+
77
+ # Phase 4A (2/2) — bare-bones views so the engine renders out
78
+ # of the box. Hosts override by dropping files at
79
+ # app/views/teams/teams/* in their own tree.
80
+ def create_views
81
+ %w[index show new edit].each do |action|
82
+ template_unless_ejected "app/views/teams/#{action}.html.erb.tt",
83
+ engine_path("app/views/teams/teams/#{action}.html.erb")
84
+ end
85
+ template_unless_ejected "app/views/memberships/index.html.erb.tt",
86
+ engine_path("app/views/teams/memberships/index.html.erb")
87
+ return unless features.include?("invitations")
88
+
89
+ template_unless_ejected "app/views/invitations/index.html.erb.tt",
90
+ engine_path("app/views/teams/invitations/index.html.erb")
91
+ end
92
+
93
+ def create_concerns
94
+ # Phase 4A — account scoping helper that pairs with Core's
95
+ # TenantScoped. Mix into models that belong to a single team.
96
+ template_unless_ejected "lib/concerns/account_scoped.rb.tt",
97
+ engine_path("lib/teams/concerns/account_scoped.rb")
98
+ # `--with=roles` ships role-based controller filters.
99
+ return unless features.include?("roles")
100
+
101
+ template_unless_ejected "lib/concerns/authorization.rb.tt",
102
+ engine_path("lib/teams/concerns/authorization.rb")
103
+ end
104
+
105
+ def create_jobs
106
+ template_unless_ejected "app/jobs/application_job.rb.tt",
107
+ engine_path("app/jobs/teams/application_job.rb")
108
+ end
109
+
110
+ def create_mailer_and_subscriber
111
+ return unless features.include?("invitations")
112
+
113
+ template_unless_ejected "app/mailers/invitation_mailer.rb.tt",
114
+ engine_path("app/mailers/teams/invitation_mailer.rb")
115
+ template_unless_ejected "app/views/invitation_mailer/invite.text.erb.tt",
116
+ engine_path("app/views/teams/invitation_mailer/invite.text.erb")
117
+ template_unless_ejected "app/subscribers/invitation_subscriber.rb.tt",
118
+ engine_path("app/subscribers/teams/invitation_subscriber.rb")
119
+ end
120
+
121
+ def create_migrations
122
+ template "db/migrate/create_teams.rb.tt",
123
+ engine_path("db/migrate/#{timestamp(0)}_create_teams.rb")
124
+ template "db/migrate/create_team_memberships.rb.tt",
125
+ engine_path("db/migrate/#{timestamp(1)}_create_team_memberships.rb")
126
+ return unless features.include?("invitations")
127
+
128
+ template "db/migrate/create_team_invitations.rb.tt",
129
+ engine_path("db/migrate/#{timestamp(2)}_create_team_invitations.rb")
130
+ end
131
+
132
+ def create_specs
133
+ template_unless_ejected "spec/models/team_spec.rb.tt",
134
+ engine_path("spec/models/teams/team_spec.rb")
135
+ template_unless_ejected "spec/models/membership_spec.rb.tt",
136
+ engine_path("spec/models/teams/membership_spec.rb")
137
+ # Phase 4A — factories live alongside the model specs so any
138
+ # spec can `create(:team)` without rolling its own fixture.
139
+ template_unless_ejected "spec/factories/teams.rb.tt",
140
+ engine_path("spec/factories/teams.rb")
141
+ return unless features.include?("invitations")
142
+
143
+ template_unless_ejected "spec/models/invitation_spec.rb.tt",
144
+ engine_path("spec/models/teams/invitation_spec.rb")
145
+ end
146
+
147
+ def overwrite_readme
148
+ template "README.md.tt", engine_path("README.md"), force: true
149
+ end
150
+
151
+ def update_exposed_concerns
152
+ rubocop_path = engine_path(".rubocop.yml")
153
+ return unless File.exist?(rubocop_path)
154
+
155
+ contents = File.read(rubocop_path)
156
+ exposed_lines = [" - Teams::AccountScoped"]
157
+ exposed_lines << " - Teams::Authorization" if features.include?("roles")
158
+ replacement = " ExposedConcerns:\n#{exposed_lines.join("\n")}"
159
+ contents.sub!(/^ ExposedConcerns: \[\]$/, replacement)
160
+ File.write(rubocop_path, contents)
161
+ end
162
+
163
+ def create_dummy_app
164
+ # Wave 9: no host User model in the dummy. The dummy ships a
165
+ # slim Auth::Identity stub at app/models/auth/identity.rb so
166
+ # the teams engine's boot-time dependency assertion (defined?
167
+ # ::Auth::Identity) passes without pulling in the full auth
168
+ # engine. The same stub also lets `create(:auth_identity)`
169
+ # build real AR rows against the auth_identities table baked
170
+ # into dummy_schema.
171
+ Seams::Generators::DummyAppWriter.write!(
172
+ engine_path: File.join(destination_root, "engines", ENGINE_NAME),
173
+ engine_module: "Teams",
174
+ mount_at: "/teams",
175
+ schema: dummy_schema,
176
+ host_user: dummy_host_identity,
177
+ host_user_path: "app/models/auth/identity.rb"
178
+ )
179
+ template "spec/runtime/boot_spec.rb.tt",
180
+ engine_path("spec/runtime/teams_boot_spec.rb")
181
+ end
182
+
183
+ def wire_into_host
184
+ # factory_bot_rails powers spec/factories/teams.rb. Lives in
185
+ # the host's test group only.
186
+ host_inject_gem("factory_bot_rails", "~> 6.4", group: :test)
187
+ host_inject_mount(engine_class: "Teams::Engine", at: "/teams")
188
+ # NB: no host_inject_include_in_user — the host User is gone
189
+ # post-Wave-9. Hosts that DO keep a User model and want
190
+ # team-membership query helpers wire those up themselves.
191
+ end
192
+
193
+ def report_summary
194
+ say ""
195
+ say " Teams engine generated at engines/teams/", :green
196
+ say ""
197
+ say " Next steps:", :yellow
198
+ say " 1. bin/rails db:migrate"
199
+ say " 2. Run the engine specs: bin/rails seams:test[teams]"
200
+ say ""
201
+ end
202
+
203
+ private
204
+
205
+ # Resolved feature list from --with. "all" (or empty / unrecognised)
206
+ # → invitations + roles. Garbage / unknown values fall back to all
207
+ # so the engine ships fully wired by default. Memoised so ERB
208
+ # branches stay consistent across the generator run.
209
+ def features
210
+ @features ||= begin
211
+ raw = options[:with].to_s.downcase.strip
212
+ if raw.empty? || raw == "all"
213
+ DEFAULT_FEATURES.dup
214
+ else
215
+ requested = raw.split(",").map(&:strip).reject(&:empty?)
216
+ allowed = requested & DEFAULT_FEATURES
217
+ allowed.empty? ? DEFAULT_FEATURES.dup : allowed
218
+ end
219
+ end
220
+ end
221
+
222
+ def engine_path(relative)
223
+ File.join(destination_root, "engines", ENGINE_NAME, relative)
224
+ end
225
+
226
+ # Offset by 300 to avoid collisions with the other canonical
227
+ # engines (auth +0/+1, notifications +100, billing +200/+201/+202).
228
+ def timestamp(offset)
229
+ base = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
230
+ (base + 300 + offset).to_s
231
+ end
232
+
233
+ # Slim Auth::Identity stub for the teams dummy app. Stands in
234
+ # for the real Auth::Identity (which lives in the auth engine,
235
+ # not loaded by the dummy) so the teams engine's boot-time
236
+ # cross-engine dependency assertion passes and specs that
237
+ # `create(:auth_identity)` get a real Active Record row backing
238
+ # the auth_identities table.
239
+ def dummy_host_identity
240
+ <<~RB
241
+ # frozen_string_literal: true
242
+ module Auth
243
+ class Identity < ApplicationRecord
244
+ self.table_name = "auth_identities"
245
+ has_secure_password
246
+ end
247
+ end
248
+ RB
249
+ end
250
+
251
+ # Includes auth_identities so factories that link memberships to
252
+ # an Identity can `create(:auth_identity)` against a real row.
253
+ # Match the auth engine's schema for that table so cross-engine
254
+ # specs don't drift.
255
+ def dummy_schema
256
+ <<~SCHEMA
257
+ create_table :auth_identities do |t|
258
+ t.text :email, null: false
259
+ t.string :password_digest, null: false
260
+ t.boolean :staff, null: false, default: false
261
+ t.timestamps
262
+ end
263
+ add_index :auth_identities, :email, unique: true
264
+ add_index :auth_identities, :staff, where: "staff = true"
265
+
266
+ create_table :teams do |t|
267
+ t.string :name, null: false
268
+ t.string :slug, null: false
269
+ t.timestamps
270
+ end
271
+ add_index :teams, :slug, unique: true
272
+
273
+ create_table :team_memberships do |t|
274
+ t.references :team, null: false
275
+ t.bigint :identity_id, null: false
276
+ t.string :role, null: false, default: "member"
277
+ t.timestamps
278
+ end
279
+ add_index :team_memberships, %i[team_id identity_id], unique: true
280
+ add_index :team_memberships, :identity_id
281
+
282
+ create_table :team_invitations do |t|
283
+ t.references :team, null: false
284
+ t.string :email, null: false
285
+ t.string :token, null: false
286
+ t.string :role, null: false, default: "member"
287
+ t.datetime :expires_at, null: false
288
+ t.datetime :accepted_at
289
+ t.timestamps
290
+ end
291
+ add_index :team_invitations, :token, unique: true
292
+ add_index :team_invitations, %i[team_id email], unique: true,
293
+ where: "accepted_at IS NULL"
294
+ SCHEMA
295
+ end
296
+ end
297
+ end
298
+ end
@@ -0,0 +1,88 @@
1
+ # Teams
2
+
3
+ > Multi-tenant teams + memberships + invitations for a Seams-powered host.
4
+
5
+ **Requires:** the `auth` engine. `Teams::Membership.identity_id`
6
+ joins to `auth_identities`. The `Teams::Authorization` concern
7
+ reads `Auth::Current.identity` for membership checks; without auth
8
+ installed, `current_identity_id` returns nil and team-admin gates
9
+ return 403 unconditionally. Install auth before teams.
10
+
11
+ ## Model
12
+
13
+ A `Teams::Team` is a peer to `Accounts::Account` — **not** nested
14
+ inside one. A `Teams::Membership` is a `(team_id, identity_id, role)`
15
+ join row that links an `Auth::Identity` directly to a `Teams::Team`.
16
+
17
+ If your application wants "Team belongs to Account" semantics, wire
18
+ that yourself with a host-side migration that adds an `account_id`
19
+ column to `teams`. The Teams engine deliberately stays out of the
20
+ Account/Tenant question: hosts that don't have Accounts (e.g. a B2C
21
+ SaaS that uses Teams as standalone groups) work without any further
22
+ plumbing.
23
+
24
+ `Auth::Identity` is referenced by id only (`identity_id` is a bare
25
+ bigint column on `team_memberships`, no `belongs_to :identity`). The
26
+ Teams engine never joins to `auth_identities` at the ActiveRecord
27
+ level — cross-engine integrity is enforced at the application layer
28
+ so Teams can move to a separate database in the future.
29
+
30
+ ## Events emitted
31
+
32
+ | Event name | Payload | Emitted when |
33
+ | --- | --- | --- |
34
+ | `team.created.teams` | `{ team_id:, creator_identity_id: }` | TeamsController#create succeeds |
35
+ | `team.member_joined.teams` | `{ team_id:, identity_id:, role: }` | MembershipsController#create succeeds |
36
+ | `team.member_left.teams` | `{ team_id:, identity_id: }` | MembershipsController#destroy runs |
37
+ | `invitation.sent.teams` | `{ invitation_id:, team_id:, email:, role:, token: }` | InvitationsController#create succeeds |
38
+ | `invitation.accepted.teams` | `{ team_id:, identity_id:, invitation_id: }` | InvitationsController#accept succeeds |
39
+
40
+ ## Events consumed
41
+
42
+ | Event name | Subscriber | What it does |
43
+ | --- | --- | --- |
44
+ | `invitation.sent.teams` | `Teams::InvitationSubscriber` | Looks up the invitation by id and enqueues `Teams::InvitationMailer.invite(invitation_id).deliver_later`. The host overrides the email body at `app/views/teams/invitation_mailer/invite.text.erb`. |
45
+
46
+ ## Exposed concerns
47
+
48
+ | Concern | Purpose |
49
+ | --- | --- |
50
+ | `Teams::Authorization` | Mixed into engine controllers; provides `require_team_member!` and `require_team_admin!`. Resolves the current human via `current_identity_id`, which by default reads `Auth::Current.identity` (the Auth engine's per-request namespace). Override `current_identity_id` to plug in a different resolver. |
51
+ | `Teams::AccountScoped` | Mix into host models that belong to a single team. Sets up `belongs_to :team` + a `default_scope` on `Teams::Current.team`. |
52
+
53
+ > **Wave 9 note.** The `Teams::Teamable` host-User concern was removed.
54
+ > Wave 9 dropped the canonical demo's host User model: hosts that
55
+ > still maintain one are responsible for adding any
56
+ > `team_memberships`-keyed helper methods themselves (querying by
57
+ > `Teams::Membership.where(identity_id: …)`).
58
+
59
+ ## Roles
60
+
61
+ | Role | Capabilities |
62
+ | --- | --- |
63
+ | `owner` | Everything an admin can, plus deleting the team. |
64
+ | `admin` | Manage memberships and invitations. |
65
+ | `member` | Read-only by default. |
66
+
67
+ Teams roles are intentionally independent of Accounts roles — a Team
68
+ is its own RBAC unit. Hosts that want a single role across both
69
+ should denormalise that themselves.
70
+
71
+ The engine ships role enforcement at the model level (`Membership#admin?`).
72
+ Authorization in controllers is the host's responsibility — Seams gives
73
+ you the data and the events; opinion-free on which authz library you use.
74
+
75
+ ## Mounting
76
+
77
+ ```ruby
78
+ # config/routes.rb (host application)
79
+ Rails.application.routes.draw do
80
+ mount Teams::Engine, at: "/teams"
81
+ end
82
+ ```
83
+
84
+ ## Running the specs
85
+
86
+ ```bash
87
+ bin/rails seams:test[teams]
88
+ ```