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,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Subscriptions
5
+ # Un-cancels a subscription that's pending cancellation at period
6
+ # end. No-op for subscriptions already past their period end —
7
+ # those need a fresh checkout flow, not a reactivate. Verified
8
+ # against
9
+ # https://docs.stripe.com/billing/subscriptions/cancel#reactivate-canceled-subscription.
10
+ class ReactivateService < Billing::StripeService
11
+ def initialize(subscription_ref:)
12
+ @subscription_ref = subscription_ref
13
+ end
14
+
15
+ def call_stripe(client)
16
+ client.update_subscription(@subscription_ref, cancel_at_period_end: false)
17
+ end
18
+
19
+ def on_success(stripe_response)
20
+ ServiceResult.ok(value: {
21
+ id: stripe_response[:id],
22
+ status: stripe_response[:status],
23
+ cancel_at_period_end: stripe_response[:cancel_at_period_end]
24
+ })
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Webhooks
5
+ # Maps a Stripe event type → the handler class that processes it.
6
+ # Hosts extend the map by reopening this module in an initializer:
7
+ #
8
+ # Billing::Webhooks::EventRouter.register(
9
+ # "customer.tax_id.created",
10
+ # "MyHost::TaxIdCreatedHandler"
11
+ # )
12
+ #
13
+ # Returns nil for unmapped events so the controller can no-op
14
+ # without raising — Stripe sends events the host hasn't subscribed
15
+ # to and that's normal.
16
+ module EventRouter
17
+ HANDLERS = {
18
+ "customer.subscription.created" => "Billing::Webhooks::Handlers::SubscriptionCreatedHandler",
19
+ "customer.subscription.updated" => "Billing::Webhooks::Handlers::SubscriptionUpdatedHandler",
20
+ "customer.subscription.deleted" => "Billing::Webhooks::Handlers::SubscriptionDeletedHandler",
21
+ "customer.subscription.trial_will_end" => "Billing::Webhooks::Handlers::SubscriptionTrialWillEndHandler",
22
+ "invoice.created" => "Billing::Webhooks::Handlers::InvoiceCreatedHandler",
23
+ "invoice.paid" => "Billing::Webhooks::Handlers::InvoicePaidHandler",
24
+ "invoice.payment_failed" => "Billing::Webhooks::Handlers::InvoicePaymentFailedHandler",
25
+ "invoice.finalized" => "Billing::Webhooks::Handlers::InvoiceFinalizedHandler",
26
+ "invoice.voided" => "Billing::Webhooks::Handlers::InvoiceVoidedHandler",
27
+ "payment_intent.succeeded" => "Billing::Webhooks::Handlers::PaymentSucceededHandler",
28
+ "payment_intent.payment_failed" => "Billing::Webhooks::Handlers::PaymentFailedHandler",
29
+ "charge.refunded" => "Billing::Webhooks::Handlers::ChargeRefundedHandler",
30
+ "checkout.session.completed" => "Billing::Webhooks::Handlers::CheckoutSessionCompletedHandler",
31
+ "checkout.session.async_payment_succeeded" => "Billing::Webhooks::Handlers::CheckoutSessionCompletedHandler",
32
+ # Follow-up generators that ship handlers for additional Stripe events (customer.tax_id.created, payment_method.attached) splice their `"event.type" => "Billing::Webhooks::Handlers::ClassName",` entry here. Trailing commas on every entry keep the hash valid both before and after a splice.
33
+ # seams:insertion-point billing.event_router.handlers
34
+ }
35
+
36
+ module_function
37
+
38
+ def register(stripe_event_type, handler_class_name)
39
+ HANDLERS[stripe_event_type] = handler_class_name
40
+ end
41
+
42
+ def handler_for(stripe_event_type)
43
+ klass_name = HANDLERS[stripe_event_type]
44
+ return nil unless klass_name
45
+
46
+ klass_name.constantize
47
+ end
48
+
49
+ def supported_event_types
50
+ HANDLERS.keys
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Webhooks
5
+ # Base class for every webhook handler. Subclasses implement
6
+ # #call which receives a normalised event hash:
7
+ #
8
+ # { id:, type:, livemode:, object:, raw: }
9
+ #
10
+ # And typically:
11
+ # 1. Upserts the local Billing::Subscription / Billing::Invoice
12
+ # so downstream subscribers can read from the DB rather than
13
+ # reparsing the raw payload.
14
+ # 2. Calls #publish with the canonical seams event name + the
15
+ # shared payload shape.
16
+ #
17
+ # Subclasses MUST set SEAMS_EVENT to the canonical event name
18
+ # (e.g. "subscription.created.billing"). EventRouter routes by the
19
+ # Stripe event type; SEAMS_EVENT is what we re-publish.
20
+ class Handler
21
+ SEAMS_EVENT = nil # override in subclasses
22
+
23
+ def initialize(event:, gateway:)
24
+ @event = event
25
+ @gateway = gateway
26
+ end
27
+
28
+ def call
29
+ raise NotImplementedError, "#{self.class} must implement #call"
30
+ end
31
+
32
+ protected
33
+
34
+ attr_reader :event, :gateway
35
+
36
+ def object_hash
37
+ @object_hash ||= begin
38
+ obj = event[:object]
39
+ obj.respond_to?(:to_hash) ? obj.to_hash : obj
40
+ end
41
+ end
42
+
43
+ def object_id
44
+ @object_id ||= begin
45
+ obj = event[:object]
46
+ return nil if obj.nil?
47
+
48
+ obj.respond_to?(:id) ? obj.id : (obj.is_a?(Hash) && (obj[:id] || obj["id"]))
49
+ end
50
+ end
51
+
52
+ def customer_ref
53
+ @customer_ref ||= object_hash.is_a?(Hash) && (object_hash[:customer] || object_hash["customer"])
54
+ end
55
+
56
+ # Resolves the Accounts::Account id for this webhook event by
57
+ # cross-referencing the gateway's customer id (`cus_*`) against
58
+ # the local Billing::Subscription mirror. Returns nil if no
59
+ # matching local row exists yet (e.g. the very first
60
+ # `customer.subscription.created` event lands before the local
61
+ # row is upserted; the leaf handler does the upsert and reads
62
+ # account_id off the just-saved row in that case).
63
+ def account_id
64
+ return @account_id if defined?(@account_id)
65
+
66
+ @account_id =
67
+ Billing::Subscription.where(customer_ref: customer_ref).pick(:account_id) if customer_ref
68
+ end
69
+
70
+ def publish(seams_event = self.class::SEAMS_EVENT, ref: object_id)
71
+ Seams::Events::Publisher.publish(
72
+ seams_event,
73
+ gateway: gateway,
74
+ livemode: event[:livemode],
75
+ account_id: account_id,
76
+ customer_ref: customer_ref,
77
+ ref: ref,
78
+ object_id: object_id,
79
+ object: object_hash
80
+ )
81
+ end
82
+
83
+ def warn_upsert_failure(error)
84
+ Seams::Observability.adapter.warn(
85
+ "billing.webhook.upsert_failed",
86
+ engine: "billing",
87
+ event: self.class::SEAMS_EVENT,
88
+ error: error.message
89
+ )
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Webhooks
5
+ module Handlers
6
+ # charge.refunded — full or partial refund applied. Subscribers
7
+ # typically use this to revoke access, send a refund-receipt
8
+ # email, or reverse internal credit grants.
9
+ class ChargeRefundedHandler < Billing::Webhooks::Handler
10
+ SEAMS_EVENT = "charge.refunded.billing"
11
+
12
+ def call
13
+ publish
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Webhooks
5
+ module Handlers
6
+ # Two distinct paths through one Stripe event:
7
+ #
8
+ # - mode: "subscription" → the recurring subscription is now
9
+ # paid + active. The customer.subscription.created event
10
+ # usually arrives first; this is the confirmation that the
11
+ # checkout flow itself completed.
12
+ # - mode: "payment" with metadata.access_type == "lifetime" →
13
+ # a Lifetime Deal was purchased. Hands off to the LTD
14
+ # service which creates the LifetimePass row + emits
15
+ # lifetime.purchased.billing.
16
+ #
17
+ # Stripe also fires checkout.session.async_payment_succeeded for
18
+ # delayed methods (ACH). The router maps both to this handler.
19
+ # Verified against
20
+ # https://docs.stripe.com/payments/checkout/fulfill-orders.
21
+ class CheckoutSessionCompletedHandler < Billing::Webhooks::Handler
22
+ SEAMS_EVENT = "checkout.session_completed.billing"
23
+
24
+ def call
25
+ if lifetime?
26
+ handle_lifetime
27
+ else
28
+ publish
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def lifetime?
35
+ mode_value == "payment" && access_type == "lifetime"
36
+ end
37
+
38
+ def handle_lifetime
39
+ Billing::Lifetime::CreatePassFromCheckoutService.call(
40
+ session: event[:object],
41
+ livemode: event[:livemode]
42
+ )
43
+ end
44
+
45
+ def mode_value
46
+ object_hash.is_a?(Hash) && (object_hash[:mode] || object_hash["mode"])
47
+ end
48
+
49
+ def access_type
50
+ metadata = object_hash.is_a?(Hash) && (object_hash[:metadata] || object_hash["metadata"])
51
+ return nil unless metadata.is_a?(Hash)
52
+
53
+ metadata[:access_type] || metadata["access_type"]
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Webhooks
5
+ module Handlers
6
+ # invoice.created fires when Stripe drafts a new invoice (e.g.
7
+ # at the start of a billing period). Status is "draft" until
8
+ # finalised. Hosts rarely need to act on this — included for
9
+ # full audit coverage.
10
+ class InvoiceCreatedHandler < InvoiceHandlerBase
11
+ SEAMS_EVENT = "invoice.created.billing"
12
+ INVOICE_STATUS = "draft"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Webhooks
5
+ module Handlers
6
+ # invoice.finalized — Stripe finalised the draft invoice and is
7
+ # about to attempt collection. Status flips draft → open.
8
+ class InvoiceFinalizedHandler < InvoiceHandlerBase
9
+ SEAMS_EVENT = "invoice.finalized.billing"
10
+ INVOICE_STATUS = "open"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Webhooks
5
+ module Handlers
6
+ # Shared base for the five invoice.* handlers. Owns the upsert
7
+ # of Billing::Invoice. Subclasses set SEAMS_EVENT + INVOICE_STATUS.
8
+ #
9
+ # Account resolution mirrors SubscriptionHandlerBase: the
10
+ # invoice belongs to the same Account as the subscription it
11
+ # references; we resolve account_id by walking
12
+ # subscription_ref → Billing::Subscription#account_id, falling
13
+ # back to a sibling Billing::Invoice on the same customer_ref
14
+ # for one-off invoices that have no subscription.
15
+ class InvoiceHandlerBase < Billing::Webhooks::Handler
16
+ DEFAULT_STATUS = "open"
17
+
18
+ def call
19
+ upsert_invoice
20
+ publish
21
+ end
22
+
23
+ protected
24
+
25
+ def upsert_invoice
26
+ return unless object_id && customer_ref && object_hash.is_a?(Hash)
27
+
28
+ invoice = Billing::Invoice.find_or_initialize_by(gateway_ref: object_id)
29
+ resolved_account_id = invoice.account_id || resolve_account_id_for_invoice
30
+ unless resolved_account_id
31
+ warn_upsert_failure(
32
+ StandardError.new("could not resolve account_id for customer_ref=#{customer_ref}")
33
+ )
34
+ return
35
+ end
36
+
37
+ invoice.account_id = resolved_account_id
38
+ invoice.customer_ref = customer_ref
39
+ invoice.subscription_ref = object_hash[:subscription] || object_hash["subscription"]
40
+ invoice.amount_cents = object_hash[:amount_paid] || object_hash["amount_paid"] ||
41
+ object_hash[:amount_due] || object_hash["amount_due"] || 0
42
+ invoice.currency = (object_hash[:currency] || object_hash["currency"] || "usd").to_s.upcase
43
+ invoice.status = invoice_status
44
+ invoice.paid_at = paid_at_for_status
45
+ invoice.save!
46
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique => e
47
+ warn_upsert_failure(e)
48
+ end
49
+
50
+ # First, try the subscription_ref → Subscription mirror.
51
+ # Fall back to any sibling Invoice on the same customer.
52
+ # Final fallback is a Subscription row on the same
53
+ # customer_ref (covers the case of a one-off invoice that
54
+ # arrives before any sibling Invoice).
55
+ def resolve_account_id_for_invoice
56
+ subscription_ref = object_hash[:subscription] || object_hash["subscription"]
57
+ if subscription_ref
58
+ id = Billing::Subscription.where(gateway_ref: subscription_ref).pick(:account_id)
59
+ return id if id
60
+ end
61
+
62
+ Billing::Invoice.where(customer_ref: customer_ref)
63
+ .where.not(account_id: nil)
64
+ .pick(:account_id) ||
65
+ Billing::Subscription.where(customer_ref: customer_ref)
66
+ .where.not(account_id: nil)
67
+ .pick(:account_id)
68
+ end
69
+
70
+ def invoice_status
71
+ self.class::INVOICE_STATUS || DEFAULT_STATUS
72
+ end
73
+
74
+ def paid_at_for_status
75
+ invoice_status == "paid" ? Time.current : nil
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Webhooks
5
+ module Handlers
6
+ class InvoicePaidHandler < InvoiceHandlerBase
7
+ SEAMS_EVENT = "invoice.paid.billing"
8
+ INVOICE_STATUS = "paid"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Webhooks
5
+ module Handlers
6
+ class InvoicePaymentFailedHandler < InvoiceHandlerBase
7
+ SEAMS_EVENT = "invoice.failed.billing"
8
+ INVOICE_STATUS = "open"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Webhooks
5
+ module Handlers
6
+ class InvoiceVoidedHandler < InvoiceHandlerBase
7
+ SEAMS_EVENT = "invoice.voided.billing"
8
+ INVOICE_STATUS = "void"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Webhooks
5
+ module Handlers
6
+ class PaymentFailedHandler < Billing::Webhooks::Handler
7
+ SEAMS_EVENT = "payment.failed.billing"
8
+
9
+ def call
10
+ publish
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Webhooks
5
+ module Handlers
6
+ # payment_intent.succeeded — a one-off payment cleared. No local
7
+ # row to upsert (PaymentIntents are not modelled locally — see
8
+ # Phase 3A scope review). Just publishes the canonical event so
9
+ # subscribers can react (notifications, audit log, etc).
10
+ class PaymentSucceededHandler < Billing::Webhooks::Handler
11
+ SEAMS_EVENT = "payment.succeeded.billing"
12
+
13
+ def call
14
+ publish
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Webhooks
5
+ module Handlers
6
+ class SubscriptionCreatedHandler < SubscriptionHandlerBase
7
+ SEAMS_EVENT = "subscription.created.billing"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Webhooks
5
+ module Handlers
6
+ # Stripe sends customer.subscription.deleted when the
7
+ # subscription has actually ended (period elapsed after a
8
+ # period-end cancel, or immediate cancellation). The local
9
+ # status flips to "canceled".
10
+ class SubscriptionDeletedHandler < SubscriptionHandlerBase
11
+ SEAMS_EVENT = "subscription.canceled.billing"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Webhooks
5
+ module Handlers
6
+ # Base for the four customer.subscription.* handlers. Owns the
7
+ # upsert of Billing::Subscription so each leaf class just sets
8
+ # SEAMS_EVENT and inherits.
9
+ #
10
+ # Account resolution: for subscriptions created via the host's
11
+ # `Account#start_subscription!` flow, StartSubscriptionJob has
12
+ # already inserted a Billing::Subscription row with `account_id`
13
+ # set; the upsert here finds it via `gateway_ref` and preserves
14
+ # the existing `account_id`. For subscriptions created directly
15
+ # in the Stripe Dashboard (no local row pre-exists), the upsert
16
+ # falls back to looking up `account_id` from any sibling row
17
+ # under the same `customer_ref`. If neither path resolves,
18
+ # the upsert is skipped + a warning is logged — the host
19
+ # supports this case by reconciling the orphaned subscription
20
+ # via a sync task (see README "Stripe-initiated subscriptions").
21
+ class SubscriptionHandlerBase < Billing::Webhooks::Handler
22
+ def call
23
+ upsert_subscription
24
+ publish
25
+ end
26
+
27
+ protected
28
+
29
+ def upsert_subscription
30
+ return unless object_id && customer_ref && object_hash.is_a?(Hash)
31
+
32
+ subscription = Billing::Subscription.find_or_initialize_by(gateway_ref: object_id)
33
+ resolved_account_id = subscription.account_id || resolve_account_id_from_sibling
34
+ unless resolved_account_id
35
+ warn_upsert_failure(
36
+ StandardError.new("could not resolve account_id for customer_ref=#{customer_ref}")
37
+ )
38
+ return
39
+ end
40
+
41
+ subscription.account_id = resolved_account_id
42
+ subscription.customer_ref = customer_ref
43
+ subscription.plan_ref = plan_ref_from_object_hash
44
+ subscription.status = object_hash[:status] || object_hash["status"] || subscription.status || "incomplete"
45
+ subscription.current_period_end = current_period_end_at
46
+ subscription.save!
47
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique => e
48
+ warn_upsert_failure(e)
49
+ end
50
+
51
+ # Look up account_id from any other Billing::Subscription row
52
+ # under the same Stripe customer. A customer almost always
53
+ # belongs to one Account post-Wave-9 (Account == Stripe
54
+ # customer), so the first sibling row's account_id is the
55
+ # correct answer.
56
+ def resolve_account_id_from_sibling
57
+ Billing::Subscription.where(customer_ref: customer_ref)
58
+ .where.not(account_id: nil)
59
+ .pick(:account_id)
60
+ end
61
+
62
+ def plan_ref_from_object_hash
63
+ first = first_subscription_item
64
+ return nil unless first
65
+
66
+ price = first[:price] || first["price"]
67
+ price.is_a?(Hash) ? (price[:id] || price["id"]) : nil
68
+ end
69
+
70
+ # Stripe moved current_period_end onto each subscription item
71
+ # in 2024; older API versions still set it at the root. Try
72
+ # root first, fall back to items.data[0].
73
+ # https://docs.stripe.com/api/subscriptions/object
74
+ def current_period_end_at
75
+ unix = object_hash[:current_period_end] ||
76
+ object_hash["current_period_end"]
77
+ unix ||= (item = first_subscription_item) &&
78
+ (item[:current_period_end] || item["current_period_end"])
79
+ unix && Time.at(unix)
80
+ end
81
+
82
+ def first_subscription_item
83
+ items = object_hash[:items] || object_hash["items"]
84
+ return nil unless items.is_a?(Hash)
85
+
86
+ first = (items[:data] || items["data"] || []).first
87
+ first if first.is_a?(Hash)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Webhooks
5
+ module Handlers
6
+ # Fires ~3 days before a trialing subscription transitions to
7
+ # active. Hosts subscribe to send "your trial is ending" email.
8
+ # Verified against
9
+ # https://docs.stripe.com/api/events/types#event_types-customer.subscription.trial_will_end
10
+ class SubscriptionTrialWillEndHandler < SubscriptionHandlerBase
11
+ SEAMS_EVENT = "subscription.trial_will_end.billing"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Webhooks
5
+ module Handlers
6
+ class SubscriptionUpdatedHandler < SubscriptionHandlerBase
7
+ SEAMS_EVENT = "subscription.updated.billing"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,36 @@
1
+ <h1>Lifetime passes</h1>
2
+
3
+ <p><%%= link_to "Grant a new pass", billing.new_admin_lifetime_pass_path %></p>
4
+
5
+ <table>
6
+ <thead>
7
+ <tr>
8
+ <th>Account</th>
9
+ <th>Customer</th>
10
+ <th>Plan</th>
11
+ <th>Type</th>
12
+ <th>Granted at</th>
13
+ <th>Status</th>
14
+ <th></th>
15
+ </tr>
16
+ </thead>
17
+ <tbody>
18
+ <%% @passes.each do |pass| %>
19
+ <tr>
20
+ <td><%%= pass.account_id %></td>
21
+ <td><%%= pass.customer_ref %></td>
22
+ <td><%%= pass.plan_ref %></td>
23
+ <td><%%= pass.paid? ? "Paid" : "Granted" %></td>
24
+ <td><%%= pass.granted_at.utc.iso8601 %></td>
25
+ <td><%%= pass.revoked? ? "Revoked at #{pass.revoked_at.utc.iso8601}" : "Active" %></td>
26
+ <td>
27
+ <%% unless pass.revoked? %>
28
+ <%%= button_to "Revoke", billing.admin_lifetime_pass_path(pass),
29
+ method: :delete,
30
+ form: { onsubmit: "return confirm('Revoke this lifetime pass?')" } %>
31
+ <%% end %>
32
+ </td>
33
+ </tr>
34
+ <%% end %>
35
+ </tbody>
36
+ </table>
@@ -0,0 +1,37 @@
1
+ <h1>Grant a lifetime pass</h1>
2
+
3
+ <%%= form_with url: billing.admin_lifetime_passes_path, scope: nil, local: true do %>
4
+ <p>
5
+ <label>Account id (Accounts::Account#id, UUID):
6
+ <input type="text" name="account_id" required>
7
+ </label>
8
+ </p>
9
+ <p>
10
+ <label>Customer ref (Stripe customer id):
11
+ <input type="text" name="customer_ref" required>
12
+ </label>
13
+ </p>
14
+ <p>
15
+ <label>Plan:
16
+ <select name="plan_ref" required>
17
+ <option value=""></option>
18
+ <%% @plan_options.each do |plan| %>
19
+ <option value="<%%= plan.gateway_ref %>">
20
+ <%%= plan.name %> · <%%= plan.gateway_ref %>
21
+ <%% if (rem = plan.lifetime_inventory_remaining) %>
22
+ · <%%= rem %> remaining
23
+ <%% end %>
24
+ </option>
25
+ <%% end %>
26
+ </select>
27
+ </label>
28
+ </p>
29
+ <p>
30
+ <label>Notes:
31
+ <textarea name="notes" rows="3"></textarea>
32
+ </label>
33
+ </p>
34
+ <button type="submit">Grant lifetime pass</button>
35
+ <%% end %>
36
+
37
+ <p><%%= link_to "Back", billing.admin_lifetime_passes_path %></p>