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,604 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../rails_helper"
4
+
5
+ # Boot spec for the seams admin engine. Wave 11A — Phase 1 (foundation)
6
+ # + Phase 2 (dashboards). Covers:
7
+ #
8
+ # - the engine constant loads
9
+ # - the four configuration knobs are wired with sensible defaults
10
+ # - the default authenticator returns truthy for a staff Identity
11
+ # and falsey for a non-staff Identity
12
+ # - the authenticator concern is present and gates correctly
13
+ # - Seams::Admin::ApplicationController inherits from
14
+ # Administrate::ApplicationController (the real gem; the Phase 1
15
+ # stub is gone now that `gem "administrate"` is in the Gemfile)
16
+ # - every Phase 2 dashboard class loads
17
+ # - every Phase 2 dashboard exposes a non-empty ATTRIBUTE_TYPES
18
+ # - the engine's routes contain mount points for each of the twelve
19
+ # dashboards
20
+ #
21
+ # Phase 3 will add Pundit policy specs once the platform-vs-tenant
22
+ # split lands.
23
+ RSpec.describe "Seams::Admin engine boot", type: :integration do
24
+ it "loads the engine constant" do
25
+ expect(defined?(Seams::Admin::Engine)).to eq("constant")
26
+ end
27
+
28
+ it "exposes the configuration class + the configure helper" do
29
+ expect(defined?(Seams::Admin::Configuration)).to eq("constant")
30
+ expect(Seams::Admin).to respond_to(:configure)
31
+ expect(Seams::Admin).to respond_to(:configuration)
32
+ expect(Seams::Admin).to respond_to(:config)
33
+ end
34
+
35
+ it "exposes the Authenticator concern" do
36
+ expect(defined?(Seams::Admin::Authenticator)).to eq("constant")
37
+ end
38
+
39
+ it "creates the schema tables the admin engine reads from" do
40
+ %i[
41
+ auth_identities
42
+ accounts
43
+ accounts_memberships
44
+ teams
45
+ team_memberships
46
+ team_invitations
47
+ notifications
48
+ notification_preferences
49
+ notification_deliveries
50
+ billing_plans
51
+ billing_subscriptions
52
+ billing_invoices
53
+ billing_lifetime_passes
54
+ billing_webhook_events
55
+ core_audit_logs
56
+ ].each do |t|
57
+ expect(ActiveRecord::Base.connection.table_exists?(t)).to be(true), "missing #{t}"
58
+ end
59
+ end
60
+
61
+ describe "Seams::Admin::Configuration defaults" do
62
+ let(:config) { Seams::Admin::Configuration.new }
63
+
64
+ it "ships the documented knobs" do
65
+ expect(config).to respond_to(:authenticator)
66
+ expect(config).to respond_to(:tenancy_scope)
67
+ expect(config).to respond_to(:theme_css_path)
68
+ expect(config).to respond_to(:before_admin_action)
69
+ # Phase 4 knob — host-pluggable resolver for the active
70
+ # Accounts::Membership.
71
+ expect(config).to respond_to(:current_membership_resolver)
72
+ end
73
+
74
+ it "defaults current_membership_resolver to a callable that returns nil when Accounts::Current is undefined" do
75
+ expect(config.current_membership_resolver).to respond_to(:call)
76
+ end
77
+
78
+ it "defaults tenancy_scope to :platform" do
79
+ expect(config.tenancy_scope).to eq(:platform)
80
+ end
81
+
82
+ it "defaults theme_css_path to nil" do
83
+ expect(config.theme_css_path).to be_nil
84
+ end
85
+
86
+ it "defaults before_admin_action to nil" do
87
+ expect(config.before_admin_action).to be_nil
88
+ end
89
+
90
+ it "defaults the authenticator to a callable that gates on staff?" do
91
+ expect(config.authenticator).to respond_to(:call)
92
+ end
93
+ end
94
+
95
+ describe "default authenticator behaviour" do
96
+ let(:gate) { Seams::Admin::Configuration.new.authenticator }
97
+ let(:staff_identity) { Auth::Identity.create!(email: "staff-#{SecureRandom.hex(4)}@example.com", password: "verysecret", staff: true) }
98
+ let(:other_identity) { Auth::Identity.create!(email: "other-#{SecureRandom.hex(4)}@example.com", password: "verysecret", staff: false) }
99
+ let(:staff_ctrl) { Struct.new(:current_identity).new(staff_identity) }
100
+ let(:other_ctrl) { Struct.new(:current_identity).new(other_identity) }
101
+ let(:nil_ctrl) { Struct.new(:current_identity).new(nil) }
102
+
103
+ it "returns truthy for a staff Identity" do
104
+ expect(gate.call(staff_ctrl)).to be(true)
105
+ end
106
+
107
+ it "returns falsey for a non-staff Identity" do
108
+ expect(gate.call(other_ctrl)).to be(false)
109
+ end
110
+
111
+ it "returns falsey when no Identity is signed in" do
112
+ expect(gate.call(nil_ctrl)).to be_nil.or be(false)
113
+ end
114
+ end
115
+
116
+ describe "Seams::Admin.configure" do
117
+ it "lets the host override the authenticator" do
118
+ original = Seams::Admin.config.authenticator
119
+ Seams::Admin.configure { |c| c.authenticator = ->(_ctrl) { :overridden } }
120
+ expect(Seams::Admin.config.authenticator.call(nil)).to eq(:overridden)
121
+ ensure
122
+ Seams::Admin.config.authenticator = original
123
+ end
124
+
125
+ it "lets the host switch tenancy_scope to :tenant" do
126
+ original = Seams::Admin.config.tenancy_scope
127
+ Seams::Admin.configure { |c| c.tenancy_scope = :tenant }
128
+ expect(Seams::Admin.config.tenancy_scope).to eq(:tenant)
129
+ ensure
130
+ Seams::Admin.config.tenancy_scope = original
131
+ end
132
+ end
133
+
134
+ # Phase 2 — verify the real Administrate gem is loaded (Phase 1's
135
+ # stub is gone) and every dashboard class is reachable. We assert
136
+ # the inheritance chain rather than just the constant — a stub that
137
+ # shadowed Administrate would still satisfy `defined?(...)`, but
138
+ # the ancestors list would be wrong.
139
+ describe "Administrate integration (Phase 2)" do
140
+ let(:dashboard_classes) do
141
+ %w[
142
+ Admin::IdentityDashboard
143
+ Admin::AccountDashboard
144
+ Admin::AccountsMembershipDashboard
145
+ Admin::TeamDashboard
146
+ Admin::TeamsMembershipDashboard
147
+ Admin::InvitationDashboard
148
+ Admin::NotificationDashboard
149
+ Admin::NotificationPreferenceDashboard
150
+ Admin::PlanDashboard
151
+ Admin::SubscriptionDashboard
152
+ Admin::InvoiceDashboard
153
+ Admin::LifetimePassDashboard
154
+ ]
155
+ end
156
+
157
+ it "Seams::Admin::ApplicationController inherits from Administrate::ApplicationController" do
158
+ expect(Seams::Admin::ApplicationController.ancestors).to include(::Administrate::ApplicationController)
159
+ end
160
+
161
+ it "loads every Phase 2 dashboard and each subclasses Administrate::BaseDashboard" do
162
+ dashboard_classes.each do |class_name|
163
+ klass = class_name.constantize
164
+ expect(klass.ancestors).to include(::Administrate::BaseDashboard), "#{class_name} not loaded"
165
+ end
166
+ end
167
+
168
+ it "every Phase 2 dashboard declares a non-empty ATTRIBUTE_TYPES" do
169
+ dashboard_classes.each do |class_name|
170
+ klass = class_name.constantize
171
+ expect(klass::ATTRIBUTE_TYPES).to be_a(Hash)
172
+ expect(klass::ATTRIBUTE_TYPES).not_to be_empty
173
+ end
174
+ end
175
+ end
176
+
177
+ describe "engine routes (Phase 2)" do
178
+ let(:route_paths) do
179
+ Seams::Admin::Engine.routes.routes.map { |r| r.path.spec.to_s }
180
+ end
181
+
182
+ %w[
183
+ /identities
184
+ /accounts
185
+ /accounts_memberships
186
+ /teams
187
+ /teams_memberships
188
+ /invitations
189
+ /notifications
190
+ /notification_preferences
191
+ /plans
192
+ /subscriptions
193
+ /invoices
194
+ /lifetime_passes
195
+ ].each do |segment|
196
+ it "mounts #{segment}" do
197
+ expect(route_paths.any? { |p| p.start_with?(segment) }).to be(true), "missing route #{segment}"
198
+ end
199
+ end
200
+ end
201
+
202
+ # Phase 3 — Pundit policies under Admin::Platform::* and
203
+ # Admin::Tenant::*, plus the audit-log auto-write on
204
+ # create/update/destroy.
205
+ describe "Pundit integration (Phase 3)" do
206
+ let(:platform_policy_classes) do
207
+ %w[
208
+ Admin::Platform::ApplicationPolicy
209
+ Admin::Platform::IdentityPolicy
210
+ Admin::Platform::AccountPolicy
211
+ Admin::Platform::AccountsMembershipPolicy
212
+ Admin::Platform::TeamPolicy
213
+ Admin::Platform::TeamsMembershipPolicy
214
+ Admin::Platform::InvitationPolicy
215
+ Admin::Platform::NotificationPolicy
216
+ Admin::Platform::NotificationPreferencePolicy
217
+ Admin::Platform::PlanPolicy
218
+ Admin::Platform::SubscriptionPolicy
219
+ Admin::Platform::InvoicePolicy
220
+ Admin::Platform::LifetimePassPolicy
221
+ ]
222
+ end
223
+
224
+ let(:tenant_policy_classes) do
225
+ %w[
226
+ Admin::Tenant::ApplicationPolicy
227
+ Admin::Tenant::IdentityPolicy
228
+ Admin::Tenant::AccountPolicy
229
+ Admin::Tenant::AccountsMembershipPolicy
230
+ Admin::Tenant::TeamPolicy
231
+ Admin::Tenant::TeamsMembershipPolicy
232
+ Admin::Tenant::InvitationPolicy
233
+ Admin::Tenant::NotificationPolicy
234
+ Admin::Tenant::NotificationPreferencePolicy
235
+ Admin::Tenant::PlanPolicy
236
+ Admin::Tenant::SubscriptionPolicy
237
+ Admin::Tenant::InvoicePolicy
238
+ Admin::Tenant::LifetimePassPolicy
239
+ ]
240
+ end
241
+
242
+ it "Seams::Admin::ApplicationController includes Pundit::Authorization" do
243
+ # Both ancestors are expected: Administrate::Punditize itself
244
+ # `include`s Pundit::Authorization, so the controller transitively
245
+ # picks it up. Asserting both pins the canonical wiring.
246
+ expect(Seams::Admin::ApplicationController.ancestors).to include(::Pundit::Authorization)
247
+ expect(Seams::Admin::ApplicationController.ancestors).to include(::Administrate::Punditize)
248
+ end
249
+
250
+ it "policy_namespace returns the Array-form Pundit expects" do
251
+ # `Administrate::Punditize` calls
252
+ # `Pundit.policy!(user, [*policy_namespace, resource])` — the
253
+ # namespace MUST be an Array of constants (or symbols), not a
254
+ # single Module. A Module-form namespace silently falls back to
255
+ # the top-level `<Resource>Policy` lookup, which doesn't exist
256
+ # in this engine. Pin the shape.
257
+ ctrl = Seams::Admin::ApplicationController.allocate
258
+ original = Seams::Admin.config.tenancy_scope
259
+ begin
260
+ Seams::Admin.config.tenancy_scope = :platform
261
+ expect(ctrl.policy_namespace).to be_an(Array)
262
+ expect(ctrl.policy_namespace.first).to eq(::Admin::Platform)
263
+
264
+ Seams::Admin.config.tenancy_scope = :tenant
265
+ expect(ctrl.policy_namespace.first).to eq(::Admin::Tenant)
266
+ ensure
267
+ Seams::Admin.config.tenancy_scope = original
268
+ end
269
+ end
270
+
271
+ it "exposes Seams::Admin::Context for the pundit_user wrapper" do
272
+ expect(defined?(Seams::Admin::Context)).to eq("constant")
273
+ ctx = Seams::Admin::Context.new(nil, nil)
274
+ expect(ctx).to respond_to(:identity)
275
+ expect(ctx).to respond_to(:membership)
276
+ expect(ctx).to respond_to(:staff?)
277
+ expect(ctx).to respond_to(:role)
278
+ expect(ctx).to respond_to(:account_id)
279
+ end
280
+
281
+ it "loads every Phase 3 platform policy class" do
282
+ platform_policy_classes.each do |class_name|
283
+ expect { class_name.constantize }.not_to raise_error
284
+ end
285
+ end
286
+
287
+ it "loads every Phase 3 tenant policy class" do
288
+ tenant_policy_classes.each do |class_name|
289
+ expect { class_name.constantize }.not_to raise_error
290
+ end
291
+ end
292
+
293
+ it "every platform policy inherits from Admin::Platform::ApplicationPolicy" do
294
+ (platform_policy_classes - %w[Admin::Platform::ApplicationPolicy]).each do |class_name|
295
+ klass = class_name.constantize
296
+ expect(klass.ancestors).to include(Admin::Platform::ApplicationPolicy), "#{class_name} not a Platform::ApplicationPolicy"
297
+ end
298
+ end
299
+
300
+ it "every tenant policy inherits from Admin::Tenant::ApplicationPolicy" do
301
+ (tenant_policy_classes - %w[Admin::Tenant::ApplicationPolicy]).each do |class_name|
302
+ klass = class_name.constantize
303
+ expect(klass.ancestors).to include(Admin::Tenant::ApplicationPolicy), "#{class_name} not a Tenant::ApplicationPolicy"
304
+ end
305
+ end
306
+ end
307
+
308
+ describe "Admin::Platform::IdentityPolicy" do
309
+ let(:identity) { Auth::Identity.create!(email: "p1-#{SecureRandom.hex(4)}@example.com", password: "verysecret", staff: true) }
310
+ let(:other_identity) { Auth::Identity.create!(email: "p2-#{SecureRandom.hex(4)}@example.com", password: "verysecret", staff: false) }
311
+ let(:staff_ctx) { Seams::Admin::Context.new(identity, nil) }
312
+ let(:non_staff_ctx) { Seams::Admin::Context.new(other_identity, nil) }
313
+
314
+ it "allows index? for a staff Identity" do
315
+ policy = Admin::Platform::IdentityPolicy.new(staff_ctx, identity)
316
+ expect(policy.index?).to be(true)
317
+ end
318
+
319
+ it "denies index? for a non-staff Identity" do
320
+ policy = Admin::Platform::IdentityPolicy.new(non_staff_ctx, identity)
321
+ expect(policy.index?).to be(false)
322
+ end
323
+
324
+ it "allows destroy? for a staff Identity" do
325
+ policy = Admin::Platform::IdentityPolicy.new(staff_ctx, identity)
326
+ expect(policy.destroy?).to be(true)
327
+ end
328
+
329
+ it "Scope#resolve returns scope.all for any user" do
330
+ Auth::Identity.create!(email: "p3-#{SecureRandom.hex(4)}@example.com", password: "verysecret")
331
+ resolved = Admin::Platform::IdentityPolicy::Scope.new(staff_ctx, Auth::Identity.all).resolve
332
+ expect(resolved.count).to eq(Auth::Identity.count)
333
+ end
334
+ end
335
+
336
+ describe "Admin::Tenant::IdentityPolicy::Scope (subquery form)" do
337
+ # The Phase 4 fix replaced `joins(:memberships)` with a subquery
338
+ # on `accounts_memberships` so the policy works without a
339
+ # `has_many :memberships` declaration on Auth::Identity (which
340
+ # the canonical seams Identity does NOT declare). The subquery
341
+ # filters Identities to those holding a Membership in the active
342
+ # account_id.
343
+ let(:account) { Accounts::Account.create!(name: "Tenant", external_account_id: SecureRandom.random_number(2**31)) }
344
+ let(:other_account) { Accounts::Account.create!(name: "Other", external_account_id: SecureRandom.random_number(2**31)) }
345
+ let(:in_account) { Auth::Identity.create!(email: "in-#{SecureRandom.hex(4)}@example.com", password: "verysecret") }
346
+ let(:out_of_account) { Auth::Identity.create!(email: "out-#{SecureRandom.hex(4)}@example.com", password: "verysecret") }
347
+ let(:admin_identity) { Auth::Identity.create!(email: "ad-#{SecureRandom.hex(4)}@example.com", password: "verysecret") }
348
+ let(:admin_membership) { Accounts::Membership.create!(account: account, identity_id: admin_identity.id, name: "Admin", role: "admin") }
349
+ let(:tenant_ctx) { Seams::Admin::Context.new(admin_identity, admin_membership) }
350
+
351
+ it "filters Identities to those who hold a Membership in the current Account" do
352
+ Accounts::Membership.create!(account: account, identity_id: in_account.id, name: "M1", role: "member")
353
+ Accounts::Membership.create!(account: other_account, identity_id: out_of_account.id, name: "M2", role: "member")
354
+
355
+ resolved = Admin::Tenant::IdentityPolicy::Scope.new(tenant_ctx, Auth::Identity.all).resolve
356
+ expect(resolved.pluck(:id)).to include(in_account.id, admin_identity.id)
357
+ expect(resolved.pluck(:id)).not_to include(out_of_account.id)
358
+ end
359
+ end
360
+
361
+ describe "Admin::Tenant::AccountPolicy::Scope" do
362
+ let(:account) { Accounts::Account.create!(name: "Tenant A", external_account_id: SecureRandom.random_number(2**31)) }
363
+ let(:other_account) { Accounts::Account.create!(name: "Tenant B", external_account_id: SecureRandom.random_number(2**31)) }
364
+ let(:identity) { Auth::Identity.create!(email: "t1-#{SecureRandom.hex(4)}@example.com", password: "verysecret", staff: false) }
365
+ let(:membership) { Accounts::Membership.create!(account: account, identity_id: identity.id, name: "Owner", role: "admin") }
366
+ let(:tenant_ctx) { Seams::Admin::Context.new(identity, membership) }
367
+
368
+ it "filters Accounts to the current_membership.account_id" do
369
+ account
370
+ other_account
371
+ resolved = Admin::Tenant::AccountPolicy::Scope.new(tenant_ctx, Accounts::Account.all).resolve
372
+ expect(resolved.pluck(:id)).to eq([account.id])
373
+ end
374
+
375
+ it "returns scope.none when the membership is nil" do
376
+ empty_ctx = Seams::Admin::Context.new(identity, nil)
377
+ account
378
+ other_account
379
+ resolved = Admin::Tenant::AccountPolicy::Scope.new(empty_ctx, Accounts::Account.all).resolve
380
+ expect(resolved.count).to eq(0)
381
+ end
382
+
383
+ it "returns every Account when the Identity is staff" do
384
+ staff_identity = Auth::Identity.create!(email: "staff-t-#{SecureRandom.hex(4)}@example.com", password: "verysecret", staff: true)
385
+ staff_ctx = Seams::Admin::Context.new(staff_identity, nil)
386
+ account
387
+ other_account
388
+ resolved = Admin::Tenant::AccountPolicy::Scope.new(staff_ctx, Accounts::Account.all).resolve
389
+ expect(resolved.count).to eq(Accounts::Account.count)
390
+ end
391
+ end
392
+
393
+ describe "Admin::Tenant per-record guard (id-tampering protection)" do
394
+ # Phase 4: a tenant admin requesting
395
+ # `/admin/accounts/<other_account_id>` by id-tampering used to
396
+ # pass `permitted?` (their membership.role IS admin) and only
397
+ # got blocked by the Scope filter. Show/update/destroy now run a
398
+ # second check via `record_in_tenant_scope?` so the policy
399
+ # method itself denies the request — the consequence is a clean
400
+ # 403 from `respond_with_admin_unauthorised` rather than a 404
401
+ # from RecordNotFound. Both shapes prevent the leak; the 403
402
+ # form is more honest.
403
+ let(:account) { Accounts::Account.create!(name: "Mine", external_account_id: SecureRandom.random_number(2**31)) }
404
+ let(:other_account) { Accounts::Account.create!(name: "Other", external_account_id: SecureRandom.random_number(2**31)) }
405
+ let(:identity) { Auth::Identity.create!(email: "g-#{SecureRandom.hex(4)}@example.com", password: "verysecret") }
406
+ let(:membership) { Accounts::Membership.create!(account: account, identity_id: identity.id, name: "Admin", role: "admin") }
407
+ let(:tenant_ctx) { Seams::Admin::Context.new(identity, membership) }
408
+
409
+ it "denies update? for an Account belonging to another tenant" do
410
+ policy = Admin::Tenant::AccountPolicy.new(tenant_ctx, other_account)
411
+ expect(policy.update?).to be(false)
412
+ end
413
+
414
+ it "permits update? for the caller's own Account" do
415
+ policy = Admin::Tenant::AccountPolicy.new(tenant_ctx, account)
416
+ expect(policy.update?).to be(true)
417
+ end
418
+
419
+ it "denies update? for an Identity who is not a member of the caller's account" do
420
+ stranger = Auth::Identity.create!(email: "s-#{SecureRandom.hex(4)}@example.com", password: "verysecret")
421
+ policy = Admin::Tenant::IdentityPolicy.new(tenant_ctx, stranger)
422
+ expect(policy.update?).to be(false)
423
+ end
424
+
425
+ it "permits update? for an Identity who IS a member of the caller's account" do
426
+ member = Auth::Identity.create!(email: "m-#{SecureRandom.hex(4)}@example.com", password: "verysecret")
427
+ Accounts::Membership.create!(account: account, identity_id: member.id, name: "Member", role: "member")
428
+
429
+ policy = Admin::Tenant::IdentityPolicy.new(tenant_ctx, member)
430
+ expect(policy.update?).to be(true)
431
+ end
432
+ end
433
+
434
+ describe "Admin::Tenant::ApplicationPolicy permitted?" do
435
+ let(:identity) { Auth::Identity.create!(email: "tap-#{SecureRandom.hex(4)}@example.com", password: "verysecret", staff: false) }
436
+ let(:account) { Accounts::Account.create!(name: "Tenant", external_account_id: SecureRandom.random_number(2**31)) }
437
+ let(:membership) { Accounts::Membership.new(account: account, identity_id: identity.id, name: "Member", role: role) }
438
+ let(:ctx) { Seams::Admin::Context.new(identity, membership) }
439
+ let(:policy) { Admin::Tenant::ApplicationPolicy.new(ctx, account) }
440
+
441
+ context "with role=admin" do
442
+ let(:role) { "admin" }
443
+
444
+ it "permits update?" do
445
+ expect(policy.update?).to be(true)
446
+ end
447
+ end
448
+
449
+ context "with role=owner" do
450
+ let(:role) { "owner" }
451
+
452
+ it "permits update?" do
453
+ expect(policy.update?).to be(true)
454
+ end
455
+ end
456
+
457
+ context "with role=member" do
458
+ let(:role) { "member" }
459
+
460
+ it "denies update?" do
461
+ expect(policy.update?).to be(false)
462
+ end
463
+ end
464
+ end
465
+
466
+ describe "audit-log auto-write" do
467
+ # Unit-test the controller's `record_admin_audit` private method
468
+ # against a fake controller subclass — wiring a full Administrate
469
+ # request cycle through the dummy app is heavier than what the
470
+ # Phase 3 brief asks for, and the after_action hook is the
471
+ # behaviour worth pinning down. The hook reads
472
+ # `requested_resource`, `action_name`, `response`, and
473
+ # `Auth::Current.identity` — all four are stubbed below.
474
+ let(:identity) { Auth::Identity.create!(email: "actor-#{SecureRandom.hex(4)}@example.com", password: "verysecret", staff: true) }
475
+ let(:account) { Accounts::Account.create!(name: "Audited", external_account_id: SecureRandom.random_number(2**31)) }
476
+ let(:fake_response) { Struct.new(:successful?, :redirect?).new(true, false) }
477
+ let(:controller) do
478
+ ctrl = Seams::Admin::ApplicationController.allocate
479
+ ctrl.define_singleton_method(:requested_resource) { @resource }
480
+ ctrl.define_singleton_method(:resource=) { |r| @resource = r }
481
+ ctrl.define_singleton_method(:action_name) { @action_name }
482
+ ctrl.define_singleton_method(:action_name=) { |a| @action_name = a }
483
+ ctrl.define_singleton_method(:response) { @response_obj }
484
+ ctrl.define_singleton_method(:response=) { |r| @response_obj = r }
485
+ ctrl.send(:resource=, account)
486
+ ctrl.send(:action_name=, "update")
487
+ ctrl.send(:response=, fake_response)
488
+ ctrl
489
+ end
490
+
491
+ around do |example|
492
+ Auth::Current.set(identity: identity) do
493
+ example.run
494
+ end
495
+ end
496
+
497
+ it "creates a Core::AuditLog row keyed on the actor" do
498
+ expect do
499
+ controller.send(:record_admin_audit)
500
+ end.to change(Core::AuditLog, :count).by(1)
501
+
502
+ log = Core::AuditLog.last
503
+ expect(log.action).to eq("update")
504
+ expect(log.auditable_type).to eq("Accounts::Account")
505
+ expect(log.auditable_id).to eq(account.id)
506
+ expect(log.actor_id).to eq(identity.id)
507
+ end
508
+
509
+ it "skips the write when the response was not successful" do
510
+ controller.send(:response=, Struct.new(:successful?, :redirect?).new(false, false))
511
+ expect do
512
+ controller.send(:record_admin_audit)
513
+ end.not_to change(Core::AuditLog, :count)
514
+ end
515
+
516
+ it "swallows errors so admin responses are never broken by audit-log failures" do
517
+ controller.send(:resource=, nil)
518
+ expect do
519
+ controller.send(:record_admin_audit)
520
+ end.not_to raise_error
521
+ end
522
+
523
+ it "skips the write when the model already includes Core::Auditable (avoids double-log)" do
524
+ # Simulate a host model that opted in to Core::Auditable. The
525
+ # concern's after_commit hook would have written one row
526
+ # already; the controller's after_action must NOT write a second.
527
+ auditable_model = Class.new(Accounts::Account) do
528
+ # Concern installs `has_many :audit_logs, class_name:
529
+ # "Core::AuditLog", as: :auditable`. We declare the same
530
+ # association on this anonymous subclass so the controller's
531
+ # detection check (`reflect_on_association(:audit_logs)`)
532
+ # picks it up.
533
+ has_many :audit_logs, class_name: "Core::AuditLog", as: :auditable
534
+ end
535
+
536
+ auditable_account = auditable_model.first || auditable_model.create!(
537
+ name: "Auditable", external_account_id: SecureRandom.random_number(2**31)
538
+ )
539
+ controller.send(:resource=, auditable_account)
540
+
541
+ expect do
542
+ controller.send(:record_admin_audit)
543
+ end.not_to change(Core::AuditLog, :count)
544
+ end
545
+
546
+ it "instruments seams.admin.audit_failed when the write raises" do
547
+ # Force the create! to raise so the rescue branch executes.
548
+ allow(Core::AuditLog).to receive(:create!).and_raise(ActiveRecord::RecordInvalid)
549
+
550
+ events = []
551
+ subscription = ActiveSupport::Notifications.subscribe("seams.admin.audit_failed") do |*, payload|
552
+ events << payload
553
+ end
554
+ begin
555
+ controller.send(:record_admin_audit)
556
+ ensure
557
+ ActiveSupport::Notifications.unsubscribe(subscription)
558
+ end
559
+
560
+ expect(events.size).to eq(1)
561
+ expect(events.first[:error_class]).to eq("ActiveRecord::RecordInvalid")
562
+ end
563
+ end
564
+
565
+ describe "Pundit::NotAuthorizedError handling" do
566
+ it "exposes a respond_with_admin_unauthorised handler" do
567
+ # Pundit denials should render 403, not bubble up to the host.
568
+ # Smoke-test the responder method exists and is private; full
569
+ # integration through a controller request cycle is heavier
570
+ # than the boot-spec budget.
571
+ expect(Seams::Admin::ApplicationController.private_instance_methods)
572
+ .to include(:respond_with_admin_unauthorised)
573
+ end
574
+
575
+ it "registers rescue_from for Pundit::NotAuthorizedError" do
576
+ handlers = Seams::Admin::ApplicationController.rescue_handlers
577
+ registered = handlers.map(&:first)
578
+ expect(registered).to include("Pundit::NotAuthorizedError")
579
+ end
580
+ end
581
+
582
+ describe "current_membership_resolver knob" do
583
+ it "delegates to Seams::Admin.config.current_membership_resolver" do
584
+ original = Seams::Admin.config.current_membership_resolver
585
+ stubbed_membership = Object.new
586
+ Seams::Admin.configure { |c| c.current_membership_resolver = ->(_ctrl) { stubbed_membership } }
587
+
588
+ ctrl = Seams::Admin::ApplicationController.allocate
589
+ expect(ctrl.send(:current_membership_for_admin)).to be(stubbed_membership)
590
+ ensure
591
+ Seams::Admin.config.current_membership_resolver = original
592
+ end
593
+
594
+ it "swallows resolver exceptions and returns nil" do
595
+ original = Seams::Admin.config.current_membership_resolver
596
+ Seams::Admin.configure { |c| c.current_membership_resolver = ->(_ctrl) { raise "boom" } }
597
+
598
+ ctrl = Seams::Admin::ApplicationController.allocate
599
+ expect(ctrl.send(:current_membership_for_admin)).to be_nil
600
+ ensure
601
+ Seams::Admin.config.current_membership_resolver = original
602
+ end
603
+ end
604
+ end