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,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ # Self-service subscription management for the current Account.
5
+ # Backed by the Phase 3 (2/4) service objects — every action is a
6
+ # thin wrapper that calls a service and renders / redirects on the
7
+ # ServiceResult.
8
+ #
9
+ # Routes (from config/routes.rb):
10
+ #
11
+ # GET /billing/subscriptions → index (list)
12
+ # GET /billing/subscriptions/:id → show
13
+ # DELETE /billing/subscriptions/:id → cancel (period-end by default)
14
+ # POST /billing/subscriptions/:id/reactivate
15
+ # POST /billing/subscriptions/:id/change_plan
16
+ #
17
+ # The host's authentication concern decides who `current_user` is —
18
+ # this controller scopes by the current Account (the tenant), not
19
+ # the human. Subscriptions belong to Accounts::Account post-Wave-9.
20
+ class SubscriptionsController < ApplicationController
21
+ before_action :require_subscription, only: %i[show cancel reactivate change_plan]
22
+
23
+ def index
24
+ @subscriptions = scoped_subscriptions.order(created_at: :desc)
25
+ end
26
+
27
+ def show
28
+ # @subscription is set by require_subscription
29
+ end
30
+
31
+ # DELETE /billing/subscriptions/:id
32
+ # Default: cancel at period end (user keeps access through what
33
+ # they have already paid for). Pass ?immediate=1 to cancel now.
34
+ def cancel
35
+ result = Billing::Subscriptions::CancelService.call(
36
+ subscription_ref: @subscription.gateway_ref,
37
+ immediate: params[:immediate].present?
38
+ )
39
+
40
+ respond_to_result(result, success: "Subscription cancelled.")
41
+ end
42
+
43
+ # POST /billing/subscriptions/:id/reactivate
44
+ # Un-cancel a subscription that is pending end-of-period cancellation.
45
+ def reactivate
46
+ result = Billing::Subscriptions::ReactivateService.call(
47
+ subscription_ref: @subscription.gateway_ref
48
+ )
49
+
50
+ respond_to_result(result, success: "Subscription reactivated.")
51
+ end
52
+
53
+ # POST /billing/subscriptions/:id/change_plan
54
+ # params[:price_ref] — required, the new Stripe price id
55
+ # params[:proration_behavior] — optional, "create_prorations" (default) | "none"
56
+ def change_plan
57
+ new_price = params[:price_ref].to_s
58
+ if new_price.empty?
59
+ redirect_to subscription_path(@subscription),
60
+ alert: "Choose a plan before submitting."
61
+ return
62
+ end
63
+
64
+ result = Billing::Subscriptions::ChangePlanService.call(
65
+ subscription_ref: @subscription.gateway_ref,
66
+ new_price_ref: new_price,
67
+ proration_behavior: params[:proration_behavior].presence || "create_prorations"
68
+ )
69
+
70
+ respond_to_result(result, success: "Plan changed.")
71
+ end
72
+
73
+ private
74
+
75
+ # Override in the host (or here) to scope by the current Account's
76
+ # billing rows. The default scopes by `current_billing_customer_ref`
77
+ # — the Stripe `cus_*` id resolved from the current Account — which
78
+ # is correct whenever the controller has an Account in scope.
79
+ def scoped_subscriptions
80
+ Billing::Subscription.where(customer_ref: current_billing_customer_ref)
81
+ end
82
+
83
+ def require_subscription
84
+ @subscription = scoped_subscriptions.find_by(id: params[:id])
85
+ return if @subscription
86
+
87
+ redirect_to subscriptions_path, alert: "Subscription not found."
88
+ end
89
+
90
+ # Reads the Stripe customer id off the current Account. Hosts on
91
+ # a pre-Wave-9 user-keyed flow can override this method to point
92
+ # at their User's own `billing_customer_ref` accessor.
93
+ def current_billing_customer_ref
94
+ account = current_billing_account
95
+ return nil unless account
96
+
97
+ account.billing_subscriptions.pick(:customer_ref) ||
98
+ account.billing_invoices.pick(:customer_ref) ||
99
+ account.billing_lifetime_passes.pick(:customer_ref)
100
+ end
101
+
102
+ def current_billing_account
103
+ return @current_billing_account if defined?(@current_billing_account)
104
+
105
+ @current_billing_account =
106
+ if defined?(Accounts::Current) && Accounts::Current.respond_to?(:account)
107
+ Accounts::Current.account
108
+ end
109
+ end
110
+
111
+ def respond_to_result(result, success:)
112
+ if result.ok?
113
+ redirect_to subscriptions_path, notice: success
114
+ else
115
+ redirect_to subscription_path(@subscription), alert: result.error
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ # Receives webhook callbacks from the configured payment gateway.
5
+ # The gateway adapter does the signature verification — we just
6
+ # record the event for idempotency and route it to the handler
7
+ # that knows how to process that event type.
8
+ #
9
+ # Trust boundary: the request body is treated as untrusted until
10
+ # the gateway adapter's `verify_webhook` returns. CSRF is
11
+ # intentionally skipped (webhooks come from Stripe, not a browser
12
+ # session) — DO NOT add authentication here; the signature IS the
13
+ # authentication.
14
+ #
15
+ # Idempotent: every accepted event is recorded by gateway+event_id;
16
+ # retries hit the unique index and short-circuit before any handler
17
+ # fires. Stripe explicitly requires this — retries can span days
18
+ # (https://docs.stripe.com/webhooks#handle-duplicate-events).
19
+ #
20
+ # Sync vs async: by default the handler runs in the request thread.
21
+ # Set Billing.configuration.process_webhooks_async = true to enqueue
22
+ # Billing::Webhooks::ProcessEventJob instead — Stripe recommends
23
+ # responding in <100ms (https://docs.stripe.com/webhooks#acknowledge-events-immediately).
24
+ class WebhooksController < ApplicationController
25
+ skip_before_action :verify_authenticity_token
26
+
27
+ # POST /billing/webhooks/stripe
28
+ def stripe
29
+ payload = request.body.read
30
+ signature = request.headers["Stripe-Signature"]
31
+
32
+ event = Billing.gateway.verify_webhook(
33
+ payload: payload,
34
+ signature: signature,
35
+ secret: Billing.configuration.webhook_secret
36
+ )
37
+
38
+ record_and_dispatch("stripe", event)
39
+ head :ok
40
+ rescue Billing::WebhookError => e
41
+ Seams::Observability.adapter.warn(
42
+ "billing.webhook.invalid", engine: "billing", error: e.message
43
+ )
44
+ head :bad_request
45
+ end
46
+
47
+ private
48
+
49
+ # Insert WebhookEvent FIRST inside a transaction that ALSO runs
50
+ # the handler. If the handler raises, the WebhookEvent row rolls
51
+ # back — Stripe retries, we get another chance to publish.
52
+ # Otherwise a transient handler bug would permanently drop one
53
+ # event behind the unique-index guard. Async path skips the
54
+ # transaction (the job has its own retry semantics).
55
+ def record_and_dispatch(gateway, event)
56
+ Billing::WebhookEvent.transaction do
57
+ Billing::WebhookEvent.create!(
58
+ gateway: gateway,
59
+ gateway_event_id: event[:id] || object_id_for(event[:object]),
60
+ event_type: event[:type],
61
+ livemode: event[:livemode]
62
+ )
63
+
64
+ dispatch(gateway: gateway, event: event)
65
+ end
66
+ rescue ActiveRecord::RecordNotUnique
67
+ # Duplicate retry — Stripe will keep retrying; replying 200 stops
68
+ # the storm without re-publishing to subscribers.
69
+ Seams::Observability.adapter.info(
70
+ "billing.webhook.duplicate", engine: "billing",
71
+ gateway: gateway, event_type: event[:type]
72
+ )
73
+ end
74
+
75
+ def dispatch(gateway:, event:)
76
+ if Billing.configuration.process_webhooks_async
77
+ Billing::Webhooks::ProcessEventJob.perform_later(
78
+ gateway: gateway, event_data: event.deep_stringify_keys
79
+ )
80
+ else
81
+ run_handler(gateway: gateway, event: event)
82
+ end
83
+ end
84
+
85
+ def run_handler(gateway:, event:)
86
+ handler_class = Billing::Webhooks::EventRouter.handler_for(event[:type])
87
+ return unless handler_class
88
+
89
+ handler_class.new(event: event, gateway: gateway).call
90
+ end
91
+
92
+ def object_id_for(object)
93
+ return nil if object.nil?
94
+
95
+ object.respond_to?(:id) ? object.id : (object.is_a?(Hash) && (object[:id] || object["id"]))
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ # Format minor-unit integer amounts (Stripe's native form — pence,
5
+ # cents, kuruş) into human-readable money strings. Stripe always
6
+ # quotes amounts as smallest-unit integers; the host's price tag
7
+ # never says "12.99" — it says "1299" for USD/GBP/EUR or "1299" for
8
+ # JPY (which has no minor unit).
9
+ #
10
+ # format_money(1299, "GBP") # => "£12.99"
11
+ # format_money(1299, "JPY") # => "¥1299"
12
+ #
13
+ # Hosts can override the symbol map by reopening this module in an
14
+ # initializer or monkey-patching SYMBOLS.
15
+ module CurrencyHelper
16
+ SYMBOLS = {
17
+ "USD" => "$",
18
+ "GBP" => "£",
19
+ "EUR" => "€",
20
+ "JPY" => "¥",
21
+ "CAD" => "$",
22
+ "AUD" => "$"
23
+ }.freeze
24
+
25
+ # Currencies with no minor unit (Stripe's "zero-decimal currencies").
26
+ # Source: https://docs.stripe.com/currencies#zero-decimal
27
+ ZERO_DECIMAL = %w[BIF CLP DJF GNF JPY KMF KRW MGA PYG RWF UGX VND VUV XAF XOF XPF].freeze
28
+
29
+ module_function
30
+
31
+ def format_money(amount_in_minor_units, currency_code)
32
+ code = currency_code.to_s.upcase
33
+ symbol = SYMBOLS[code] || "#{code} "
34
+
35
+ if ZERO_DECIMAL.include?(code)
36
+ "#{symbol}#{amount_in_minor_units.to_i}"
37
+ else
38
+ major = amount_in_minor_units.to_i / 100.0
39
+ decimal = format("%.2f", major)
40
+ "#{symbol}#{decimal}"
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ class ApplicationJob < ::ApplicationJob
5
+ end
6
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ class CancelSubscriptionJob < ApplicationJob
5
+ queue_as :billing
6
+
7
+ def perform(subscription_ref:)
8
+ result = Billing.gateway.cancel_subscription(subscription_ref: subscription_ref)
9
+
10
+ sub = Billing::Subscription.find_by(gateway_ref: subscription_ref)
11
+ sub&.update!(status: result[:status])
12
+
13
+ if sub.nil?
14
+ # Local row missing — host's notification subscriber has no
15
+ # account_id / customer_ref to resolve the Account and will
16
+ # silently no-op. Log so the gap is visible (e.g. drift
17
+ # between Stripe + local DB after a partial migration).
18
+ Seams::Observability.adapter.warn(
19
+ "billing.cancel_subscription.local_row_missing",
20
+ engine: "billing", subscription_ref: subscription_ref
21
+ )
22
+ end
23
+
24
+ # Canonical event payload — must match what WebhooksController
25
+ # emits for the same event so subscribers can read one shape:
26
+ # { gateway:, livemode:, account_id:, customer_ref:, ref:, object_id:, object: }
27
+ Seams::Events::Publisher.publish(
28
+ "subscription.canceled.billing",
29
+ gateway: Billing.configuration.gateway_name,
30
+ livemode: false,
31
+ account_id: sub&.account_id,
32
+ customer_ref: sub&.customer_ref,
33
+ ref: subscription_ref,
34
+ object_id: subscription_ref,
35
+ object: { id: subscription_ref, account_id: sub&.account_id, status: result[:status] }
36
+ )
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ class StartSubscriptionJob < ApplicationJob
5
+ queue_as :billing
6
+
7
+ def perform(account_id:, customer_ref:, plan_ref:)
8
+ result = Billing.gateway.create_subscription(customer_ref: customer_ref, plan_ref: plan_ref)
9
+
10
+ Billing::Subscription.create!(
11
+ account_id: account_id,
12
+ customer_ref: customer_ref,
13
+ plan_ref: result[:plan_ref] || plan_ref,
14
+ gateway_ref: result[:id],
15
+ status: result[:status]
16
+ )
17
+
18
+ # Canonical event payload — same shape as WebhooksController emits:
19
+ # { gateway:, livemode:, account_id:, customer_ref:, ref:, object_id:, object: }
20
+ Seams::Events::Publisher.publish(
21
+ "subscription.created.billing",
22
+ gateway: Billing.configuration.gateway_name,
23
+ livemode: false,
24
+ account_id: account_id,
25
+ customer_ref: customer_ref,
26
+ ref: result[:id],
27
+ object_id: result[:id],
28
+ object: { id: result[:id], account_id: account_id, plan: { id: plan_ref }, status: result[:status] }
29
+ )
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Webhooks
5
+ # Async wrapper around the synchronous handler dispatch. Hosts
6
+ # who want sub-100ms webhook responses (Stripe's recommendation —
7
+ # https://docs.stripe.com/webhooks#acknowledge-events-immediately)
8
+ # set Billing.configuration.process_webhooks_async = true and the
9
+ # WebhooksController enqueues this job instead of running the
10
+ # handler in the request thread.
11
+ #
12
+ # The event payload is serialised as a hash so the job survives
13
+ # Active Job's argument restrictions; the handler reconstructs
14
+ # what it needs from that hash.
15
+ class ProcessEventJob < Billing::ApplicationJob
16
+ queue_as :billing
17
+
18
+ def perform(gateway:, event_data:)
19
+ event = symbolize(event_data)
20
+ handler = Billing::Webhooks::EventRouter.handler_for(event[:type])
21
+ return unless handler
22
+
23
+ handler.new(event: event, gateway: gateway).call
24
+ end
25
+
26
+ private
27
+
28
+ def symbolize(hash)
29
+ case hash
30
+ when Hash then hash.transform_keys(&:to_sym).transform_values { |value| symbolize(value) }
31
+ when Array then hash.map { |element| symbolize(element) }
32
+ else hash
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ class ApplicationRecord < ::ApplicationRecord
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ # Local mirror of a gateway invoice. Keyed on `account_id` (the
5
+ # Accounts::Account tenant) AND `customer_ref` (the gateway's
6
+ # `cus_*` id) so subscribers can address it either way. No
7
+ # `belongs_to :account` — cross-engine model access is forbidden;
8
+ # querying flows through `account.billing_invoices` (provided by
9
+ # Billing::Billable on the host's Account model).
10
+ class Invoice < ApplicationRecord
11
+ self.table_name = "billing_invoices"
12
+
13
+ STATUSES = %w[draft open paid void uncollectible].freeze
14
+
15
+ validates :gateway_ref, presence: true, uniqueness: true
16
+ validates :account_id, presence: true
17
+ validates :customer_ref, presence: true
18
+ validates :amount_cents, numericality: { greater_than_or_equal_to: 0 }
19
+ validates :currency, presence: true
20
+ validates :status, inclusion: { in: STATUSES }
21
+
22
+ # Convenience association — Stripe sends `subscription` as a
23
+ # gateway-side id (sub_*), not a local FK; resolve through the
24
+ # local subscription mirror by gateway_ref.
25
+ def subscription
26
+ return nil if subscription_ref.blank?
27
+
28
+ Billing::Subscription.find_by(gateway_ref: subscription_ref)
29
+ end
30
+
31
+ def paid?
32
+ status == "paid"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ # A LifetimePass is a one-time-paid OR privately-granted permanent
5
+ # access record. It deliberately lives in its own table rather than
6
+ # piggy-backing on Subscription with a `lifetime: true` flag — the
7
+ # semantics diverge enough (no current_period_end, no renewal
8
+ # webhook, no proration) that mixing them invites bugs.
9
+ #
10
+ # Belongs to an Accounts::Account (the tenant the entitlement
11
+ # applies to). The "granted_by" / "revoked_by" columns reference
12
+ # an Auth::Identity (the human who pressed the button) — Identity
13
+ # rather than Account because they track human action, not
14
+ # tenant-level data.
15
+ #
16
+ # Two creation paths:
17
+ # 1. Public — host's pricing page → Stripe Checkout (mode: payment)
18
+ # → `checkout.session.completed` webhook → CreatePassFromCheckoutService.
19
+ # `gateway_ref` is the Stripe Checkout Session id; `granted_by_identity_id`
20
+ # is nil (paid, not granted).
21
+ # 2. Private grant — admin form → GrantPassService.
22
+ # `gateway_ref` is nil (no Stripe charge); `granted_by_identity_id`
23
+ # is the admin Identity who issued it.
24
+ #
25
+ # Active passes can be revoked via RevokePassService (refunded,
26
+ # ToS violation, etc.) — sets `revoked_at` rather than deleting so
27
+ # the audit trail survives.
28
+ class LifetimePass < ApplicationRecord
29
+ self.table_name = "billing_lifetime_passes"
30
+
31
+ belongs_to :plan, class_name: "Billing::Plan",
32
+ foreign_key: :plan_ref, primary_key: :gateway_ref,
33
+ optional: true
34
+
35
+ validates :account_id, :customer_ref, :plan_ref, :granted_at, presence: true
36
+ validates :account_id, uniqueness: { scope: :plan_ref,
37
+ message: "already has a lifetime pass for this plan" }
38
+
39
+ scope :active, -> { where(revoked_at: nil) }
40
+ scope :revoked, -> { where.not(revoked_at: nil) }
41
+
42
+ # True for paid passes (Stripe Checkout completed), false for
43
+ # privately-granted ones (admin form, no charge).
44
+ def paid?
45
+ gateway_ref.present?
46
+ end
47
+
48
+ def granted?
49
+ granted_by_identity_id.present?
50
+ end
51
+
52
+ def revoked?
53
+ revoked_at.present?
54
+ end
55
+
56
+ def active?
57
+ !revoked?
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ # A subscription plan. Mirror of a Stripe Price (or whatever your
5
+ # gateway calls a recurring billable thing). Hosts seed Plans
6
+ # locally so the pricing page can render without a Stripe round-trip.
7
+ class Plan < ApplicationRecord
8
+ # Raised when a lifetime plan with a finite `max_lifetime_units`
9
+ # capacity has no remaining inventory at the moment of purchase.
10
+ # `Billing::Lifetime::CreateLifetimeSessionService` rescues this
11
+ # and converts it into a ServiceResult-style failure so callers
12
+ # see a friendly "sold out" message rather than an exception
13
+ # bubbling up from inside a transaction.
14
+ class SoldOut < StandardError; end
15
+
16
+ self.table_name = "billing_plans"
17
+
18
+ # `lifetime` is the LTD interval — a one-time payment with permanent
19
+ # access. Stripe Checkout uses `mode: "payment"` for these instead
20
+ # of `mode: "subscription"`. See LifetimePass + the LTD section in
21
+ # the README for the trade-off (no recurring revenue, indefinite
22
+ # support burden).
23
+ INTERVALS = %w[day week month year lifetime].freeze
24
+
25
+ has_many :lifetime_passes, class_name: "Billing::LifetimePass",
26
+ foreign_key: :plan_ref, primary_key: :gateway_ref,
27
+ dependent: :nullify
28
+
29
+ validates :gateway_ref, presence: true, uniqueness: true
30
+ validates :name, presence: true
31
+ validates :amount_cents, numericality: { greater_than_or_equal_to: 0 }
32
+ validates :currency, presence: true
33
+ validates :interval, inclusion: { in: INTERVALS }
34
+
35
+ scope :active, -> { where(active: true) }
36
+ scope :recurring, -> { where.not(interval: "lifetime") }
37
+ scope :lifetime, -> { where(interval: "lifetime") }
38
+
39
+ def free?
40
+ amount_cents.zero?
41
+ end
42
+
43
+ def has_trial?
44
+ trial_period_days.to_i.positive?
45
+ end
46
+
47
+ def lifetime?
48
+ interval == "lifetime"
49
+ end
50
+
51
+ # nil = unlimited; otherwise the count of LTD slots still available
52
+ # (max_lifetime_units minus already-issued LifetimePass count).
53
+ def lifetime_inventory_remaining
54
+ return nil if max_lifetime_units.nil? || !lifetime?
55
+
56
+ [max_lifetime_units - lifetime_passes.count, 0].max
57
+ end
58
+
59
+ def lifetime_sold_out?
60
+ lifetime? && lifetime_inventory_remaining&.zero?
61
+ end
62
+
63
+ # Lock-and-check guard that the lifetime inventory cap has not
64
+ # already been hit. Must be called from inside an open
65
+ # `transaction` block so the `SELECT ... FOR UPDATE` row-lock is
66
+ # held until commit — that's what makes the check race-free across
67
+ # concurrent buyers.
68
+ #
69
+ # No-op for plans without a `max_lifetime_units` cap (nil = unlimited)
70
+ # and for non-lifetime plans (cap is meaningless on recurring plans).
71
+ #
72
+ # Trade-off: holding a row lock across the Stripe API call (the
73
+ # caller does this so the lock spans the Checkout-session creation)
74
+ # means concurrent buyers serialise. For viral promos this can slow
75
+ # checkout under load. The alternative — letting the check race —
76
+ # oversells the plan, which costs refunds + support time + brand
77
+ # damage. For LTDs (capped, low-volume by design) the lock is the
78
+ # right trade. A more sophisticated optimistic alternative
79
+ # (counter column + `UPDATE ... WHERE remaining > 0 RETURNING`) is
80
+ # documented in the README but is out of scope for the generator.
81
+ #
82
+ # Raises `Billing::Plan::SoldOut` when the cap is exhausted.
83
+ def enforce_lifetime_inventory_or_raise!
84
+ return unless lifetime?
85
+ return if max_lifetime_units.nil?
86
+
87
+ lock!
88
+ return unless lifetime_sold_out?
89
+
90
+ raise SoldOut,
91
+ "Plan #{gateway_ref.inspect} is sold out " \
92
+ "(cap: #{max_lifetime_units})"
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ # Subscription record mirrored from the gateway. Keyed locally on
5
+ # `account_id` (UUID, the Accounts::Account tenant id) and on
6
+ # `customer_ref` (the gateway's `cus_*` id). Both are present on
7
+ # every row so the same data can be addressed via either side.
8
+ #
9
+ # No `belongs_to :account` declaration: cross-engine model access
10
+ # is forbidden by Seams::Cops::NoCrossEngineModelAccess. Hosts that
11
+ # want a typed handle can call `.account` on their Account model
12
+ # via the `Billing::Billable` concern (which queries the inverse
13
+ # association `account.billing_subscriptions`).
14
+ class Subscription < ApplicationRecord
15
+ self.table_name = "billing_subscriptions"
16
+
17
+ STATUSES = %w[trialing active past_due canceled unpaid incomplete incomplete_expired paused].freeze
18
+
19
+ validates :gateway_ref, presence: true, uniqueness: true
20
+ validates :account_id, presence: true
21
+ validates :customer_ref, presence: true
22
+ validates :plan_ref, presence: true
23
+ validates :status, inclusion: { in: STATUSES }
24
+
25
+ scope :active_or_trialing, -> { where(status: %w[active trialing]) }
26
+
27
+ def active?
28
+ %w[active trialing].include?(status)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ # Audit log of every accepted webhook event. Used to dedupe retries
5
+ # from the gateway (Stripe in particular retries until the endpoint
6
+ # responds 2xx).
7
+ class WebhookEvent < ApplicationRecord
8
+ self.table_name = "billing_webhook_events"
9
+
10
+ validates :gateway, :gateway_event_id, :event_type, presence: true
11
+ validates :gateway_event_id, uniqueness: { scope: :gateway }
12
+ end
13
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Checkout
5
+ # Creates a hosted-checkout session for a given customer + plan.
6
+ # Returns a Result with ok?, url, error.
7
+ module CreateSessionService
8
+ Result = Struct.new(:ok?, :url, :error, keyword_init: true)
9
+
10
+ module_function
11
+
12
+ def call(customer_ref:, plan_ref:, success_url:, cancel_url:)
13
+ session = Billing.gateway.create_checkout_session(
14
+ customer_ref: customer_ref,
15
+ plan_ref: plan_ref,
16
+ success_url: success_url,
17
+ cancel_url: cancel_url
18
+ )
19
+ Result.new(ok?: true, url: session[:url])
20
+ rescue Billing::GatewayError => e
21
+ Result.new(ok?: false, error: e.message)
22
+ end
23
+ end
24
+ end
25
+ end