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,355 @@
1
+ # Billing
2
+
3
+ > Subscription billing for a Seams-powered host. Stripe by default,
4
+ > swap-able to any gateway via the `Billing::Gateways::Abstract`
5
+ > contract.
6
+
7
+ **Requires:** by default, the `accounts` engine — billing rows
8
+ carry `account_id` and the `Billing::Billable` concern auto-includes
9
+ into `Billing.configuration.billable_class` (default
10
+ `"Accounts::Account"`). Hosts that prefer User-as-customer
11
+ override `billable_class` in `config/initializers/billing.rb` and
12
+ can run without the accounts engine.
13
+
14
+ ## The Account-as-customer model
15
+
16
+ Post-Wave-9, the **Stripe customer represents an Account, not an
17
+ Identity (human)**. Subscriptions, invoices, and lifetime passes
18
+ belong to the tenant. The same Identity who is a member of two
19
+ Accounts has two independent billing relationships — even though
20
+ they're the same human.
21
+
22
+ Concrete: every billing table carries `account_id` (UUID, the
23
+ `Accounts::Account#id`) as its local foreign key. The Stripe
24
+ `customer_ref` (the `cus_*` id) is also stored on every row, so
25
+ webhook handlers can address rows either way. There is no
26
+ `user_id` / `host_user_id` column anywhere — those were removed in
27
+ this refactor.
28
+
29
+ `granted_by_identity_id` and `revoked_by_identity_id` on
30
+ `billing_lifetime_passes` are deliberate exceptions: they reference
31
+ an `Auth::Identity` (the human who pressed the "grant" or "revoke"
32
+ button), not an Account. The columns track human action, not
33
+ tenant-level data.
34
+
35
+ ### Migration story for hosts on a prior wave
36
+
37
+ If your host already had Wave 8 billing in production with `user_id`
38
+ on `billing_subscriptions` etc, those columns are gone. Write a
39
+ host-side data migration that backfills `account_id` from your
40
+ existing User → Account mapping before adopting the new schema.
41
+ There is no automated migration shipped with seams — every host's
42
+ mapping is different.
43
+
44
+ ## Wiring `Billing::Billable` into your tenant model
45
+
46
+ `Billing::Billable` provides the helpers that the host's tenant
47
+ model needs:
48
+
49
+ ```ruby
50
+ account.billing_subscriptions # association
51
+ account.billing_invoices # association
52
+ account.billing_lifetime_passes # association
53
+ account.has_active_billing? # paying right now?
54
+ account.lifetime? # holds an active LTD?
55
+ account.has_lifetime_for?(plan_ref:) # specific LTD?
56
+ account.start_subscription!(plan_ref:, email:)
57
+ account.cancel_subscription!(subscription_ref:)
58
+ account.stripe_customer_ref!(email:) # lazy Stripe customer creation
59
+ ```
60
+
61
+ The engine wires the concern into `Accounts::Account` automatically
62
+ at boot via:
63
+
64
+ ```ruby
65
+ # lib/billing/engine.rb (paraphrased)
66
+ config.to_prepare do
67
+ Billing.configuration.billable_class.constantize.include(Billing::Billable)
68
+ end
69
+ ```
70
+
71
+ Override the target class in `config/initializers/billing.rb` if your
72
+ tenant lives on a different model:
73
+
74
+ ```ruby
75
+ Billing.configure do |c|
76
+ c.billable_class = "Workspaces::Workspace"
77
+ end
78
+ ```
79
+
80
+ Set `c.billable_class = nil` to opt out and `include Billing::Billable`
81
+ manually wherever it makes sense.
82
+
83
+ ## Events emitted
84
+
85
+ All billing events publish the **same canonical payload shape** so
86
+ subscribers can read one format regardless of source:
87
+
88
+ ```
89
+ { gateway: "stripe", # which adapter emitted it
90
+ livemode: true | false, # gateway's livemode flag
91
+ account_id: "uuid", # the Accounts::Account that owns this row
92
+ customer_ref: "cus_xxx", # gateway customer id
93
+ ref: "sub_xxx", # canonical id of the subject
94
+ object_id: "sub_xxx", # raw object id from the gateway
95
+ object: { ... } } # the gateway object as a hash (incl. account_id where applicable)
96
+ ```
97
+
98
+ | Event name | Subject | Emitted when |
99
+ | --- | --- | --- |
100
+ | `subscription.created.billing` | Subscription | StartSubscriptionJob succeeds, or `customer.subscription.created` webhook fires |
101
+ | `subscription.updated.billing` | Subscription | `customer.subscription.updated` webhook fires |
102
+ | `subscription.canceled.billing` | Subscription | CancelSubscriptionJob succeeds, or `customer.subscription.deleted` webhook fires |
103
+ | `subscription.trial_will_end.billing` | Subscription | `customer.subscription.trial_will_end` webhook fires (~3 days before trial end) |
104
+ | `invoice.created.billing` | Invoice | `invoice.created` webhook fires (status: draft) |
105
+ | `invoice.paid.billing` | Invoice | `invoice.paid` webhook fires |
106
+ | `invoice.failed.billing` | Invoice | `invoice.payment_failed` webhook fires |
107
+ | `invoice.finalized.billing` | Invoice | `invoice.finalized` webhook fires |
108
+ | `invoice.voided.billing` | Invoice | `invoice.voided` webhook fires |
109
+ | `payment.succeeded.billing` | PaymentIntent | `payment_intent.succeeded` webhook fires |
110
+ | `payment.failed.billing` | PaymentIntent | `payment_intent.payment_failed` webhook fires |
111
+ | `charge.refunded.billing` | Charge | `charge.refunded` webhook fires |
112
+ | `checkout.session_completed.billing` | CheckoutSession | `checkout.session.completed` webhook fires (subscription mode; LTD mode forks to lifetime.purchased.billing) |
113
+ | `lifetime.granted.billing` | LifetimePass | Admin grants a Lifetime Deal via `Billing::Lifetime::GrantPassService`. Includes `granted_by_identity_id`. |
114
+ | `lifetime.purchased.billing` | LifetimePass | Customer pays for an LTD via Stripe Checkout `mode: "payment"` |
115
+ | `lifetime.revoked.billing` | LifetimePass | `Billing::Lifetime::RevokePassService` is called. Includes `revoked_by_identity_id`. |
116
+
117
+ The webhook controller upserts a local `Billing::Subscription` /
118
+ `Billing::Invoice` row before publishing, so subscribers can resolve
119
+ either from `payload[:account_id]` directly or by querying the
120
+ local DB with `payload[:ref]`.
121
+
122
+ ## Events consumed
123
+
124
+ This engine does not subscribe to any other engine's events by default.
125
+ Hosts often subscribe to `account.created.accounts` to create a Stripe
126
+ customer at tenant creation time.
127
+
128
+ ## Exposed concerns
129
+
130
+ | Concern | Purpose |
131
+ | --- | --- |
132
+ | `Billing::Billable` | Mix into your tenant model (default `Accounts::Account`) for `start_subscription!` / `cancel_subscription!` / `lifetime?` / `has_lifetime_for?(plan_ref:)` / `has_active_billing?` helpers. The engine auto-includes it via `Billing.configuration.billable_class` at boot. |
133
+
134
+ ## Lifetime Deals (LTD)
135
+
136
+ The engine ships native LTD support — one-time payment for permanent
137
+ access — alongside recurring subscriptions. Two flows:
138
+
139
+ 1. **Public purchase** — Stripe Checkout with `mode: "payment"`. The
140
+ pricing page renders any `Billing::Plan` with `interval: "lifetime"`
141
+ under a "Buy once, own forever" section. POST
142
+ `/billing/checkout/lifetime?plan=<gateway_ref>` →
143
+ `Billing::Lifetime::CreateLifetimeSessionService` (which threads
144
+ the current Account's id into Stripe session metadata) →
145
+ redirect to Stripe → on `checkout.session.completed` (or
146
+ `checkout.session.async_payment_succeeded`) the webhook handler
147
+ creates the `Billing::LifetimePass` row (with `account_id` read
148
+ off the metadata) and publishes `lifetime.purchased.billing`.
149
+
150
+ 2. **Private grant** — admin issues an LTD without a Stripe charge via
151
+ `Billing::Admin::LifetimePassesController` (mounted at
152
+ `/billing/admin/lifetime_passes`). Use for early adopters,
153
+ influencer giveaways, ToS-violation refund-then-re-grant. Calls
154
+ `Billing::Lifetime::GrantPassService` with the target Account's id
155
+ AND the Identity of the admin pressing the button. Publishes
156
+ `lifetime.granted.billing` with `granted_by_identity_id` set.
157
+
158
+ ### Trade-off (read this before turning LTDs on)
159
+
160
+ LTDs lock you into supporting those users **indefinitely with no
161
+ recurring revenue**. They're a strong early-adopter / launch lever —
162
+ quick cash, fast feedback — but get expensive long-term as your
163
+ support load grows while LTD users contribute zero MRR. Cap inventory
164
+ on every LTD plan via `max_lifetime_units` (nil = unlimited):
165
+
166
+ ```ruby
167
+ Billing::Plan.create!(
168
+ gateway_ref: "price_lifetime_pro_2026",
169
+ name: "Pro Lifetime",
170
+ interval: "lifetime",
171
+ amount_cents: 249_00,
172
+ currency: "usd",
173
+ max_lifetime_units: 100 # only the first 100 buyers
174
+ )
175
+ ```
176
+
177
+ The pricing page reads `Plan#lifetime_inventory_remaining` and
178
+ disables the "Buy lifetime" button when `lifetime_sold_out?`.
179
+
180
+ ### Authorization model
181
+
182
+ The engine ships no admin gate — `Billing::Admin::LifetimePassesController`
183
+ mounts at `/billing/admin/...` and the host wires their own
184
+ `require_admin!` `before_action` (ActiveAdmin / Avo / your own
185
+ solution). Per the issue #2 4B scope decision, Seams doesn't ship its
186
+ own admin engine.
187
+
188
+ ### Revoke
189
+
190
+ `Billing::Lifetime::RevokePassService.call(pass:, revoked_by:, notes:)`
191
+ soft-revokes via `revoked_at`. `revoked_by` is an Auth::Identity
192
+ (the human pressing the revoke button); pass `nil` for system-revoked
193
+ passes. The pass row stays in the DB so the audit trail survives.
194
+ Stripe refund (for paid LTDs) is the caller's responsibility — this
195
+ service updates the local row only. Publishes `lifetime.revoked.billing`.
196
+
197
+ ## Gateways
198
+
199
+ | Gateway | Default | Configure via |
200
+ | --- | --- | --- |
201
+ | `Billing::Gateways::Stripe` | yes | `Billing.configure { \|c\| c.gateway = "Billing::Gateways::Stripe" }` |
202
+
203
+ To add a gateway, subclass `Billing::Gateways::Abstract` and implement
204
+ `#create_subscription`, `#cancel_subscription`, `#fetch_subscription`,
205
+ `#verify_webhook`. Then point `Billing.configuration.gateway` at the
206
+ new class.
207
+
208
+ ## Webhook setup
209
+
210
+ Stripe will POST events to `/billing/webhooks/stripe`. The engine
211
+ verifies signatures via `Billing::Stripe::WebhookSignature`. Set:
212
+
213
+ ```bash
214
+ STRIPE_SECRET_KEY=sk_live_...
215
+ STRIPE_WEBHOOK_SECRET=whsec_...
216
+ ```
217
+
218
+ In your Stripe dashboard, configure the webhook endpoint to send any
219
+ of the events listed below. Each maps to a handler class in
220
+ `app/services/billing/webhooks/handlers/`; missing handlers no-op
221
+ gracefully so subscribing to extras is safe.
222
+
223
+ | Stripe event | Handler | Canonical seams event |
224
+ | --- | --- | --- |
225
+ | `customer.subscription.created` | `SubscriptionCreatedHandler` | `subscription.created.billing` |
226
+ | `customer.subscription.updated` | `SubscriptionUpdatedHandler` | `subscription.updated.billing` |
227
+ | `customer.subscription.deleted` | `SubscriptionDeletedHandler` | `subscription.canceled.billing` |
228
+ | `customer.subscription.trial_will_end` | `SubscriptionTrialWillEndHandler` | `subscription.trial_will_end.billing`|
229
+ | `invoice.created` | `InvoiceCreatedHandler` | `invoice.created.billing` |
230
+ | `invoice.paid` | `InvoicePaidHandler` | `invoice.paid.billing` |
231
+ | `invoice.payment_failed` | `InvoicePaymentFailedHandler` | `invoice.failed.billing` |
232
+ | `invoice.finalized` | `InvoiceFinalizedHandler` | `invoice.finalized.billing` |
233
+ | `invoice.voided` | `InvoiceVoidedHandler` | `invoice.voided.billing` |
234
+ | `payment_intent.succeeded` | `PaymentSucceededHandler` | `payment.succeeded.billing` |
235
+ | `payment_intent.payment_failed` | `PaymentFailedHandler` | `payment.failed.billing` |
236
+ | `charge.refunded` | `ChargeRefundedHandler` | `charge.refunded.billing` |
237
+ | `checkout.session.completed` | `CheckoutSessionCompletedHandler` | (subscription path / LTD path) |
238
+
239
+ The handler base resolves `account_id` for each webhook by looking
240
+ up the local `Billing::Subscription` / `Billing::Invoice` row keyed
241
+ on the gateway's `customer_ref`. For brand-new Stripe-initiated
242
+ subscriptions (created in the Stripe Dashboard, not via
243
+ `Account#start_subscription!`), there's no local row to resolve —
244
+ the upsert logs a warning and skips. Reconcile those via a
245
+ host-side sync task that maps Stripe customers to Accounts.
246
+
247
+ Adding a new event type means registering a handler from your host —
248
+ no fork required:
249
+
250
+ ```ruby
251
+ Billing::Webhooks::EventRouter.register(
252
+ "customer.tax_id.created",
253
+ "MyApp::TaxIdCreatedHandler"
254
+ )
255
+ ```
256
+
257
+ Default dispatch is synchronous so handler raises roll back the
258
+ `WebhookEvent` row and Stripe retries. Flip
259
+ `Billing.configuration.process_webhooks_async = true` to enqueue
260
+ `Billing::Webhooks::ProcessEventJob.perform_later` instead — Stripe
261
+ recommends responding in <100ms.
262
+
263
+ ## Self-service controllers
264
+
265
+ | Controller | Action |
266
+ | --- | --- |
267
+ | `Billing::SubscriptionsController#index` | List the current Account's subscriptions |
268
+ | `#show` | Single subscription with cancel / change-plan controls|
269
+ | `#cancel` | Period-end cancel (immediate via `?immediate=1`) |
270
+ | `#reactivate` | Un-cancel a pending-cancellation subscription |
271
+ | `#change_plan` | Switch to a new price (proration configurable) |
272
+ | `Billing::InvoicesController#index/#show` | Read-only billing history |
273
+
274
+ The controllers expect `current_billing_account` to return the
275
+ current `Accounts::Account` — by default they read
276
+ `Accounts::Current.account` (set up by the accounts engine's
277
+ controller concern). Override `#current_billing_account` and
278
+ `#current_billing_customer_ref` in your host's `SubscriptionsController` /
279
+ `InvoicesController` if your tenant resolution is bound differently.
280
+
281
+ ## Stripe API surface used
282
+
283
+ Every Stripe call has a doc URL cited inline in
284
+ `lib/billing/gateways/stripe.rb` and in each `Billing::Stripe::Client`
285
+ method:
286
+
287
+ | Stripe call | Docs URL |
288
+ | --- | --- |
289
+ | `POST /v1/customers` | https://docs.stripe.com/api/customers/create |
290
+ | `GET /v1/customers/search` | https://docs.stripe.com/api/customers/search |
291
+ | `POST /v1/subscriptions` | https://docs.stripe.com/api/subscriptions/create |
292
+ | `POST /v1/subscriptions/:id` | https://docs.stripe.com/api/subscriptions/update |
293
+ | `DELETE /v1/subscriptions/:id` | https://docs.stripe.com/api/subscriptions/cancel |
294
+ | `GET /v1/subscriptions/:id` | https://docs.stripe.com/api/subscriptions/retrieve |
295
+ | `GET /v1/invoices/:id` | https://docs.stripe.com/api/invoices/retrieve |
296
+ | `POST /v1/checkout/sessions` | https://docs.stripe.com/api/checkout/sessions/create |
297
+ | `POST /v1/billing_portal/sessions`| https://docs.stripe.com/api/customer_portal/sessions/create |
298
+ | Webhook signature verification | https://docs.stripe.com/webhooks/signatures |
299
+
300
+ ## Verifying the Stripe Checkout flow against test mode
301
+
302
+ For end-to-end confidence in the Stripe wiring, run a real Checkout
303
+ session against Stripe's test mode — no mocks, no webmock.
304
+
305
+ 1. Grab a test secret key + webhook secret from
306
+ https://dashboard.stripe.com/test/apikeys and
307
+ https://dashboard.stripe.com/test/webhooks (point the webhook at a
308
+ tunnelled URL via `stripe listen --forward-to localhost:3000/billing/webhooks/stripe`).
309
+ 2. Set the env vars:
310
+ ```bash
311
+ STRIPE_SECRET_KEY=sk_test_...
312
+ STRIPE_WEBHOOK_SECRET=whsec_... # from `stripe listen` output
313
+ ```
314
+ 3. Seed at least one Plan whose `gateway_ref` matches a Stripe test
315
+ price id.
316
+ 4. Visit `/billing/plans`, click "Subscribe", complete Checkout with
317
+ the test card `4242 4242 4242 4242`.
318
+ 5. Watch your logs — `customer.subscription.created` and
319
+ `invoice.paid` should arrive within seconds; the local
320
+ `Billing::Subscription` + `Billing::Invoice` rows should appear
321
+ with the current Account's id pinned to them.
322
+ 6. Visit `/billing/subscriptions` — your new subscription should be
323
+ listed. Cancel + reactivate via the UI to exercise the full
324
+ service-object surface.
325
+
326
+ If webhook events do not arrive, check
327
+ `Billing::WebhookEvent.where(gateway: "stripe").order(created_at: :desc)`
328
+ — rows mean Stripe reached you but a handler raised; absence means
329
+ the signature verification or the URL is wrong.
330
+
331
+ ## Gateway contract specs
332
+
333
+ The shared example `"a billing gateway"` lives at
334
+ `spec/support/shared_examples/a_billing_gateway.rb`. Every gateway
335
+ adapter (Stripe is the reference; Paddle / Adyen / your own) MUST
336
+ satisfy it:
337
+
338
+ ```ruby
339
+ RSpec.describe Billing::Gateways::Paddle do
340
+ it_behaves_like "a billing gateway"
341
+ end
342
+ ```
343
+
344
+ The contract checks that every method on `Billing::Gateways::Abstract`
345
+ exists on the subclass with the documented keyword arguments, and
346
+ that `verify_webhook` raises `Billing::WebhookError` on a bad
347
+ signature. Wiring-level correctness ("does Paddle actually charge
348
+ people?") needs an integration test against that gateway's test mode
349
+ — see the Stripe walk-through above for the pattern.
350
+
351
+ ## Running the specs
352
+
353
+ ```bash
354
+ bin/rails seams:test[billing]
355
+ ```
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Admin
5
+ # Admin-side controller for issuing + revoking LifetimePasses
6
+ # without a Stripe charge. Use for early adopters, influencer
7
+ # giveaways, and ToS revocations.
8
+ #
9
+ # Authorization is the host's responsibility — Seams ships no
10
+ # admin engine (per the explicit scope decision in issue #2 4B).
11
+ # Mount this behind whatever admin gate the host uses (ActiveAdmin,
12
+ # Avo, your own #require_admin! before_action).
13
+ #
14
+ # Suggested host-side wiring:
15
+ #
16
+ # # config/routes.rb
17
+ # namespace :admin do
18
+ # resources :lifetime_passes, controller: "billing/admin/lifetime_passes",
19
+ # only: %i[index new create destroy]
20
+ # end
21
+ #
22
+ # # app/controllers/billing/admin/lifetime_passes_controller_decorator.rb
23
+ # Billing::Admin::LifetimePassesController.class_eval do
24
+ # before_action :require_admin!
25
+ # end
26
+ class LifetimePassesController < ApplicationController
27
+ def index
28
+ @passes = Billing::LifetimePass.order(granted_at: :desc).limit(100)
29
+ end
30
+
31
+ def new
32
+ @plan_options = Billing::Plan.active.lifetime
33
+ end
34
+
35
+ def create
36
+ result = Billing::Lifetime::GrantPassService.call(
37
+ account_id: params.require(:account_id),
38
+ customer_ref: params.require(:customer_ref),
39
+ plan_ref: params.require(:plan_ref),
40
+ granted_by: current_admin_identity,
41
+ notes: params[:notes]
42
+ )
43
+
44
+ if result.ok?
45
+ redirect_to billing.admin_lifetime_passes_path,
46
+ notice: "Lifetime pass issued."
47
+ else
48
+ redirect_to billing.new_admin_lifetime_pass_path,
49
+ alert: result.error
50
+ end
51
+ end
52
+
53
+ def destroy
54
+ pass = Billing::LifetimePass.find(params[:id])
55
+ result = Billing::Lifetime::RevokePassService.call(
56
+ pass: pass,
57
+ revoked_by: current_admin_identity,
58
+ notes: params[:notes]
59
+ )
60
+
61
+ if result.ok?
62
+ redirect_to billing.admin_lifetime_passes_path,
63
+ notice: "Lifetime pass revoked."
64
+ else
65
+ redirect_to billing.admin_lifetime_passes_path,
66
+ alert: result.error
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ # Override in the host. The default reads `current_user` if the
73
+ # host's admin gate uses the same `current_user` helper — for a
74
+ # post-Wave-9 host that means the Auth::Identity row currently
75
+ # signed in. Pre-Wave-9 hosts (host User present) override this
76
+ # to return the User; the GrantPassService coerces both shapes.
77
+ def current_admin_identity
78
+ return nil unless respond_to?(:current_user)
79
+
80
+ current_user
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ class CheckoutController < ApplicationController
5
+ # POST /billing/checkout?plan=price_xxx
6
+ def create
7
+ plan = Billing::Plan.active.find_by!(gateway_ref: params[:plan])
8
+
9
+ result = Billing::Checkout::CreateSessionService.call(
10
+ customer_ref: customer_ref_or_create!,
11
+ plan_ref: plan.gateway_ref,
12
+ success_url: billing.checkout_success_url,
13
+ cancel_url: billing.plans_url
14
+ )
15
+
16
+ if result.ok?
17
+ redirect_to result.url, allow_other_host: true, status: :see_other
18
+ else
19
+ redirect_to billing.plans_path, alert: result.error
20
+ end
21
+ end
22
+
23
+ # GET /billing/checkout/success
24
+ def success
25
+ # Stripe will fire `checkout.session.completed` webhook; the
26
+ # subscription row gets created/updated by the webhook handler.
27
+ render :success
28
+ end
29
+
30
+ # POST /billing/checkout/lifetime?plan=price_xxx
31
+ # LTD purchase flow — same shape as #create but uses Stripe's
32
+ # `mode: "payment"`. The webhook handler distinguishes the two via
33
+ # session.metadata.access_type.
34
+ def lifetime
35
+ plan = Billing::Plan.active.find_by!(gateway_ref: params[:plan])
36
+
37
+ result = Billing::Lifetime::CreateLifetimeSessionService.call(
38
+ account_id: current_billing_account.id,
39
+ customer_ref: customer_ref_or_create!,
40
+ plan_ref: plan.gateway_ref,
41
+ success_url: billing.checkout_success_url,
42
+ cancel_url: billing.plans_url
43
+ )
44
+
45
+ if result.ok?
46
+ redirect_to result.url, allow_other_host: true, status: :see_other
47
+ else
48
+ redirect_to billing.plans_path, alert: result.error
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ # Resolves the Stripe customer id for the current Account. The
55
+ # host must implement `current_billing_account` (or set it from
56
+ # a before_action) — typically `Accounts::Current.account`.
57
+ # Hosts pre-Wave-9 can override the entire method to use their
58
+ # own user-keyed lookup.
59
+ def customer_ref_or_create!
60
+ account = current_billing_account
61
+ raise Billing::Error, "current_billing_account is not set" unless account
62
+ raise Billing::Error, "Account #{account.id} does not respond to stripe_customer_ref!" unless account.respond_to?(:stripe_customer_ref!)
63
+
64
+ account.stripe_customer_ref!(email: billing_contact_email_for(account))
65
+ end
66
+
67
+ # Override in the host to point at the right contact email for
68
+ # the Stripe customer record. Default reads `current_user.email`
69
+ # if the host's auth concern wires one up — usually the right
70
+ # answer for B2C flows. B2B hosts often want the Account owner's
71
+ # email instead; override accordingly.
72
+ def billing_contact_email_for(_account)
73
+ return current_user.email_address if respond_to?(:current_user) && current_user.respond_to?(:email_address)
74
+ return current_user.email if respond_to?(:current_user) && current_user.respond_to?(:email)
75
+
76
+ raise Billing::Error,
77
+ "Override #billing_contact_email_for(account) to supply the Stripe customer email."
78
+ end
79
+
80
+ # Default implementation reads `Accounts::Current.account` if the
81
+ # accounts engine is installed. Override in the host if your
82
+ # Account is bound differently (e.g. via params, subdomain, etc).
83
+ def current_billing_account
84
+ return @current_billing_account if defined?(@current_billing_account)
85
+
86
+ @current_billing_account =
87
+ if defined?(Accounts::Current) && Accounts::Current.respond_to?(:account)
88
+ Accounts::Current.account
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ # Read-only billing history for the current Account. The local
5
+ # Billing::Invoice rows are populated by the InvoiceHandlerBase
6
+ # webhook handlers as Stripe fires invoice.* events; SyncService
7
+ # can refresh on demand.
8
+ #
9
+ # No `download` action — Stripe hosts the PDF; link to the
10
+ # `hosted_invoice_url` Stripe returns on the invoice object. Saves
11
+ # us from being a redirect proxy for content we do not own.
12
+ #
13
+ # GET /billing/invoices → index (paginated history)
14
+ # GET /billing/invoices/:id → show
15
+ class InvoicesController < ApplicationController
16
+ before_action :require_invoice, only: %i[show]
17
+
18
+ def index
19
+ @invoices = scoped_invoices.order(created_at: :desc)
20
+ end
21
+
22
+ def show
23
+ # @invoice is set by require_invoice. Hosts that want a fresh
24
+ # read from Stripe before rendering can opt in:
25
+ #
26
+ # Billing::Invoices::SyncService.call(invoice_ref: @invoice.gateway_ref)
27
+ #
28
+ # The default render is the local DB row — webhook lag is
29
+ # usually <1s, so this is good enough for almost every UI.
30
+ end
31
+
32
+ private
33
+
34
+ def scoped_invoices
35
+ Billing::Invoice.where(customer_ref: current_billing_customer_ref)
36
+ end
37
+
38
+ def require_invoice
39
+ @invoice = scoped_invoices.find_by(id: params[:id])
40
+ return if @invoice
41
+
42
+ redirect_to invoices_path, alert: "Invoice not found."
43
+ end
44
+
45
+ def current_billing_customer_ref
46
+ account = current_billing_account
47
+ return nil unless account
48
+
49
+ account.billing_subscriptions.pick(:customer_ref) ||
50
+ account.billing_invoices.pick(:customer_ref) ||
51
+ account.billing_lifetime_passes.pick(:customer_ref)
52
+ end
53
+
54
+ def current_billing_account
55
+ return @current_billing_account if defined?(@current_billing_account)
56
+
57
+ @current_billing_account =
58
+ if defined?(Accounts::Current) && Accounts::Current.respond_to?(:account)
59
+ Accounts::Current.account
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ class PlansController < ApplicationController
5
+ def index
6
+ @recurring_plans = Billing::Plan.active.recurring.order(:amount_cents)
7
+ # LTD plans grouped separately on the pricing page so the host
8
+ # can render them under a "buy once, own forever" section.
9
+ # `lifetime_inventory_remaining` is nil for unlimited; integer
10
+ # for capped (used by the view to show "X seats left").
11
+ @lifetime_plans = Billing::Plan.active.lifetime.order(:amount_cents)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ class PortalController < ApplicationController
5
+ # POST /billing/portal
6
+ def create
7
+ account = current_billing_account
8
+ raise Billing::Error, "current_billing_account is not set" unless account
9
+
10
+ result = Billing::Portal::CreateSessionService.call(
11
+ customer_ref: account_customer_ref(account),
12
+ return_url: billing.plans_url
13
+ )
14
+
15
+ if result.ok?
16
+ redirect_to result.url, allow_other_host: true, status: :see_other
17
+ else
18
+ redirect_to billing.plans_path, alert: result.error
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ # Reads the Stripe customer id off any existing billing row for
25
+ # this Account. Avoids a synchronous Stripe API call — the
26
+ # portal is only meaningful when the Account already has a
27
+ # subscription anyway.
28
+ def account_customer_ref(account)
29
+ account.billing_subscriptions.pick(:customer_ref) ||
30
+ account.billing_invoices.pick(:customer_ref) ||
31
+ account.billing_lifetime_passes.pick(:customer_ref) ||
32
+ raise(Billing::Error, "Account has no billing customer_ref yet — create a subscription or LTD first.")
33
+ end
34
+
35
+ # See CheckoutController#current_billing_account — same default.
36
+ def current_billing_account
37
+ return @current_billing_account if defined?(@current_billing_account)
38
+
39
+ @current_billing_account =
40
+ if defined?(Accounts::Current) && Accounts::Current.respond_to?(:account)
41
+ Accounts::Current.account
42
+ end
43
+ end
44
+ end
45
+ end