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,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Gateways
5
+ # Contract every billing gateway must implement. Subclass this
6
+ # in the host application to wire Paddle, Adyen, Lemon Squeezy,
7
+ # then point Billing.configuration.gateway at the subclass name.
8
+ class Abstract
9
+ # Create or fetch a recurring subscription for the host's
10
+ # customer. `customer_ref` is whatever opaque identifier the
11
+ # host stores on its user (typically a Stripe customer id).
12
+ def create_subscription(customer_ref:, plan_ref:, **)
13
+ raise NotImplementedError, "#{self.class} must implement #create_subscription"
14
+ end
15
+
16
+ def cancel_subscription(subscription_ref:, **)
17
+ raise NotImplementedError, "#{self.class} must implement #cancel_subscription"
18
+ end
19
+
20
+ # Retrieve a subscription as a normalised hash:
21
+ # { id:, status:, current_period_end:, plan_ref: }
22
+ def fetch_subscription(subscription_ref:)
23
+ raise NotImplementedError, "#{self.class} must implement #fetch_subscription"
24
+ end
25
+
26
+ # Create a hosted checkout session. Returns a normalised hash:
27
+ # { id:, url: }
28
+ # The host redirects the user to `url`, the gateway handles the
29
+ # payment UI, and webhooks tell us the result.
30
+ def create_checkout_session(customer_ref:, plan_ref:, success_url:, cancel_url:, **)
31
+ raise NotImplementedError, "#{self.class} must implement #create_checkout_session"
32
+ end
33
+
34
+ # Create a customer-portal session so the user can self-serve
35
+ # their subscription (cancel, change plan, update payment
36
+ # method). Returns: { id:, url: }.
37
+ def create_billing_portal_session(customer_ref:, return_url:, **)
38
+ raise NotImplementedError, "#{self.class} must implement #create_billing_portal_session"
39
+ end
40
+
41
+ # Create a one-time-payment hosted-checkout session for an LTD
42
+ # plan. Same return shape as #create_checkout_session, but uses
43
+ # the gateway's `mode: payment` flow (Stripe) — no recurring
44
+ # billing, no customer.subscription.* webhooks. Idempotency
45
+ # carries through metadata so the webhook handler can resolve
46
+ # the LTD plan_ref without a separate API call.
47
+ def create_lifetime_checkout_session(customer_ref:, plan_ref:, success_url:, cancel_url:, **)
48
+ raise NotImplementedError, "#{self.class} must implement #create_lifetime_checkout_session"
49
+ end
50
+
51
+ # Verify a webhook payload + signature and return a normalised
52
+ # event hash:
53
+ # { id:, type:, livemode:, object: <provider's raw object>, raw: <payload> }
54
+ #
55
+ # `id:` is the gateway's own event id (e.g. evt_* from Stripe).
56
+ # The Billing webhook controller uses it to dedupe retries via
57
+ # Billing::WebhookEvent.unique(gateway, gateway_event_id).
58
+ # Implementations MUST raise Billing::WebhookError on any
59
+ # signature mismatch.
60
+ def verify_webhook(payload:, signature:, secret:)
61
+ raise NotImplementedError, "#{self.class} must implement #verify_webhook"
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Gateways
5
+ # Adyen gateway stub — generated when the host runs
6
+ # `bin/rails generate seams:billing --gateway=adyen`. Each method
7
+ # raises NotImplementedError. Verify against
8
+ # https://docs.adyen.com/api-explorer/ and ship via Faraday
9
+ # (no Net::HTTP — see feedback_external_apis.md).
10
+ class Adyen < Abstract
11
+ # Override every Abstract method here. Adyen uses a recurring
12
+ # payments / shopper reference flow — design your customer_ref
13
+ # mapping accordingly.
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Billing
4
+ module Gateways
5
+ # Paddle gateway stub — generated when the host runs
6
+ # `bin/rails generate seams:billing --gateway=paddle`. Each method
7
+ # raises NotImplementedError so the host knows exactly where to
8
+ # plug in Paddle's REST API. Verify against
9
+ # https://developer.paddle.com/api-reference and ship via Faraday
10
+ # (no Net::HTTP — see feedback_external_apis.md).
11
+ class Paddle < Abstract
12
+ # Override every Abstract method here. As a starting point:
13
+ #
14
+ # def create_checkout_session(customer_ref:, plan_ref:, success_url:, cancel_url:, **)
15
+ # # POST /transactions to https://api.paddle.com via Faraday.
16
+ # # Return: { id:, url: }
17
+ # end
18
+ #
19
+ # See lib/billing/stripe/client.rb for the Faraday wrapper pattern.
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "billing/gateways/abstract"
4
+ require "billing/stripe/client"
5
+ require "billing/stripe/webhook_signature"
6
+
7
+ module Billing
8
+ module Gateways
9
+ # Stripe billing gateway. Implements the Billing::Gateways::Abstract
10
+ # contract on top of the official `stripe` Ruby gem
11
+ # (https://github.com/stripe/stripe-ruby), wrapped in a thin
12
+ # facade at Billing::Stripe::Client so the rest of the engine
13
+ # has one place to call. Webhook signature verification uses
14
+ # Stripe::Webhook.construct_event from the same gem rather than
15
+ # a hand-rolled HMAC. Wave 8's earlier decision to roll a Faraday
16
+ # client was reversed in 2026-05 — the gem is actively maintained
17
+ # by Stripe and tracking their API ourselves wasn't worth the
18
+ # centralisation benefit Faraday alone provided.
19
+ #
20
+ # API surface used (verified against current Stripe docs):
21
+ # POST /v1/customers https://docs.stripe.com/api/customers/create
22
+ # GET /v1/customers/search https://docs.stripe.com/api/customers/search
23
+ # POST /v1/subscriptions https://docs.stripe.com/api/subscriptions/create
24
+ # POST /v1/subscriptions/{id} https://docs.stripe.com/api/subscriptions/update
25
+ # DELETE /v1/subscriptions/{id} https://docs.stripe.com/api/subscriptions/cancel
26
+ # GET /v1/subscriptions/{id} https://docs.stripe.com/api/subscriptions/retrieve
27
+ # GET /v1/invoices/{id} https://docs.stripe.com/api/invoices/retrieve
28
+ # POST /v1/checkout/sessions https://docs.stripe.com/api/checkout/sessions/create
29
+ # POST /v1/billing_portal/sessions https://docs.stripe.com/api/customer_portal/sessions/create
30
+ # Webhook signature https://docs.stripe.com/webhooks/signatures
31
+ class Stripe < Abstract
32
+ def initialize(client: nil)
33
+ super()
34
+ @injected_client = client
35
+ end
36
+
37
+ # Lazy so `Billing::Gateways::Stripe.new` works without an API
38
+ # key configured — important for contract specs that only check
39
+ # the method shape via reflection. Real callers exercise actual
40
+ # endpoints, which forces the client to be built (and the
41
+ # missing-key error to fire) at the call site, not at boot.
42
+ def client
43
+ @injected_client || @client ||= Billing::Stripe::Client.new(api_key: Billing.configuration.api_key)
44
+ end
45
+
46
+ # POST /v1/subscriptions
47
+ def create_subscription(customer_ref:, plan_ref:, **opts)
48
+ sub = client.create_subscription(
49
+ customer: customer_ref,
50
+ items: [{ price: plan_ref }],
51
+ **opts
52
+ )
53
+ normalise_subscription(sub)
54
+ end
55
+
56
+ # DELETE /v1/subscriptions/{id}
57
+ def cancel_subscription(subscription_ref:, **opts)
58
+ sub = client.cancel_subscription(subscription_ref, **opts)
59
+ normalise_subscription(sub)
60
+ end
61
+
62
+ # GET /v1/subscriptions/{id}
63
+ def fetch_subscription(subscription_ref:)
64
+ sub = client.retrieve_subscription(subscription_ref)
65
+ normalise_subscription(sub)
66
+ end
67
+
68
+ # POST /v1/checkout/sessions (mode: "subscription")
69
+ def create_checkout_session(customer_ref:, plan_ref:, success_url:, cancel_url:, **opts)
70
+ session = client.create_checkout_session({
71
+ mode: "subscription",
72
+ line_items: [{ price: plan_ref, quantity: 1 }],
73
+ customer: customer_ref,
74
+ success_url: success_url,
75
+ cancel_url: cancel_url
76
+ }.merge(opts))
77
+ { id: session["id"], url: session["url"] }
78
+ end
79
+
80
+ # POST /v1/billing_portal/sessions
81
+ def create_billing_portal_session(customer_ref:, return_url:, **)
82
+ session = client.create_billing_portal_session(
83
+ customer: customer_ref,
84
+ return_url: return_url
85
+ )
86
+ { id: session["id"], url: session["url"] }
87
+ end
88
+
89
+ # POST /v1/checkout/sessions (mode: "payment", LTD path).
90
+ # metadata.access_type=lifetime + metadata.plan_ref=<plan_ref>
91
+ # are duplicated on the session AND payment_intent_data so the
92
+ # webhook handler can read either one. Caller-supplied metadata
93
+ # (typically `account_id` so the webhook handler knows which
94
+ # tenant the resulting LifetimePass belongs to) is merged in;
95
+ # `access_type` and `plan_ref` always win to avoid an LTD
96
+ # session being misclassified.
97
+ def create_lifetime_checkout_session(customer_ref:, plan_ref:, success_url:, cancel_url:, metadata: {}, **opts)
98
+ merged_metadata = metadata.merge(access_type: "lifetime", plan_ref: plan_ref)
99
+ session = client.create_checkout_session({
100
+ mode: "payment",
101
+ line_items: [{ price: plan_ref, quantity: 1 }],
102
+ customer: customer_ref,
103
+ success_url: success_url,
104
+ cancel_url: cancel_url,
105
+ payment_intent_data: { metadata: merged_metadata },
106
+ metadata: merged_metadata
107
+ }.merge(opts))
108
+ { id: session["id"], url: session["url"] }
109
+ end
110
+
111
+ # https://docs.stripe.com/webhooks/signatures
112
+ # Verifies the Stripe-Signature header against `secret` via the
113
+ # official Stripe::Webhook helper. Raises Billing::WebhookError
114
+ # on any failure (no header / signature mismatch / timestamp
115
+ # outside the 5-minute tolerance window / non-JSON payload).
116
+ # Returns the normalised event hash on success.
117
+ def verify_webhook(payload:, signature:, secret:)
118
+ event = Billing::Stripe::WebhookSignature.verify!(
119
+ payload: payload,
120
+ signature_header: signature,
121
+ secret: secret
122
+ )
123
+ # event.data["object"] is a Stripe::StripeObject. Convert to
124
+ # a plain Hash so subscribers downstream don't need to know
125
+ # they're dealing with a Stripe SDK type — the engine's
126
+ # contract is "events are hashes".
127
+ object_hash = event.data["object"].respond_to?(:to_hash) ? event.data["object"].to_hash.transform_keys(&:to_s) : event.data["object"]
128
+ {
129
+ id: event["id"], # evt_* — controller dedupes via Billing::WebhookEvent
130
+ type: event["type"],
131
+ livemode: event["livemode"],
132
+ object: object_hash,
133
+ raw: payload
134
+ }
135
+ end
136
+
137
+ private
138
+
139
+ # Stripe's response shape changed: current_period_end now lives
140
+ # on items.data[].current_period_end. We try the subscription
141
+ # root first for backwards compatibility, then fall back to the
142
+ # first item.
143
+ def normalise_subscription(sub)
144
+ items_data = sub.dig("items", "data") || []
145
+ first_item = items_data.first || {}
146
+ {
147
+ id: sub["id"],
148
+ status: sub["status"],
149
+ current_period_end: sub["current_period_end"] || first_item["current_period_end"],
150
+ plan_ref: first_item.dig("price", "id")
151
+ }
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stripe"
4
+
5
+ module Billing
6
+ module Stripe
7
+ # Thin facade over the official `stripe` Ruby gem so the engine's
8
+ # service objects + gateway have one shape to call. The previous
9
+ # version of this file was a hand-rolled Faraday REST client we
10
+ # owned; that decision was reversed in 2026-05 (Wave 8 → reversed
11
+ # Wave 6) because Stripe maintains the gem as a first-class
12
+ # deliverable and keeping our own client current with their API
13
+ # was strictly more work than the centralisation benefit Faraday
14
+ # alone gave us.
15
+ #
16
+ # The public method surface is unchanged — every service object
17
+ # and the gateway adapter call this Client without knowing
18
+ # whether the implementation is Faraday or the gem.
19
+ #
20
+ # Stripe gem version pinned to ~> 13 in the host Gemfile by the
21
+ # Billing generator. The StripeClient API
22
+ # (`client.v1.<resource>.<action>`) is what Stripe documents at
23
+ # https://docs.stripe.com/api?lang=ruby for every endpoint we use.
24
+ #
25
+ # Verified against:
26
+ # https://docs.stripe.com/api/customers/create
27
+ # https://docs.stripe.com/api/customers/search
28
+ # https://docs.stripe.com/api/subscriptions/create
29
+ # https://docs.stripe.com/api/subscriptions/update
30
+ # https://docs.stripe.com/api/subscriptions/cancel
31
+ # https://docs.stripe.com/api/subscriptions/retrieve
32
+ # https://docs.stripe.com/api/invoices/retrieve
33
+ # https://docs.stripe.com/api/checkout/sessions/create
34
+ # https://docs.stripe.com/api/customer_portal/sessions/create
35
+ # https://docs.stripe.com/api/idempotent_requests
36
+ class Client
37
+ def initialize(api_key:)
38
+ @sdk = ::Stripe::StripeClient.new(api_key)
39
+ end
40
+
41
+ # POST /v1/subscriptions
42
+ def create_subscription(customer:, items:, **extra)
43
+ sdk.v1.subscriptions.create({ customer: customer, items: items }.merge(extra))
44
+ end
45
+
46
+ # DELETE /v1/subscriptions/{id}
47
+ def cancel_subscription(subscription_id, **extra)
48
+ sdk.v1.subscriptions.cancel(subscription_id, extra)
49
+ end
50
+
51
+ # GET /v1/subscriptions/{id}
52
+ def retrieve_subscription(subscription_id)
53
+ sdk.v1.subscriptions.retrieve(subscription_id)
54
+ end
55
+
56
+ # POST /v1/subscriptions/{id} — used by ChangePlanService (items[])
57
+ # and ReactivateService (cancel_at_period_end: false).
58
+ def update_subscription(subscription_id, **fields)
59
+ sdk.v1.subscriptions.update(subscription_id, fields)
60
+ end
61
+
62
+ # POST /v1/checkout/sessions — params is a Hash (subscription
63
+ # mode for recurring; payment mode for LTDs).
64
+ def create_checkout_session(params)
65
+ sdk.v1.checkout.sessions.create(params)
66
+ end
67
+
68
+ # POST /v1/billing_portal/sessions
69
+ def create_billing_portal_session(customer:, return_url: nil)
70
+ body = { customer: customer }
71
+ body[:return_url] = return_url if return_url
72
+ sdk.v1.billing_portal.sessions.create(body)
73
+ end
74
+
75
+ # POST /v1/customers. The optional `idempotency_key:` is the gem's
76
+ # standard per-request opts hash (Stripe holds the response for
77
+ # ~24h and replays it for the same key, so concurrent
78
+ # FindOrCreate calls during a signup burst produce one customer
79
+ # instead of two — see https://docs.stripe.com/api/idempotent_requests).
80
+ def create_customer(email:, idempotency_key: nil, **extra)
81
+ body = { email: email }.merge(extra)
82
+ opts = idempotency_key ? { idempotency_key: idempotency_key } : {}
83
+ sdk.v1.customers.create(body, opts)
84
+ end
85
+
86
+ # GET /v1/customers/search
87
+ def search_customers(query:, limit: 1)
88
+ sdk.v1.customers.search(query: query, limit: limit)
89
+ end
90
+
91
+ # GET /v1/invoices/{id}
92
+ def retrieve_invoice(invoice_id)
93
+ sdk.v1.invoices.retrieve(invoice_id)
94
+ end
95
+
96
+ private
97
+
98
+ attr_reader :sdk
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stripe"
4
+
5
+ module Billing
6
+ module Stripe
7
+ # Verifies the Stripe-Signature header on incoming webhooks via
8
+ # the official `stripe` gem's first-party helper.
9
+ #
10
+ # Stripe maintains the verification logic — HMAC-SHA256 over
11
+ # `<timestamp>.<payload>`, 5-minute tolerance window, multiple
12
+ # v1= entries for key rotation, constant-time comparison. We used
13
+ # to reimplement this against https://docs.stripe.com/webhooks/signatures
14
+ # in a hand-rolled HMAC module; that decision was reversed in
15
+ # 2026-05 because Stripe ships breaking changes to the signing
16
+ # scheme over time and the gem tracks them as a first-class
17
+ # commitment.
18
+ #
19
+ # Public surface preserved (verify!) so callers (Billing::Gateways::Stripe)
20
+ # don't need to change. Stripe::SignatureVerificationError is
21
+ # rethrown as Billing::WebhookError to keep the engine's error
22
+ # boundary intact.
23
+ module WebhookSignature
24
+ DEFAULT_TOLERANCE = 300 # seconds — matches Stripe's default
25
+
26
+ module_function
27
+
28
+ # Returns the parsed Stripe::Event on success; raises
29
+ # Billing::WebhookError on any failure mode (no header,
30
+ # malformed header, no v1, no matching v1, timestamp outside
31
+ # tolerance, malformed JSON).
32
+ def verify!(payload:, signature_header:, secret:, tolerance: DEFAULT_TOLERANCE)
33
+ raise Billing::WebhookError, "Stripe-Signature header missing" if signature_header.nil? || signature_header.empty?
34
+
35
+ ::Stripe::Webhook.construct_event(payload, signature_header, secret, tolerance: tolerance)
36
+ rescue ::Stripe::SignatureVerificationError => e
37
+ raise Billing::WebhookError, "Stripe signature invalid: #{e.message}"
38
+ rescue JSON::ParserError => e
39
+ raise Billing::WebhookError, "Stripe payload not JSON: #{e.message}"
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Smoke-check Billing configuration without hitting Stripe.
4
+ #
5
+ # Run from a host or CI before promoting a deploy:
6
+ #
7
+ # bin/rails seams:billing:check_config
8
+ #
9
+ # Exit non-zero if anything required is missing — wire it into your
10
+ # pre-deploy gate so a missing STRIPE_SECRET_KEY fails the deploy
11
+ # instead of silently surviving boot and 500ing on the first webhook.
12
+ namespace :seams do
13
+ namespace :billing do
14
+ desc "Verify Billing configuration is complete (api_key + webhook_secret) — exits non-zero if not"
15
+ task check_config: :environment do
16
+ missing = []
17
+ missing << "Billing.configuration.api_key (set STRIPE_SECRET_KEY or assign in initializer)" \
18
+ if Billing.configuration.api_key.to_s.strip.empty?
19
+ missing << "Billing.configuration.webhook_secret (set STRIPE_WEBHOOK_SECRET or assign in initializer)" \
20
+ if Billing.configuration.webhook_secret.to_s.strip.empty?
21
+
22
+ if missing.any?
23
+ warn "Billing config incomplete:"
24
+ missing.each { |m| warn " - #{m}" }
25
+ warn ""
26
+ warn "Without these, Billing::Gateways::Stripe will raise at the first " \
27
+ "outbound API call or webhook, not at boot. Set them before deploying."
28
+ abort("Billing config check failed.")
29
+ end
30
+
31
+ puts "Billing config OK: api_key + webhook_secret are present."
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Factories for Billing engine specs. Sequences keep ref columns
4
+ # (gateway_ref, customer_ref, plan_ref) unique across the spec run
5
+ # so uniqueness validations don't trip.
6
+ #
7
+ # Post-Wave-9: every billing-row factory carries an `account_id`
8
+ # (UUID) — the Accounts::Account the row belongs to. We don't
9
+ # `association :account` because cross-engine model access is
10
+ # forbidden by the Seams cops; the spec generates a fresh
11
+ # SecureRandom UUID per row, which is safe in a test DB without a
12
+ # DB-level FK to accounts. Hosts that have an Accounts::Account
13
+ # factory available can override the `account_id` value with one
14
+ # from a real `create(:account)`.
15
+ FactoryBot.define do
16
+ factory :billing_plan, class: "Billing::Plan" do
17
+ sequence(:gateway_ref) { |n| "price_test_#{n}" }
18
+ sequence(:name) { |n| "Plan #{n}" }
19
+ amount_cents { 1299 }
20
+ currency { "GBP" }
21
+ interval { "month" }
22
+ trial_period_days { 0 }
23
+
24
+ factory :billing_lifetime_plan do
25
+ interval { "lifetime" }
26
+ max_lifetime_units { nil }
27
+ sequence(:gateway_ref) { |n| "price_test_lifetime_#{n}" }
28
+ end
29
+ end
30
+
31
+ factory :billing_subscription, class: "Billing::Subscription" do
32
+ account_id { SecureRandom.uuid }
33
+ sequence(:gateway_ref) { |n| "sub_test_#{n}" }
34
+ sequence(:customer_ref) { |n| "cus_test_#{n}" }
35
+ sequence(:plan_ref) { |n| "price_test_#{n}" }
36
+ status { "active" }
37
+ current_period_end { 30.days.from_now }
38
+ end
39
+
40
+ factory :billing_invoice, class: "Billing::Invoice" do
41
+ account_id { SecureRandom.uuid }
42
+ sequence(:gateway_ref) { |n| "in_test_#{n}" }
43
+ sequence(:customer_ref) { |n| "cus_test_#{n}" }
44
+ sequence(:subscription_ref) { |n| "sub_test_#{n}" }
45
+ status { "paid" }
46
+ amount_cents { 1299 }
47
+ currency { "GBP" }
48
+ paid_at { Time.current }
49
+ end
50
+
51
+ factory :billing_lifetime_pass, class: "Billing::LifetimePass" do
52
+ account_id { SecureRandom.uuid }
53
+ sequence(:customer_ref) { |n| "cus_test_lifetime_#{n}" }
54
+ sequence(:plan_ref) { |n| "price_test_lifetime_#{n}" }
55
+ sequence(:gateway_ref) { |n| "cs_test_#{n}" }
56
+ granted_at { Time.current }
57
+ end
58
+
59
+ factory :billing_webhook_event, class: "Billing::WebhookEvent" do
60
+ gateway { "stripe" }
61
+ sequence(:gateway_event_id) { |n| "evt_test_#{n}" }
62
+ event_type { "customer.subscription.created" }
63
+ livemode { false }
64
+ end
65
+ end
@@ -0,0 +1,19 @@
1
+ {
2
+ "id": "evt_test_charge_refunded",
3
+ "object": "event",
4
+ "type": "charge.refunded",
5
+ "livemode": false,
6
+ "created": 1731100000,
7
+ "data": {
8
+ "object": {
9
+ "id": "ch_test_123",
10
+ "object": "charge",
11
+ "customer": "cus_test_123",
12
+ "payment_intent": "pi_test_123",
13
+ "amount": 1299,
14
+ "amount_refunded": 1299,
15
+ "currency": "gbp",
16
+ "refunded": true
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "id": "evt_test_checkout_session_completed",
3
+ "object": "event",
4
+ "type": "checkout.session.completed",
5
+ "livemode": false,
6
+ "created": 1731200000,
7
+ "data": {
8
+ "object": {
9
+ "id": "cs_test_123",
10
+ "object": "checkout.session",
11
+ "mode": "subscription",
12
+ "customer": "cus_test_123",
13
+ "subscription": "sub_test_123",
14
+ "payment_status": "paid"
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "id": "evt_test_subscription_created",
3
+ "object": "event",
4
+ "type": "customer.subscription.created",
5
+ "livemode": false,
6
+ "created": 1730000000,
7
+ "data": {
8
+ "object": {
9
+ "id": "sub_test_123",
10
+ "object": "subscription",
11
+ "customer": "cus_test_123",
12
+ "status": "active",
13
+ "current_period_end": 1732678400,
14
+ "items": {
15
+ "data": [
16
+ {
17
+ "id": "si_test_123",
18
+ "price": { "id": "price_test_pro", "lookup_key": "pro" },
19
+ "current_period_end": 1732678400
20
+ }
21
+ ]
22
+ }
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "id": "evt_test_subscription_deleted",
3
+ "object": "event",
4
+ "type": "customer.subscription.deleted",
5
+ "livemode": false,
6
+ "created": 1730200000,
7
+ "data": {
8
+ "object": {
9
+ "id": "sub_test_123",
10
+ "object": "subscription",
11
+ "customer": "cus_test_123",
12
+ "status": "canceled",
13
+ "canceled_at": 1730200000,
14
+ "items": { "data": [] }
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "id": "evt_test_trial_will_end",
3
+ "object": "event",
4
+ "type": "customer.subscription.trial_will_end",
5
+ "livemode": false,
6
+ "created": 1730300000,
7
+ "data": {
8
+ "object": {
9
+ "id": "sub_test_123",
10
+ "object": "subscription",
11
+ "customer": "cus_test_123",
12
+ "status": "trialing",
13
+ "trial_end": 1730560000,
14
+ "items": { "data": [{ "price": { "lookup_key": "pro" } }] }
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "id": "evt_test_subscription_updated",
3
+ "object": "event",
4
+ "type": "customer.subscription.updated",
5
+ "livemode": false,
6
+ "created": 1730100000,
7
+ "data": {
8
+ "object": {
9
+ "id": "sub_test_123",
10
+ "object": "subscription",
11
+ "customer": "cus_test_123",
12
+ "status": "active",
13
+ "current_period_end": 1735270400,
14
+ "items": {
15
+ "data": [
16
+ {
17
+ "id": "si_test_123",
18
+ "price": { "id": "price_test_pro_annual", "lookup_key": "pro_annual" },
19
+ "current_period_end": 1735270400
20
+ }
21
+ ]
22
+ }
23
+ },
24
+ "previous_attributes": {
25
+ "items": { "data": [{ "price": { "lookup_key": "pro" } }] }
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "id": "evt_test_invoice_created",
3
+ "object": "event",
4
+ "type": "invoice.created",
5
+ "livemode": false,
6
+ "created": 1730400000,
7
+ "data": {
8
+ "object": {
9
+ "id": "in_test_123",
10
+ "object": "invoice",
11
+ "customer": "cus_test_123",
12
+ "subscription": "sub_test_123",
13
+ "status": "draft",
14
+ "amount_due": 1299,
15
+ "currency": "gbp"
16
+ }
17
+ }
18
+ }