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,382 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../rails_helper"
4
+ require_relative "../support/stripe_helpers"
5
+
6
+ # Runtime coverage for the 13 webhook handlers shipped under
7
+ # app/services/billing/webhooks/handlers/. Generator specs already
8
+ # assert the FILES exist + contain the right SEAMS_EVENT constants;
9
+ # this spec instantiates each handler with the matching Stripe event
10
+ # fixture, calls #call, and asserts:
11
+ #
12
+ # 1. The local Billing::Subscription / Billing::Invoice row is
13
+ # upserted with the expected columns (or NOT upserted, for the
14
+ # stateless payment_intent / charge handlers).
15
+ # 2. The canonical seams event is published with the expected
16
+ # payload shape (including `account_id` post-Wave-9).
17
+ #
18
+ # Why this exists: a regression in SubscriptionHandlerBase#upsert_subscription
19
+ # or InvoiceHandlerBase#upsert_invoice would not be caught by the
20
+ # string-matching generator specs alone — they ship green even if the
21
+ # upsert silently no-ops. This spec exercises the real ActiveRecord
22
+ # round-trip + the Seams::Events::Publisher contract.
23
+ #
24
+ # Account resolution: post-Wave-9 the upsert requires an `account_id`
25
+ # (the Accounts::Account UUID). The handler resolves it from any
26
+ # pre-existing Billing::Subscription / Billing::Invoice row sharing
27
+ # the same `customer_ref`. Each test seeds a sentinel row with the
28
+ # right customer_ref + a known account_id, then asserts the handler
29
+ # preserved or wrote that account_id on the row it cares about.
30
+ #
31
+ # CheckoutSessionCompletedHandler is covered for both the subscription
32
+ # branch (publishes checkout.session_completed.billing) and the
33
+ # Lifetime Deal branch (forks to Billing::Lifetime::CreatePassFromCheckoutService).
34
+ RSpec.describe "Billing webhook handlers (runtime)", type: :integration do
35
+ include StripeHelpers
36
+
37
+ # The known Account that every test references — kept stable so
38
+ # cross-test assertions (account_id is preserved through upsert,
39
+ # for instance) stay obvious.
40
+ TEST_ACCOUNT_ID = "11111111-1111-1111-1111-111111111111"
41
+
42
+ # Helper: load a fixture and reshape it into the event hash that
43
+ # Billing::Gateways::Stripe#verify_webhook hands to the handler.
44
+ # Fixtures are full Stripe event envelopes (top-level id/type +
45
+ # nested data.object); handlers expect a flat hash with :object
46
+ # holding what was data.object.
47
+ def event_for(fixture_name)
48
+ full = stripe_event_fixture(fixture_name)
49
+ {
50
+ id: full[:id],
51
+ type: full[:type],
52
+ livemode: full[:livemode],
53
+ object: full.dig(:data, :object),
54
+ raw: full.to_json
55
+ }
56
+ end
57
+
58
+ # Capture every payload published under +event_name+ during the
59
+ # block. Returns the Array of payload hashes. Uses the Publisher's
60
+ # public subscribe API so we exercise the real adapter dispatch
61
+ # (ActiveSupport::Notifications by default) rather than mocking the
62
+ # Publisher away.
63
+ def capture_published(event_name)
64
+ captured = []
65
+ subscriber = Seams::Events::Publisher.subscribe(event_name) { |payload| captured << payload }
66
+ yield
67
+ captured
68
+ ensure
69
+ Seams::Events::Publisher.unsubscribe(subscriber) if subscriber
70
+ end
71
+
72
+ # Seed a sibling Billing::Subscription on `customer_ref` so the
73
+ # handler base's resolve_account_id_from_sibling lookup can find
74
+ # it. Returns the seeded row.
75
+ def seed_account_on(customer_ref)
76
+ create(:billing_subscription,
77
+ account_id: TEST_ACCOUNT_ID,
78
+ customer_ref: customer_ref,
79
+ gateway_ref: "sub_seed_#{customer_ref}",
80
+ plan_ref: "price_seed",
81
+ status: "active")
82
+ end
83
+
84
+ let(:gateway) { "stripe" }
85
+
86
+ describe Billing::Webhooks::Handlers::SubscriptionCreatedHandler do
87
+ it "upserts a Billing::Subscription row + publishes subscription.created.billing" do
88
+ seed_account_on("cus_test_123")
89
+
90
+ event = event_for("customer_subscription_created")
91
+ captured = capture_published("subscription.created.billing") do
92
+ described_class.new(event: event, gateway: gateway).call
93
+ end
94
+
95
+ subscription = Billing::Subscription.find_by(gateway_ref: "sub_test_123")
96
+ expect(subscription).not_to be_nil
97
+ expect(subscription.account_id).to eq(TEST_ACCOUNT_ID)
98
+ expect(subscription.customer_ref).to eq("cus_test_123")
99
+ expect(subscription.plan_ref).to eq("price_test_pro")
100
+ expect(subscription.status).to eq("active")
101
+ expect(subscription.current_period_end).to be_within(1.second).of(Time.at(1_732_678_400))
102
+
103
+ expect(captured.size).to eq(1)
104
+ expect(captured.first[:gateway]).to eq("stripe")
105
+ expect(captured.first[:account_id]).to eq(TEST_ACCOUNT_ID)
106
+ expect(captured.first[:customer_ref]).to eq("cus_test_123")
107
+ expect(captured.first[:object_id]).to eq("sub_test_123")
108
+ end
109
+ end
110
+
111
+ describe Billing::Webhooks::Handlers::SubscriptionUpdatedHandler do
112
+ it "updates the existing Billing::Subscription row + publishes subscription.updated.billing" do
113
+ # Seed the row first so the handler exercises the find branch
114
+ # of find_or_initialize_by — guards against a regression that
115
+ # accidentally creates duplicates instead of updating.
116
+ create(:billing_subscription,
117
+ account_id: TEST_ACCOUNT_ID,
118
+ gateway_ref: "sub_test_123",
119
+ customer_ref: "cus_test_123",
120
+ plan_ref: "price_test_pro",
121
+ status: "active")
122
+
123
+ event = event_for("customer_subscription_updated")
124
+ captured = capture_published("subscription.updated.billing") do
125
+ described_class.new(event: event, gateway: gateway).call
126
+ end
127
+
128
+ subscription = Billing::Subscription.find_by(gateway_ref: "sub_test_123")
129
+ expect(Billing::Subscription.where(gateway_ref: "sub_test_123").count).to eq(1)
130
+ expect(subscription.account_id).to eq(TEST_ACCOUNT_ID)
131
+ expect(subscription.plan_ref).to eq("price_test_pro_annual")
132
+ expect(subscription.current_period_end).to be_within(1.second).of(Time.at(1_735_270_400))
133
+ expect(captured.size).to eq(1)
134
+ end
135
+ end
136
+
137
+ describe Billing::Webhooks::Handlers::SubscriptionDeletedHandler do
138
+ it "flips the row status to canceled + publishes subscription.canceled.billing" do
139
+ create(:billing_subscription,
140
+ account_id: TEST_ACCOUNT_ID,
141
+ gateway_ref: "sub_test_123",
142
+ customer_ref: "cus_test_123",
143
+ plan_ref: "price_test_pro",
144
+ status: "active")
145
+
146
+ # Stripe sends an empty items.data on the deletion event, but
147
+ # the handler still overwrites plan_ref from the payload — the
148
+ # fixture would fail validation. We patch in a minimal items
149
+ # block here so the upsert succeeds and we can assert on the
150
+ # status flip the handler is responsible for.
151
+ full = stripe_event_fixture("customer_subscription_deleted").deep_dup
152
+ full[:data][:object][:items] = {
153
+ data: [{ price: { id: "price_test_pro" } }]
154
+ }
155
+ event = {
156
+ id: full[:id],
157
+ type: full[:type],
158
+ livemode: full[:livemode],
159
+ object: full[:data][:object],
160
+ raw: full.to_json
161
+ }
162
+
163
+ captured = capture_published("subscription.canceled.billing") do
164
+ described_class.new(event: event, gateway: gateway).call
165
+ end
166
+
167
+ subscription = Billing::Subscription.find_by(gateway_ref: "sub_test_123")
168
+ expect(subscription.status).to eq("canceled")
169
+ expect(subscription.account_id).to eq(TEST_ACCOUNT_ID)
170
+ expect(captured.size).to eq(1)
171
+ end
172
+ end
173
+
174
+ describe Billing::Webhooks::Handlers::SubscriptionTrialWillEndHandler do
175
+ it "upserts the row + publishes subscription.trial_will_end.billing" do
176
+ seed_account_on("cus_test_123")
177
+
178
+ # The shipped fixture's items.data[0].price intentionally omits
179
+ # `id` (lookup_key only). We add `id` here so plan_ref_from_object_hash
180
+ # returns a value the model's presence validation accepts.
181
+ full = stripe_event_fixture("customer_subscription_trial_will_end").deep_dup
182
+ full[:data][:object][:items][:data][0][:price][:id] = "price_test_pro"
183
+ event = {
184
+ id: full[:id],
185
+ type: full[:type],
186
+ livemode: full[:livemode],
187
+ object: full[:data][:object],
188
+ raw: full.to_json
189
+ }
190
+
191
+ captured = capture_published("subscription.trial_will_end.billing") do
192
+ described_class.new(event: event, gateway: gateway).call
193
+ end
194
+
195
+ subscription = Billing::Subscription.find_by(gateway_ref: "sub_test_123")
196
+ expect(subscription).not_to be_nil
197
+ expect(subscription.status).to eq("trialing")
198
+ expect(subscription.account_id).to eq(TEST_ACCOUNT_ID)
199
+ expect(captured.size).to eq(1)
200
+ end
201
+ end
202
+
203
+ describe Billing::Webhooks::Handlers::InvoiceCreatedHandler do
204
+ it "upserts a draft Billing::Invoice row + publishes invoice.created.billing" do
205
+ seed_account_on("cus_test_123")
206
+
207
+ event = event_for("invoice_created")
208
+ captured = capture_published("invoice.created.billing") do
209
+ described_class.new(event: event, gateway: gateway).call
210
+ end
211
+
212
+ invoice = Billing::Invoice.find_by(gateway_ref: "in_test_123")
213
+ expect(invoice).not_to be_nil
214
+ expect(invoice.account_id).to eq(TEST_ACCOUNT_ID)
215
+ expect(invoice.customer_ref).to eq("cus_test_123")
216
+ expect(invoice.subscription_ref).to eq("sub_test_123")
217
+ expect(invoice.status).to eq("draft")
218
+ expect(invoice.amount_cents).to eq(1299)
219
+ expect(invoice.currency).to eq("GBP")
220
+ expect(invoice.paid_at).to be_nil
221
+ expect(captured.size).to eq(1)
222
+ end
223
+ end
224
+
225
+ describe Billing::Webhooks::Handlers::InvoicePaidHandler do
226
+ it "marks the invoice paid (status + paid_at) + publishes invoice.paid.billing" do
227
+ seed_account_on("cus_test_123")
228
+
229
+ event = event_for("invoice_paid")
230
+ captured = capture_published("invoice.paid.billing") do
231
+ described_class.new(event: event, gateway: gateway).call
232
+ end
233
+
234
+ invoice = Billing::Invoice.find_by(gateway_ref: "in_test_123")
235
+ expect(invoice.status).to eq("paid")
236
+ expect(invoice.paid_at).not_to be_nil
237
+ expect(invoice.amount_cents).to eq(1299)
238
+ expect(invoice.account_id).to eq(TEST_ACCOUNT_ID)
239
+ expect(captured.size).to eq(1)
240
+ end
241
+ end
242
+
243
+ describe Billing::Webhooks::Handlers::InvoicePaymentFailedHandler do
244
+ it "leaves the invoice in 'open' + publishes invoice.failed.billing" do
245
+ seed_account_on("cus_test_123")
246
+
247
+ event = event_for("invoice_payment_failed")
248
+ captured = capture_published("invoice.failed.billing") do
249
+ described_class.new(event: event, gateway: gateway).call
250
+ end
251
+
252
+ invoice = Billing::Invoice.find_by(gateway_ref: "in_test_123")
253
+ expect(invoice.status).to eq("open")
254
+ expect(invoice.paid_at).to be_nil
255
+ expect(captured.size).to eq(1)
256
+ end
257
+ end
258
+
259
+ describe Billing::Webhooks::Handlers::InvoiceFinalizedHandler do
260
+ it "promotes draft → open + publishes invoice.finalized.billing" do
261
+ create(:billing_invoice,
262
+ account_id: TEST_ACCOUNT_ID,
263
+ gateway_ref: "in_test_123",
264
+ status: "draft",
265
+ paid_at: nil)
266
+
267
+ event = event_for("invoice_finalized")
268
+ captured = capture_published("invoice.finalized.billing") do
269
+ described_class.new(event: event, gateway: gateway).call
270
+ end
271
+
272
+ invoice = Billing::Invoice.find_by(gateway_ref: "in_test_123")
273
+ expect(invoice.status).to eq("open")
274
+ expect(captured.size).to eq(1)
275
+ end
276
+ end
277
+
278
+ describe Billing::Webhooks::Handlers::InvoiceVoidedHandler do
279
+ it "flips status to void + publishes invoice.voided.billing" do
280
+ seed_account_on("cus_test_123")
281
+
282
+ event = event_for("invoice_voided")
283
+ captured = capture_published("invoice.voided.billing") do
284
+ described_class.new(event: event, gateway: gateway).call
285
+ end
286
+
287
+ invoice = Billing::Invoice.find_by(gateway_ref: "in_test_123")
288
+ expect(invoice.status).to eq("void")
289
+ expect(captured.size).to eq(1)
290
+ end
291
+ end
292
+
293
+ describe Billing::Webhooks::Handlers::PaymentSucceededHandler do
294
+ it "publishes payment.succeeded.billing without writing any local row" do
295
+ event = event_for("payment_intent_succeeded")
296
+ captured = capture_published("payment.succeeded.billing") do
297
+ described_class.new(event: event, gateway: gateway).call
298
+ end
299
+
300
+ # PaymentIntents are not modelled locally — see the handler comment.
301
+ expect(Billing::Subscription.count).to eq(0)
302
+ expect(Billing::Invoice.count).to eq(0)
303
+ expect(captured.size).to eq(1)
304
+ expect(captured.first[:object_id]).to eq("pi_test_123")
305
+ end
306
+ end
307
+
308
+ describe Billing::Webhooks::Handlers::PaymentFailedHandler do
309
+ it "publishes payment.failed.billing without writing any local row" do
310
+ event = event_for("payment_intent_payment_failed")
311
+ captured = capture_published("payment.failed.billing") do
312
+ described_class.new(event: event, gateway: gateway).call
313
+ end
314
+
315
+ expect(Billing::Subscription.count).to eq(0)
316
+ expect(Billing::Invoice.count).to eq(0)
317
+ expect(captured.size).to eq(1)
318
+ end
319
+ end
320
+
321
+ describe Billing::Webhooks::Handlers::ChargeRefundedHandler do
322
+ it "publishes charge.refunded.billing without writing any local row" do
323
+ event = event_for("charge_refunded")
324
+ captured = capture_published("charge.refunded.billing") do
325
+ described_class.new(event: event, gateway: gateway).call
326
+ end
327
+
328
+ expect(captured.size).to eq(1)
329
+ expect(captured.first[:object_id]).to eq("ch_test_123")
330
+ end
331
+ end
332
+
333
+ describe Billing::Webhooks::Handlers::CheckoutSessionCompletedHandler do
334
+ context "subscription mode (default fixture)" do
335
+ it "publishes checkout.session_completed.billing and does NOT touch LifetimePass" do
336
+ event = event_for("checkout_session_completed")
337
+ captured = capture_published("checkout.session_completed.billing") do
338
+ described_class.new(event: event, gateway: gateway).call
339
+ end
340
+
341
+ expect(captured.size).to eq(1)
342
+ expect(Billing::LifetimePass.count).to eq(0)
343
+ end
344
+ end
345
+
346
+ context "lifetime mode (mode=payment + metadata.access_type=lifetime)" do
347
+ it "forks to CreatePassFromCheckoutService which creates a LifetimePass + emits lifetime.purchased.billing" do
348
+ full = stripe_event_fixture("checkout_session_completed").deep_dup
349
+ full[:data][:object][:mode] = "payment"
350
+ full[:data][:object][:metadata] = {
351
+ access_type: "lifetime",
352
+ plan_ref: "price_test_lifetime_1",
353
+ account_id: TEST_ACCOUNT_ID
354
+ }
355
+ event = {
356
+ id: full[:id],
357
+ type: full[:type],
358
+ livemode: full[:livemode],
359
+ object: full[:data][:object],
360
+ raw: full.to_json
361
+ }
362
+
363
+ captured_lifetime = capture_published("lifetime.purchased.billing") do
364
+ captured_session = capture_published("checkout.session_completed.billing") do
365
+ described_class.new(event: event, gateway: gateway).call
366
+ end
367
+ # Lifetime path forks BEFORE publish — checkout.session_completed
368
+ # is intentionally not emitted on the LTD branch.
369
+ expect(captured_session).to be_empty
370
+ end
371
+
372
+ pass = Billing::LifetimePass.find_by(gateway_ref: "cs_test_123")
373
+ expect(pass).not_to be_nil
374
+ expect(pass.account_id).to eq(TEST_ACCOUNT_ID)
375
+ expect(pass.customer_ref).to eq("cus_test_123")
376
+ expect(pass.plan_ref).to eq("price_test_lifetime_1")
377
+ expect(captured_lifetime.size).to eq(1)
378
+ expect(captured_lifetime.first[:account_id]).to eq(TEST_ACCOUNT_ID)
379
+ end
380
+ end
381
+ end
382
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shared contract every Billing gateway adapter must satisfy. Use
4
+ # this when you write a new gateway (Paddle, Adyen, etc) so you know
5
+ # you have implemented the full Abstract interface that the rest of
6
+ # the engine depends on.
7
+ #
8
+ # Usage:
9
+ #
10
+ # require "rails_helper"
11
+ # require "support/shared_examples/a_billing_gateway"
12
+ #
13
+ # RSpec.describe Billing::Gateways::Paddle do
14
+ # it_behaves_like "a billing gateway"
15
+ # end
16
+ #
17
+ # The shared example only checks the *contract* (the methods exist
18
+ # and accept the documented arguments). Wiring-level behaviour —
19
+ # does Paddle actually charge people? — needs an integration test
20
+ # that hits Paddle's test mode.
21
+ RSpec.shared_examples "a billing gateway" do
22
+ let(:gateway) { described_class.new }
23
+
24
+ describe "#create_subscription" do
25
+ it "accepts customer_ref + plan_ref + arbitrary keyword extras" do
26
+ expect(gateway).to respond_to(:create_subscription)
27
+ method = gateway.method(:create_subscription)
28
+ keyword_names = method.parameters.select { |type, _| %i[key keyreq].include?(type) }.map(&:last)
29
+ expect(keyword_names).to include(:customer_ref, :plan_ref)
30
+ end
31
+ end
32
+
33
+ describe "#cancel_subscription" do
34
+ it "accepts subscription_ref + arbitrary keyword extras" do
35
+ expect(gateway).to respond_to(:cancel_subscription)
36
+ keyword_names = gateway.method(:cancel_subscription).parameters
37
+ .select { |type, _| %i[key keyreq].include?(type) }.map(&:last)
38
+ expect(keyword_names).to include(:subscription_ref)
39
+ end
40
+ end
41
+
42
+ describe "#fetch_subscription" do
43
+ it "accepts subscription_ref" do
44
+ expect(gateway).to respond_to(:fetch_subscription)
45
+ keyword_names = gateway.method(:fetch_subscription).parameters
46
+ .select { |type, _| %i[key keyreq].include?(type) }.map(&:last)
47
+ expect(keyword_names).to include(:subscription_ref)
48
+ end
49
+ end
50
+
51
+ describe "#create_checkout_session" do
52
+ it "accepts customer_ref + plan_ref + success_url + cancel_url" do
53
+ expect(gateway).to respond_to(:create_checkout_session)
54
+ keyword_names = gateway.method(:create_checkout_session).parameters
55
+ .select { |type, _| %i[key keyreq].include?(type) }.map(&:last)
56
+ %i[customer_ref plan_ref success_url cancel_url].each do |required|
57
+ expect(keyword_names).to include(required), "missing #{required.inspect}"
58
+ end
59
+ end
60
+ end
61
+
62
+ describe "#create_billing_portal_session" do
63
+ it "accepts customer_ref + return_url" do
64
+ expect(gateway).to respond_to(:create_billing_portal_session)
65
+ keyword_names = gateway.method(:create_billing_portal_session).parameters
66
+ .select { |type, _| %i[key keyreq].include?(type) }.map(&:last)
67
+ expect(keyword_names).to include(:customer_ref)
68
+ end
69
+ end
70
+
71
+ describe "#create_lifetime_checkout_session" do
72
+ it "accepts the same shape as #create_checkout_session (mode: payment under the hood)" do
73
+ expect(gateway).to respond_to(:create_lifetime_checkout_session)
74
+ keyword_names = gateway.method(:create_lifetime_checkout_session).parameters
75
+ .select { |type, _| %i[key keyreq].include?(type) }.map(&:last)
76
+ %i[customer_ref plan_ref success_url cancel_url].each do |required|
77
+ expect(keyword_names).to include(required), "missing #{required.inspect}"
78
+ end
79
+ end
80
+ end
81
+
82
+ describe "#verify_webhook" do
83
+ it "accepts payload + signature + secret and returns a normalised event hash" do
84
+ expect(gateway).to respond_to(:verify_webhook)
85
+ keyword_names = gateway.method(:verify_webhook).parameters
86
+ .select { |type, _| %i[key keyreq].include?(type) }.map(&:last)
87
+ %i[payload signature secret].each do |required|
88
+ expect(keyword_names).to include(required), "missing #{required.inspect}"
89
+ end
90
+ end
91
+
92
+ it "raises Billing::WebhookError on signature mismatch" do
93
+ skip "subclass must seed a verifier that rejects bad signatures" unless gateway.respond_to?(:verify_webhook)
94
+
95
+ expect {
96
+ gateway.verify_webhook(payload: "{}", signature: "bad", secret: "x")
97
+ }.to raise_error(Billing::WebhookError)
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Stub helpers for specs that talk to the Stripe REST API via
4
+ # Billing::Stripe::Client (the official `stripe` gem, which uses
5
+ # Net::HTTP under the hood). WebMock intercepts the request before
6
+ # it leaves the process — no live network. The dummy app's
7
+ # rails_helper.rb requires "webmock/rspec" automatically when the
8
+ # webmock gem is bundled, so individual specs only need:
9
+ #
10
+ # require_relative "support/stripe_helpers"
11
+ # RSpec.configure { |c| c.include StripeHelpers }
12
+ #
13
+ # Usage in a spec:
14
+ #
15
+ # stub_stripe(:post, "/v1/checkout/sessions", returns: {
16
+ # id: "cs_test_123", url: "https://stripe.test/cs_test_123"
17
+ # })
18
+ # result = Billing::Checkout::CreateSessionService.call(...)
19
+ # expect(result.value[:id]).to eq("cs_test_123")
20
+ #
21
+ # Or load a saved fixture:
22
+ #
23
+ # stub_stripe_fixture(:post, "/v1/customers", fixture: "customer_created")
24
+ module StripeHelpers
25
+ STRIPE_BASE = "https://api.stripe.com"
26
+
27
+ def stub_stripe(verb, path, returns:, status: 200)
28
+ WebMock.stub_request(verb, "#{STRIPE_BASE}#{path}")
29
+ .to_return(status: status,
30
+ headers: { "Content-Type" => "application/json" },
31
+ body: returns.to_json)
32
+ end
33
+
34
+ def stub_stripe_fixture(verb, path, fixture:, status: 200)
35
+ body = File.read(stripe_fixture_path(fixture))
36
+ WebMock.stub_request(verb, "#{STRIPE_BASE}#{path}")
37
+ .to_return(status: status,
38
+ headers: { "Content-Type" => "application/json" },
39
+ body: body)
40
+ end
41
+
42
+ def stub_stripe_failure(verb, path, status:, error_message: "Bad request")
43
+ body = { error: { message: error_message } }.to_json
44
+ WebMock.stub_request(verb, "#{STRIPE_BASE}#{path}")
45
+ .to_return(status: status,
46
+ headers: { "Content-Type" => "application/json" },
47
+ body: body)
48
+ end
49
+
50
+ def stripe_event_fixture(name)
51
+ JSON.parse(File.read(stripe_fixture_path(name)), symbolize_names: true)
52
+ end
53
+
54
+ private
55
+
56
+ def stripe_fixture_path(name)
57
+ File.expand_path("../fixtures/stripe/#{name}.json", __dir__)
58
+ end
59
+ end