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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +335 -0
- data/LICENSE +21 -0
- data/README.md +104 -0
- data/lib/generators/seams/accounts/accounts_generator.rb +272 -0
- data/lib/generators/seams/accounts/templates/README.md.tt +219 -0
- data/lib/generators/seams/accounts/templates/app/models/account.rb.tt +124 -0
- data/lib/generators/seams/accounts/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/accounts/templates/app/models/current.rb.tt +38 -0
- data/lib/generators/seams/accounts/templates/app/models/membership.rb.tt +114 -0
- data/lib/generators/seams/accounts/templates/config/routes.rb.tt +8 -0
- data/lib/generators/seams/accounts/templates/db/migrate/create_accounts.rb.tt +29 -0
- data/lib/generators/seams/accounts/templates/db/migrate/create_accounts_memberships.rb.tt +49 -0
- data/lib/generators/seams/accounts/templates/lib/accounts.rb.tt +21 -0
- data/lib/generators/seams/accounts/templates/lib/concerns/account_scoped.rb.tt +97 -0
- data/lib/generators/seams/accounts/templates/lib/concerns/authorization.rb.tt +105 -0
- data/lib/generators/seams/accounts/templates/lib/configuration.rb.tt +26 -0
- data/lib/generators/seams/accounts/templates/lib/engine.rb.tt +55 -0
- data/lib/generators/seams/accounts/templates/spec/factories/accounts.rb.tt +49 -0
- data/lib/generators/seams/accounts/templates/spec/models/accounts/account_spec.rb.tt +64 -0
- data/lib/generators/seams/accounts/templates/spec/models/accounts/membership_spec.rb.tt +99 -0
- data/lib/generators/seams/accounts/templates/spec/runtime/accounts_boot_spec.rb.tt +181 -0
- data/lib/generators/seams/admin/admin_generator.rb +852 -0
- data/lib/generators/seams/admin/templates/README.md.tt +266 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/accounts_controller.rb.tt +16 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/accounts_memberships_controller.rb.tt +16 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/application_controller.rb.tt +282 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/identities_controller.rb.tt +26 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/invitations_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/invoices_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/lifetime_passes_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/notification_preferences_controller.rb.tt +15 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/notifications_controller.rb.tt +18 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/plans_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/subscriptions_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/teams_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/teams_memberships_controller.rb.tt +16 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/account_dashboard.rb.tt +50 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/accounts_membership_dashboard.rb.tt +58 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/identity_dashboard.rb.tt +48 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/invitation_dashboard.rb.tt +51 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/invoice_dashboard.rb.tt +67 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/lifetime_pass_dashboard.rb.tt +65 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_dashboard.rb.tt +58 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_preference_dashboard.rb.tt +43 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/plan_dashboard.rb.tt +72 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/subscription_dashboard.rb.tt +59 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/team_dashboard.rb.tt +39 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/teams_membership_dashboard.rb.tt +43 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/account_policy.rb.tt +10 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/accounts_membership_policy.rb.tt +10 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/application_policy.rb.tt +85 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/identity_policy.rb.tt +18 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/invitation_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/invoice_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/lifetime_pass_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/notification_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/notification_preference_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/plan_policy.rb.tt +11 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/subscription_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/team_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/teams_membership_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/account_policy.rb.tt +33 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/accounts_membership_policy.rb.tt +24 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/application_policy.rb.tt +169 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/identity_policy.rb.tt +67 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invitation_policy.rb.tt +24 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invoice_policy.rb.tt +21 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/lifetime_pass_policy.rb.tt +21 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_policy.rb.tt +25 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_preference_policy.rb.tt +23 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/plan_policy.rb.tt +47 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/subscription_policy.rb.tt +22 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/team_policy.rb.tt +28 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/teams_membership_policy.rb.tt +24 -0
- data/lib/generators/seams/admin/templates/config/routes.rb.tt +38 -0
- data/lib/generators/seams/admin/templates/lib/admin.rb.tt +36 -0
- data/lib/generators/seams/admin/templates/lib/concerns/authenticator.rb.tt +66 -0
- data/lib/generators/seams/admin/templates/lib/configuration.rb.tt +90 -0
- data/lib/generators/seams/admin/templates/lib/context.rb.tt +44 -0
- data/lib/generators/seams/admin/templates/lib/engine.rb.tt +68 -0
- data/lib/generators/seams/admin/templates/spec/factories/admin.rb.tt +10 -0
- data/lib/generators/seams/admin/templates/spec/runtime/admin_boot_spec.rb.tt +604 -0
- data/lib/generators/seams/auth/add_oauth_provider/add_oauth_provider_generator.rb +157 -0
- data/lib/generators/seams/auth/add_oauth_provider/templates/adapter.rb.tt +95 -0
- data/lib/generators/seams/auth/add_oauth_provider/templates/adapter_spec.rb.tt +58 -0
- data/lib/generators/seams/auth/auth_generator.rb +311 -0
- data/lib/generators/seams/auth/templates/README.md.tt +289 -0
- data/lib/generators/seams/auth/templates/app/controllers/oauth/callbacks_controller.rb.tt +80 -0
- data/lib/generators/seams/auth/templates/app/controllers/password_resets_controller.rb.tt +44 -0
- data/lib/generators/seams/auth/templates/app/controllers/registrations_controller.rb.tt +34 -0
- data/lib/generators/seams/auth/templates/app/controllers/sessions_controller.rb.tt +49 -0
- data/lib/generators/seams/auth/templates/app/jobs/application_job.rb.tt +7 -0
- data/lib/generators/seams/auth/templates/app/jobs/cleanup_expired_sessions_job.rb.tt +30 -0
- data/lib/generators/seams/auth/templates/app/mailers/passwords_mailer.rb.tt +15 -0
- data/lib/generators/seams/auth/templates/app/models/api_token.rb.tt +62 -0
- data/lib/generators/seams/auth/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/auth/templates/app/models/current.rb.tt +15 -0
- data/lib/generators/seams/auth/templates/app/models/identity.rb.tt +74 -0
- data/lib/generators/seams/auth/templates/app/models/oauth/provider.rb.tt +48 -0
- data/lib/generators/seams/auth/templates/app/models/session.rb.tt +28 -0
- data/lib/generators/seams/auth/templates/app/services/authenticate_identity.rb.tt +31 -0
- data/lib/generators/seams/auth/templates/app/services/generate_api_token.rb.tt +35 -0
- data/lib/generators/seams/auth/templates/app/services/oauth/authenticator.rb.tt +94 -0
- data/lib/generators/seams/auth/templates/app/services/register_identity.rb.tt +57 -0
- data/lib/generators/seams/auth/templates/app/services/reset_password.rb.tt +41 -0
- data/lib/generators/seams/auth/templates/app/services/revoke_api_token.rb.tt +38 -0
- data/lib/generators/seams/auth/templates/app/views/password_resets/edit.html.erb.tt +12 -0
- data/lib/generators/seams/auth/templates/app/views/password_resets/new.html.erb.tt +11 -0
- data/lib/generators/seams/auth/templates/app/views/passwords_mailer/reset_email.html.erb.tt +7 -0
- data/lib/generators/seams/auth/templates/app/views/registrations/new.html.erb.tt +26 -0
- data/lib/generators/seams/auth/templates/app/views/sessions/_oauth_buttons.html.erb.tt +18 -0
- data/lib/generators/seams/auth/templates/app/views/sessions/new.html.erb.tt +17 -0
- data/lib/generators/seams/auth/templates/config/routes.rb.tt +21 -0
- data/lib/generators/seams/auth/templates/db/migrate/create_auth_api_tokens.rb.tt +26 -0
- data/lib/generators/seams/auth/templates/db/migrate/create_auth_identities.rb.tt +29 -0
- data/lib/generators/seams/auth/templates/db/migrate/create_auth_oauth_providers.rb.tt +35 -0
- data/lib/generators/seams/auth/templates/db/migrate/create_auth_sessions.rb.tt +19 -0
- data/lib/generators/seams/auth/templates/lib/auth.rb.tt +39 -0
- data/lib/generators/seams/auth/templates/lib/concerns/api_authenticatable.rb.tt +58 -0
- data/lib/generators/seams/auth/templates/lib/concerns/authenticatable.rb.tt +32 -0
- data/lib/generators/seams/auth/templates/lib/concerns/authentication.rb.tt +60 -0
- data/lib/generators/seams/auth/templates/lib/configuration.rb.tt +45 -0
- data/lib/generators/seams/auth/templates/lib/engine.rb.tt +46 -0
- data/lib/generators/seams/auth/templates/lib/oauth/abstract.rb.tt +87 -0
- data/lib/generators/seams/auth/templates/lib/oauth/github.rb.tt +112 -0
- data/lib/generators/seams/auth/templates/lib/oauth/google.rb.tt +78 -0
- data/lib/generators/seams/auth/templates/lib/tasks/auth_pii.rake.tt +68 -0
- data/lib/generators/seams/auth/templates/spec/factories/auth.rb.tt +38 -0
- data/lib/generators/seams/auth/templates/spec/mailers/passwords_mailer_spec.rb.tt +37 -0
- data/lib/generators/seams/auth/templates/spec/models/api_token_spec.rb.tt +84 -0
- data/lib/generators/seams/auth/templates/spec/models/identity_spec.rb.tt +56 -0
- data/lib/generators/seams/auth/templates/spec/models/oauth/provider_spec.rb.tt +64 -0
- data/lib/generators/seams/auth/templates/spec/models/session_spec.rb.tt +34 -0
- data/lib/generators/seams/auth/templates/spec/runtime/boot_spec.rb.tt +30 -0
- data/lib/generators/seams/auth/templates/spec/runtime/event_payload_spec.rb.tt +29 -0
- data/lib/generators/seams/auth/templates/spec/runtime/login_flow_spec.rb.tt +45 -0
- data/lib/generators/seams/billing/billing_generator.rb +476 -0
- data/lib/generators/seams/billing/templates/README.md.tt +355 -0
- data/lib/generators/seams/billing/templates/app/controllers/admin/lifetime_passes_controller.rb.tt +84 -0
- data/lib/generators/seams/billing/templates/app/controllers/checkout_controller.rb.tt +92 -0
- data/lib/generators/seams/billing/templates/app/controllers/invoices_controller.rb.tt +63 -0
- data/lib/generators/seams/billing/templates/app/controllers/plans_controller.rb.tt +14 -0
- data/lib/generators/seams/billing/templates/app/controllers/portal_controller.rb.tt +45 -0
- data/lib/generators/seams/billing/templates/app/controllers/subscriptions_controller.rb.tt +119 -0
- data/lib/generators/seams/billing/templates/app/controllers/webhooks_controller.rb.tt +98 -0
- data/lib/generators/seams/billing/templates/app/helpers/currency_helper.rb.tt +44 -0
- data/lib/generators/seams/billing/templates/app/jobs/application_job.rb.tt +6 -0
- data/lib/generators/seams/billing/templates/app/jobs/cancel_subscription_job.rb.tt +39 -0
- data/lib/generators/seams/billing/templates/app/jobs/start_subscription_job.rb.tt +32 -0
- data/lib/generators/seams/billing/templates/app/jobs/webhooks/process_event_job.rb.tt +37 -0
- data/lib/generators/seams/billing/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/billing/templates/app/models/invoice.rb.tt +35 -0
- data/lib/generators/seams/billing/templates/app/models/lifetime_pass.rb.tt +60 -0
- data/lib/generators/seams/billing/templates/app/models/plan.rb.tt +95 -0
- data/lib/generators/seams/billing/templates/app/models/subscription.rb.tt +31 -0
- data/lib/generators/seams/billing/templates/app/models/webhook_event.rb.tt +13 -0
- data/lib/generators/seams/billing/templates/app/services/checkout_session_service.rb.tt +25 -0
- data/lib/generators/seams/billing/templates/app/services/customers/find_or_create_service.rb.tt +73 -0
- data/lib/generators/seams/billing/templates/app/services/invoices/sync_service.rb.tt +50 -0
- data/lib/generators/seams/billing/templates/app/services/lifetime/create_lifetime_session_service.rb.tt +82 -0
- data/lib/generators/seams/billing/templates/app/services/lifetime/create_pass_from_checkout_service.rb.tt +88 -0
- data/lib/generators/seams/billing/templates/app/services/lifetime/grant_pass_service.rb.tt +80 -0
- data/lib/generators/seams/billing/templates/app/services/lifetime/revoke_pass_service.rb.tt +59 -0
- data/lib/generators/seams/billing/templates/app/services/portal_session_service.rb.tt +23 -0
- data/lib/generators/seams/billing/templates/app/services/service_result.rb.tt +38 -0
- data/lib/generators/seams/billing/templates/app/services/stripe_service.rb.tt +67 -0
- data/lib/generators/seams/billing/templates/app/services/subscriptions/cancel_service.rb.tt +42 -0
- data/lib/generators/seams/billing/templates/app/services/subscriptions/change_plan_service.rb.tt +48 -0
- data/lib/generators/seams/billing/templates/app/services/subscriptions/reactivate_service.rb.tt +28 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/event_router.rb.tt +54 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handler.rb.tt +93 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/charge_refunded_handler.rb.tt +18 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/checkout_session_completed_handler.rb.tt +58 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_created_handler.rb.tt +16 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_finalized_handler.rb.tt +14 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_handler_base.rb.tt +80 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_paid_handler.rb.tt +12 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_payment_failed_handler.rb.tt +12 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_voided_handler.rb.tt +12 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/payment_failed_handler.rb.tt +15 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/payment_succeeded_handler.rb.tt +19 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_created_handler.rb.tt +11 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_deleted_handler.rb.tt +15 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_handler_base.rb.tt +92 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_trial_will_end_handler.rb.tt +15 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_updated_handler.rb.tt +11 -0
- data/lib/generators/seams/billing/templates/app/views/admin/lifetime_passes/index.html.erb.tt +36 -0
- data/lib/generators/seams/billing/templates/app/views/admin/lifetime_passes/new.html.erb.tt +37 -0
- data/lib/generators/seams/billing/templates/app/views/checkout/success.html.erb.tt +5 -0
- data/lib/generators/seams/billing/templates/app/views/invoices/index.html.erb.tt +22 -0
- data/lib/generators/seams/billing/templates/app/views/invoices/show.html.erb.tt +14 -0
- data/lib/generators/seams/billing/templates/app/views/plans/index.html.erb.tt +51 -0
- data/lib/generators/seams/billing/templates/app/views/subscriptions/index.html.erb.tt +16 -0
- data/lib/generators/seams/billing/templates/app/views/subscriptions/show.html.erb.tt +25 -0
- data/lib/generators/seams/billing/templates/config/routes.rb.tt +39 -0
- data/lib/generators/seams/billing/templates/db/migrate/create_billing_invoices.rb.tt +32 -0
- data/lib/generators/seams/billing/templates/db/migrate/create_billing_lifetime_passes.rb.tt +43 -0
- data/lib/generators/seams/billing/templates/db/migrate/create_billing_plans.rb.tt +31 -0
- data/lib/generators/seams/billing/templates/db/migrate/create_billing_subscriptions.rb.tt +33 -0
- data/lib/generators/seams/billing/templates/db/migrate/create_billing_webhook_events.rb.tt +24 -0
- data/lib/generators/seams/billing/templates/lib/billing.rb.tt +34 -0
- data/lib/generators/seams/billing/templates/lib/concerns/billable.rb.tt +100 -0
- data/lib/generators/seams/billing/templates/lib/configuration.rb.tt +52 -0
- data/lib/generators/seams/billing/templates/lib/engine.rb.tt +72 -0
- data/lib/generators/seams/billing/templates/lib/gateways/abstract.rb.tt +65 -0
- data/lib/generators/seams/billing/templates/lib/gateways/adyen.rb.tt +16 -0
- data/lib/generators/seams/billing/templates/lib/gateways/paddle.rb.tt +22 -0
- data/lib/generators/seams/billing/templates/lib/gateways/stripe.rb.tt +155 -0
- data/lib/generators/seams/billing/templates/lib/stripe/client.rb.tt +101 -0
- data/lib/generators/seams/billing/templates/lib/stripe/webhook_signature.rb.tt +43 -0
- data/lib/generators/seams/billing/templates/lib/tasks/billing_check.rake.tt +34 -0
- data/lib/generators/seams/billing/templates/spec/factories/billing.rb.tt +65 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/charge_refunded.json.tt +19 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/checkout_session_completed.json.tt +17 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_created.json.tt +25 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_deleted.json.tt +17 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_trial_will_end.json.tt +17 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_updated.json.tt +28 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_created.json.tt +18 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_finalized.json.tt +18 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_paid.json.tt +19 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_payment_failed.json.tt +20 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_voided.json.tt +18 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_payment_failed.json.tt +21 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_succeeded.json.tt +17 -0
- data/lib/generators/seams/billing/templates/spec/gateways/contract_spec.rb.tt +11 -0
- data/lib/generators/seams/billing/templates/spec/gateways/stripe_spec.rb.tt +53 -0
- data/lib/generators/seams/billing/templates/spec/models/plan_spec.rb.tt +81 -0
- data/lib/generators/seams/billing/templates/spec/models/subscription_spec.rb.tt +43 -0
- data/lib/generators/seams/billing/templates/spec/runtime/boot_spec.rb.tt +38 -0
- data/lib/generators/seams/billing/templates/spec/runtime/webhook_handlers_spec.rb.tt +382 -0
- data/lib/generators/seams/billing/templates/spec/support/shared_examples/a_billing_gateway.rb.tt +100 -0
- data/lib/generators/seams/billing/templates/spec/support/stripe_helpers.rb.tt +59 -0
- data/lib/generators/seams/core/core_generator.rb +191 -0
- data/lib/generators/seams/core/templates/README.md.tt +45 -0
- data/lib/generators/seams/core/templates/app/controllers/concerns/has_current_attributes.rb.tt +71 -0
- data/lib/generators/seams/core/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/core/templates/app/models/audit_log.rb.tt +19 -0
- data/lib/generators/seams/core/templates/app/models/concerns/auditable.rb.tt +64 -0
- data/lib/generators/seams/core/templates/app/models/concerns/sluggable.rb.tt +53 -0
- data/lib/generators/seams/core/templates/app/models/concerns/soft_deletable.rb.tt +37 -0
- data/lib/generators/seams/core/templates/app/models/concerns/tenant_scoped.rb.tt +39 -0
- data/lib/generators/seams/core/templates/app/models/current.rb.tt +16 -0
- data/lib/generators/seams/core/templates/app/services/event_publisher.rb.tt +23 -0
- data/lib/generators/seams/core/templates/app/validators/email_format_validator.rb.tt +21 -0
- data/lib/generators/seams/core/templates/db/migrate/create_core_audit_logs.rb.tt +29 -0
- data/lib/generators/seams/core/templates/lib/core.rb.tt +8 -0
- data/lib/generators/seams/core/templates/lib/engine.rb.tt +28 -0
- data/lib/generators/seams/core/templates/spec/concerns/auditable_spec.rb.tt +39 -0
- data/lib/generators/seams/core/templates/spec/concerns/sluggable_spec.rb.tt +29 -0
- data/lib/generators/seams/core/templates/spec/models/audit_log_spec.rb.tt +22 -0
- data/lib/generators/seams/core/templates/spec/runtime/boot_spec.rb.tt +25 -0
- data/lib/generators/seams/core/templates/spec/validators/email_format_validator_spec.rb.tt +29 -0
- data/lib/generators/seams/engine/engine_generator.rb +165 -0
- data/lib/generators/seams/engine/templates/Gemfile.tt +19 -0
- data/lib/generators/seams/engine/templates/LICENSE.tt +21 -0
- data/lib/generators/seams/engine/templates/README.md.tt +40 -0
- data/lib/generators/seams/engine/templates/Rakefile.tt +14 -0
- data/lib/generators/seams/engine/templates/app/application_controller.rb.tt +6 -0
- data/lib/generators/seams/engine/templates/app/application_record.rb.tt +16 -0
- data/lib/generators/seams/engine/templates/config/locales/en.yml.tt +14 -0
- data/lib/generators/seams/engine/templates/config/routes.rb.tt +4 -0
- data/lib/generators/seams/engine/templates/gemspec.tt +20 -0
- data/lib/generators/seams/engine/templates/host_initializer.rb.tt +13 -0
- data/lib/generators/seams/engine/templates/lib/engine.rb.tt +27 -0
- data/lib/generators/seams/engine/templates/lib/root.rb.tt +7 -0
- data/lib/generators/seams/engine/templates/lib/version.rb.tt +5 -0
- data/lib/generators/seams/engine/templates/rubocop.yml.tt +55 -0
- data/lib/generators/seams/engine/templates/spec/example_spec.rb.tt +16 -0
- data/lib/generators/seams/engine/templates/spec/spec_helper.rb.tt +23 -0
- data/lib/generators/seams/install/install_generator.rb +211 -0
- data/lib/generators/seams/install/templates/Dockerfile.tt +52 -0
- data/lib/generators/seams/install/templates/Procfile.tt +14 -0
- data/lib/generators/seams/install/templates/bin_seams.tt +107 -0
- data/lib/generators/seams/install/templates/ci.yml.tt +123 -0
- data/lib/generators/seams/install/templates/deploy.yml.tt +63 -0
- data/lib/generators/seams/install/templates/doc/ARCHITECTURE.md.tt +86 -0
- data/lib/generators/seams/install/templates/docker-entrypoint.tt +27 -0
- data/lib/generators/seams/install/templates/rubocop.yml.tt +33 -0
- data/lib/generators/seams/install/templates/ruby-version.tt +1 -0
- data/lib/generators/seams/install/templates/script/collate_coverage.rb.tt +33 -0
- data/lib/generators/seams/install/templates/script/run_affected_tests.sh.tt +64 -0
- data/lib/generators/seams/install/templates/seams.rake.tt +65 -0
- data/lib/generators/seams/install/templates/seams.rb.tt +9 -0
- data/lib/generators/seams/install/templates/seams_engines.rb.tt +15 -0
- data/lib/generators/seams/notifications/notifications_generator.rb +395 -0
- data/lib/generators/seams/notifications/templates/README.md.tt +269 -0
- data/lib/generators/seams/notifications/templates/app/channels/notification_channel.rb.tt +36 -0
- data/lib/generators/seams/notifications/templates/app/controllers/notifications_controller.rb.tt +58 -0
- data/lib/generators/seams/notifications/templates/app/controllers/preferences_controller.rb.tt +54 -0
- data/lib/generators/seams/notifications/templates/app/javascript/controllers/notification_bell_controller.js.tt +34 -0
- data/lib/generators/seams/notifications/templates/app/jobs/application_job.rb.tt +6 -0
- data/lib/generators/seams/notifications/templates/app/jobs/create_notification_job.rb.tt +31 -0
- data/lib/generators/seams/notifications/templates/app/jobs/send_due_notifications_job.rb.tt +22 -0
- data/lib/generators/seams/notifications/templates/app/jobs/send_notification_job.rb.tt +13 -0
- data/lib/generators/seams/notifications/templates/app/mailers/application_mailer.rb.tt +12 -0
- data/lib/generators/seams/notifications/templates/app/mailers/notification_mailer.rb.tt +23 -0
- data/lib/generators/seams/notifications/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/notifications/templates/app/models/delivery.rb.tt +13 -0
- data/lib/generators/seams/notifications/templates/app/models/notification.rb.tt +218 -0
- data/lib/generators/seams/notifications/templates/app/models/notification_preference.rb.tt +29 -0
- data/lib/generators/seams/notifications/templates/app/models/strategies/email.rb.tt +38 -0
- data/lib/generators/seams/notifications/templates/app/models/strategies/in_app.rb.tt +26 -0
- data/lib/generators/seams/notifications/templates/app/models/strategies/sms.rb.tt +33 -0
- data/lib/generators/seams/notifications/templates/app/subscribers/auth_subscriber.rb.tt +71 -0
- data/lib/generators/seams/notifications/templates/app/subscribers/billing_subscriber.rb.tt +127 -0
- data/lib/generators/seams/notifications/templates/app/views/layouts/notifications/mailer.html.erb.tt +22 -0
- data/lib/generators/seams/notifications/templates/app/views/layouts/notifications/mailer.text.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/notifications/_bell.html.erb.tt +15 -0
- data/lib/generators/seams/notifications/templates/app/views/notifications/index.html.erb.tt +15 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_failed.html.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_failed.text.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_paid.html.erb.tt +3 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_paid.text.erb.tt +3 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_granted.html.erb.tt +5 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_granted.text.erb.tt +5 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_purchased.html.erb.tt +5 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_purchased.text.erb.tt +5 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_canceled.html.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_canceled.text.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_started.html.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_started.text.erb.tt +5 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_updated.html.erb.tt +3 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_updated.text.erb.tt +3 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/default.html.erb.tt +10 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/default.text.erb.tt +11 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/welcome.html.erb.tt +6 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/welcome.text.erb.tt +6 -0
- data/lib/generators/seams/notifications/templates/config/initializers/notifications.rb.tt +58 -0
- data/lib/generators/seams/notifications/templates/config/routes.rb.tt +17 -0
- data/lib/generators/seams/notifications/templates/db/migrate/create_notification_deliveries.rb.tt +16 -0
- data/lib/generators/seams/notifications/templates/db/migrate/create_notification_preferences.rb.tt +25 -0
- data/lib/generators/seams/notifications/templates/db/migrate/create_notifications.rb.tt +35 -0
- data/lib/generators/seams/notifications/templates/lib/adapters/abstract.rb.tt +20 -0
- data/lib/generators/seams/notifications/templates/lib/adapters/action_mailer.rb.tt +17 -0
- data/lib/generators/seams/notifications/templates/lib/adapters/null_sms.rb.tt +23 -0
- data/lib/generators/seams/notifications/templates/lib/concerns/notifiable.rb.tt +135 -0
- data/lib/generators/seams/notifications/templates/lib/configuration.rb.tt +24 -0
- data/lib/generators/seams/notifications/templates/lib/engine.rb.tt +35 -0
- data/lib/generators/seams/notifications/templates/lib/notifications.rb.tt +75 -0
- data/lib/generators/seams/notifications/templates/lib/type_registry.rb.tt +74 -0
- data/lib/generators/seams/notifications/templates/spec/factories/notifications.rb.tt +53 -0
- data/lib/generators/seams/notifications/templates/spec/models/delivery_spec.rb.tt +28 -0
- data/lib/generators/seams/notifications/templates/spec/models/notification_preference_spec.rb.tt +46 -0
- data/lib/generators/seams/notifications/templates/spec/models/notification_spec.rb.tt +60 -0
- data/lib/generators/seams/notifications/templates/spec/runtime/bell_broadcast_spec.rb.tt +59 -0
- data/lib/generators/seams/notifications/templates/spec/runtime/billing_subscriber_skip_spec.rb.tt +87 -0
- data/lib/generators/seams/notifications/templates/spec/runtime/boot_spec.rb.tt +39 -0
- data/lib/generators/seams/notifications/templates/spec/runtime/schedule_round_trip_spec.rb.tt +55 -0
- data/lib/generators/seams/remove/remove_generator.rb +259 -0
- data/lib/generators/seams/teams/teams_generator.rb +298 -0
- data/lib/generators/seams/teams/templates/README.md.tt +88 -0
- data/lib/generators/seams/teams/templates/app/controllers/invitations_controller.rb.tt +102 -0
- data/lib/generators/seams/teams/templates/app/controllers/memberships_controller.rb.tt +54 -0
- data/lib/generators/seams/teams/templates/app/controllers/teams_controller.rb.tt +68 -0
- data/lib/generators/seams/teams/templates/app/jobs/application_job.rb.tt +6 -0
- data/lib/generators/seams/teams/templates/app/mailers/invitation_mailer.rb.tt +34 -0
- data/lib/generators/seams/teams/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/teams/templates/app/models/current.rb.tt +30 -0
- data/lib/generators/seams/teams/templates/app/models/invitation.rb.tt +36 -0
- data/lib/generators/seams/teams/templates/app/models/membership.rb.tt +36 -0
- data/lib/generators/seams/teams/templates/app/models/team.rb.tt +32 -0
- data/lib/generators/seams/teams/templates/app/subscribers/invitation_subscriber.rb.tt +36 -0
- data/lib/generators/seams/teams/templates/app/views/invitation_mailer/invite.text.erb.tt +8 -0
- data/lib/generators/seams/teams/templates/app/views/invitations/index.html.erb.tt +44 -0
- data/lib/generators/seams/teams/templates/app/views/memberships/index.html.erb.tt +32 -0
- data/lib/generators/seams/teams/templates/app/views/teams/edit.html.erb.tt +28 -0
- data/lib/generators/seams/teams/templates/app/views/teams/index.html.erb.tt +15 -0
- data/lib/generators/seams/teams/templates/app/views/teams/new.html.erb.tt +24 -0
- data/lib/generators/seams/teams/templates/app/views/teams/show.html.erb.tt +17 -0
- data/lib/generators/seams/teams/templates/config/routes.rb.tt +19 -0
- data/lib/generators/seams/teams/templates/db/migrate/create_team_invitations.rb.tt +24 -0
- data/lib/generators/seams/teams/templates/db/migrate/create_team_memberships.rb.tt +25 -0
- data/lib/generators/seams/teams/templates/db/migrate/create_teams.rb.tt +18 -0
- data/lib/generators/seams/teams/templates/lib/concerns/account_scoped.rb.tt +79 -0
- data/lib/generators/seams/teams/templates/lib/concerns/authorization.rb.tt +55 -0
- data/lib/generators/seams/teams/templates/lib/configuration.rb.tt +45 -0
- data/lib/generators/seams/teams/templates/lib/engine.rb.tt +51 -0
- data/lib/generators/seams/teams/templates/lib/teams.rb.tt +22 -0
- data/lib/generators/seams/teams/templates/spec/factories/teams.rb.tt +47 -0
- data/lib/generators/seams/teams/templates/spec/models/invitation_spec.rb.tt +25 -0
- data/lib/generators/seams/teams/templates/spec/models/membership_spec.rb.tt +29 -0
- data/lib/generators/seams/teams/templates/spec/models/team_spec.rb.tt +23 -0
- data/lib/generators/seams/teams/templates/spec/runtime/boot_spec.rb.tt +32 -0
- data/lib/seams/cli/list.rb +111 -0
- data/lib/seams/cli/quality.rb +99 -0
- data/lib/seams/cli/resolve.rb +276 -0
- data/lib/seams/cli/test_changed.rb +116 -0
- data/lib/seams/cli.rb +48 -0
- data/lib/seams/configuration.rb +19 -0
- data/lib/seams/cops/known_queue_names.rb +42 -0
- data/lib/seams/cops/migration_comments.rb +68 -0
- data/lib/seams/cops/no_cross_engine_dependency.rb +58 -0
- data/lib/seams/cops/no_cross_engine_model_access.rb +153 -0
- data/lib/seams/cops.rb +18 -0
- data/lib/seams/event_registry.rb +49 -0
- data/lib/seams/events/adapter.rb +24 -0
- data/lib/seams/events/adapters/active_support.rb +31 -0
- data/lib/seams/events/publisher.rb +178 -0
- data/lib/seams/events.rb +39 -0
- data/lib/seams/generators/dummy_app_writer.rb +424 -0
- data/lib/seams/generators/eject_aware.rb +102 -0
- data/lib/seams/generators/follow_up_generator.rb +148 -0
- data/lib/seams/generators/host_injector.rb +124 -0
- data/lib/seams/generators/sibling_rubocop_writer.rb +77 -0
- data/lib/seams/generators/splicer.rb +217 -0
- data/lib/seams/observability/adapter.rb +33 -0
- data/lib/seams/observability/adapters/rails_logger.rb +59 -0
- data/lib/seams/observability.rb +34 -0
- data/lib/seams/runtime.rb +23 -0
- data/lib/seams/version.rb +5 -0
- data/lib/seams.rb +23 -0
- metadata +493 -0
data/lib/generators/seams/billing/templates/app/services/subscriptions/reactivate_service.rb.tt
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Billing
|
|
4
|
+
module Subscriptions
|
|
5
|
+
# Un-cancels a subscription that's pending cancellation at period
|
|
6
|
+
# end. No-op for subscriptions already past their period end —
|
|
7
|
+
# those need a fresh checkout flow, not a reactivate. Verified
|
|
8
|
+
# against
|
|
9
|
+
# https://docs.stripe.com/billing/subscriptions/cancel#reactivate-canceled-subscription.
|
|
10
|
+
class ReactivateService < Billing::StripeService
|
|
11
|
+
def initialize(subscription_ref:)
|
|
12
|
+
@subscription_ref = subscription_ref
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call_stripe(client)
|
|
16
|
+
client.update_subscription(@subscription_ref, cancel_at_period_end: false)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def on_success(stripe_response)
|
|
20
|
+
ServiceResult.ok(value: {
|
|
21
|
+
id: stripe_response[:id],
|
|
22
|
+
status: stripe_response[:status],
|
|
23
|
+
cancel_at_period_end: stripe_response[:cancel_at_period_end]
|
|
24
|
+
})
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Billing
|
|
4
|
+
module Webhooks
|
|
5
|
+
# Maps a Stripe event type → the handler class that processes it.
|
|
6
|
+
# Hosts extend the map by reopening this module in an initializer:
|
|
7
|
+
#
|
|
8
|
+
# Billing::Webhooks::EventRouter.register(
|
|
9
|
+
# "customer.tax_id.created",
|
|
10
|
+
# "MyHost::TaxIdCreatedHandler"
|
|
11
|
+
# )
|
|
12
|
+
#
|
|
13
|
+
# Returns nil for unmapped events so the controller can no-op
|
|
14
|
+
# without raising — Stripe sends events the host hasn't subscribed
|
|
15
|
+
# to and that's normal.
|
|
16
|
+
module EventRouter
|
|
17
|
+
HANDLERS = {
|
|
18
|
+
"customer.subscription.created" => "Billing::Webhooks::Handlers::SubscriptionCreatedHandler",
|
|
19
|
+
"customer.subscription.updated" => "Billing::Webhooks::Handlers::SubscriptionUpdatedHandler",
|
|
20
|
+
"customer.subscription.deleted" => "Billing::Webhooks::Handlers::SubscriptionDeletedHandler",
|
|
21
|
+
"customer.subscription.trial_will_end" => "Billing::Webhooks::Handlers::SubscriptionTrialWillEndHandler",
|
|
22
|
+
"invoice.created" => "Billing::Webhooks::Handlers::InvoiceCreatedHandler",
|
|
23
|
+
"invoice.paid" => "Billing::Webhooks::Handlers::InvoicePaidHandler",
|
|
24
|
+
"invoice.payment_failed" => "Billing::Webhooks::Handlers::InvoicePaymentFailedHandler",
|
|
25
|
+
"invoice.finalized" => "Billing::Webhooks::Handlers::InvoiceFinalizedHandler",
|
|
26
|
+
"invoice.voided" => "Billing::Webhooks::Handlers::InvoiceVoidedHandler",
|
|
27
|
+
"payment_intent.succeeded" => "Billing::Webhooks::Handlers::PaymentSucceededHandler",
|
|
28
|
+
"payment_intent.payment_failed" => "Billing::Webhooks::Handlers::PaymentFailedHandler",
|
|
29
|
+
"charge.refunded" => "Billing::Webhooks::Handlers::ChargeRefundedHandler",
|
|
30
|
+
"checkout.session.completed" => "Billing::Webhooks::Handlers::CheckoutSessionCompletedHandler",
|
|
31
|
+
"checkout.session.async_payment_succeeded" => "Billing::Webhooks::Handlers::CheckoutSessionCompletedHandler",
|
|
32
|
+
# Follow-up generators that ship handlers for additional Stripe events (customer.tax_id.created, payment_method.attached) splice their `"event.type" => "Billing::Webhooks::Handlers::ClassName",` entry here. Trailing commas on every entry keep the hash valid both before and after a splice.
|
|
33
|
+
# seams:insertion-point billing.event_router.handlers
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
def register(stripe_event_type, handler_class_name)
|
|
39
|
+
HANDLERS[stripe_event_type] = handler_class_name
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def handler_for(stripe_event_type)
|
|
43
|
+
klass_name = HANDLERS[stripe_event_type]
|
|
44
|
+
return nil unless klass_name
|
|
45
|
+
|
|
46
|
+
klass_name.constantize
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def supported_event_types
|
|
50
|
+
HANDLERS.keys
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Billing
|
|
4
|
+
module Webhooks
|
|
5
|
+
# Base class for every webhook handler. Subclasses implement
|
|
6
|
+
# #call which receives a normalised event hash:
|
|
7
|
+
#
|
|
8
|
+
# { id:, type:, livemode:, object:, raw: }
|
|
9
|
+
#
|
|
10
|
+
# And typically:
|
|
11
|
+
# 1. Upserts the local Billing::Subscription / Billing::Invoice
|
|
12
|
+
# so downstream subscribers can read from the DB rather than
|
|
13
|
+
# reparsing the raw payload.
|
|
14
|
+
# 2. Calls #publish with the canonical seams event name + the
|
|
15
|
+
# shared payload shape.
|
|
16
|
+
#
|
|
17
|
+
# Subclasses MUST set SEAMS_EVENT to the canonical event name
|
|
18
|
+
# (e.g. "subscription.created.billing"). EventRouter routes by the
|
|
19
|
+
# Stripe event type; SEAMS_EVENT is what we re-publish.
|
|
20
|
+
class Handler
|
|
21
|
+
SEAMS_EVENT = nil # override in subclasses
|
|
22
|
+
|
|
23
|
+
def initialize(event:, gateway:)
|
|
24
|
+
@event = event
|
|
25
|
+
@gateway = gateway
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def call
|
|
29
|
+
raise NotImplementedError, "#{self.class} must implement #call"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
protected
|
|
33
|
+
|
|
34
|
+
attr_reader :event, :gateway
|
|
35
|
+
|
|
36
|
+
def object_hash
|
|
37
|
+
@object_hash ||= begin
|
|
38
|
+
obj = event[:object]
|
|
39
|
+
obj.respond_to?(:to_hash) ? obj.to_hash : obj
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def object_id
|
|
44
|
+
@object_id ||= begin
|
|
45
|
+
obj = event[:object]
|
|
46
|
+
return nil if obj.nil?
|
|
47
|
+
|
|
48
|
+
obj.respond_to?(:id) ? obj.id : (obj.is_a?(Hash) && (obj[:id] || obj["id"]))
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def customer_ref
|
|
53
|
+
@customer_ref ||= object_hash.is_a?(Hash) && (object_hash[:customer] || object_hash["customer"])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Resolves the Accounts::Account id for this webhook event by
|
|
57
|
+
# cross-referencing the gateway's customer id (`cus_*`) against
|
|
58
|
+
# the local Billing::Subscription mirror. Returns nil if no
|
|
59
|
+
# matching local row exists yet (e.g. the very first
|
|
60
|
+
# `customer.subscription.created` event lands before the local
|
|
61
|
+
# row is upserted; the leaf handler does the upsert and reads
|
|
62
|
+
# account_id off the just-saved row in that case).
|
|
63
|
+
def account_id
|
|
64
|
+
return @account_id if defined?(@account_id)
|
|
65
|
+
|
|
66
|
+
@account_id =
|
|
67
|
+
Billing::Subscription.where(customer_ref: customer_ref).pick(:account_id) if customer_ref
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def publish(seams_event = self.class::SEAMS_EVENT, ref: object_id)
|
|
71
|
+
Seams::Events::Publisher.publish(
|
|
72
|
+
seams_event,
|
|
73
|
+
gateway: gateway,
|
|
74
|
+
livemode: event[:livemode],
|
|
75
|
+
account_id: account_id,
|
|
76
|
+
customer_ref: customer_ref,
|
|
77
|
+
ref: ref,
|
|
78
|
+
object_id: object_id,
|
|
79
|
+
object: object_hash
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def warn_upsert_failure(error)
|
|
84
|
+
Seams::Observability.adapter.warn(
|
|
85
|
+
"billing.webhook.upsert_failed",
|
|
86
|
+
engine: "billing",
|
|
87
|
+
event: self.class::SEAMS_EVENT,
|
|
88
|
+
error: error.message
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Billing
|
|
4
|
+
module Webhooks
|
|
5
|
+
module Handlers
|
|
6
|
+
# charge.refunded — full or partial refund applied. Subscribers
|
|
7
|
+
# typically use this to revoke access, send a refund-receipt
|
|
8
|
+
# email, or reverse internal credit grants.
|
|
9
|
+
class ChargeRefundedHandler < Billing::Webhooks::Handler
|
|
10
|
+
SEAMS_EVENT = "charge.refunded.billing"
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
publish
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Billing
|
|
4
|
+
module Webhooks
|
|
5
|
+
module Handlers
|
|
6
|
+
# Two distinct paths through one Stripe event:
|
|
7
|
+
#
|
|
8
|
+
# - mode: "subscription" → the recurring subscription is now
|
|
9
|
+
# paid + active. The customer.subscription.created event
|
|
10
|
+
# usually arrives first; this is the confirmation that the
|
|
11
|
+
# checkout flow itself completed.
|
|
12
|
+
# - mode: "payment" with metadata.access_type == "lifetime" →
|
|
13
|
+
# a Lifetime Deal was purchased. Hands off to the LTD
|
|
14
|
+
# service which creates the LifetimePass row + emits
|
|
15
|
+
# lifetime.purchased.billing.
|
|
16
|
+
#
|
|
17
|
+
# Stripe also fires checkout.session.async_payment_succeeded for
|
|
18
|
+
# delayed methods (ACH). The router maps both to this handler.
|
|
19
|
+
# Verified against
|
|
20
|
+
# https://docs.stripe.com/payments/checkout/fulfill-orders.
|
|
21
|
+
class CheckoutSessionCompletedHandler < Billing::Webhooks::Handler
|
|
22
|
+
SEAMS_EVENT = "checkout.session_completed.billing"
|
|
23
|
+
|
|
24
|
+
def call
|
|
25
|
+
if lifetime?
|
|
26
|
+
handle_lifetime
|
|
27
|
+
else
|
|
28
|
+
publish
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def lifetime?
|
|
35
|
+
mode_value == "payment" && access_type == "lifetime"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def handle_lifetime
|
|
39
|
+
Billing::Lifetime::CreatePassFromCheckoutService.call(
|
|
40
|
+
session: event[:object],
|
|
41
|
+
livemode: event[:livemode]
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def mode_value
|
|
46
|
+
object_hash.is_a?(Hash) && (object_hash[:mode] || object_hash["mode"])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def access_type
|
|
50
|
+
metadata = object_hash.is_a?(Hash) && (object_hash[:metadata] || object_hash["metadata"])
|
|
51
|
+
return nil unless metadata.is_a?(Hash)
|
|
52
|
+
|
|
53
|
+
metadata[:access_type] || metadata["access_type"]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Billing
|
|
4
|
+
module Webhooks
|
|
5
|
+
module Handlers
|
|
6
|
+
# invoice.created fires when Stripe drafts a new invoice (e.g.
|
|
7
|
+
# at the start of a billing period). Status is "draft" until
|
|
8
|
+
# finalised. Hosts rarely need to act on this — included for
|
|
9
|
+
# full audit coverage.
|
|
10
|
+
class InvoiceCreatedHandler < InvoiceHandlerBase
|
|
11
|
+
SEAMS_EVENT = "invoice.created.billing"
|
|
12
|
+
INVOICE_STATUS = "draft"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Billing
|
|
4
|
+
module Webhooks
|
|
5
|
+
module Handlers
|
|
6
|
+
# invoice.finalized — Stripe finalised the draft invoice and is
|
|
7
|
+
# about to attempt collection. Status flips draft → open.
|
|
8
|
+
class InvoiceFinalizedHandler < InvoiceHandlerBase
|
|
9
|
+
SEAMS_EVENT = "invoice.finalized.billing"
|
|
10
|
+
INVOICE_STATUS = "open"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Billing
|
|
4
|
+
module Webhooks
|
|
5
|
+
module Handlers
|
|
6
|
+
# Shared base for the five invoice.* handlers. Owns the upsert
|
|
7
|
+
# of Billing::Invoice. Subclasses set SEAMS_EVENT + INVOICE_STATUS.
|
|
8
|
+
#
|
|
9
|
+
# Account resolution mirrors SubscriptionHandlerBase: the
|
|
10
|
+
# invoice belongs to the same Account as the subscription it
|
|
11
|
+
# references; we resolve account_id by walking
|
|
12
|
+
# subscription_ref → Billing::Subscription#account_id, falling
|
|
13
|
+
# back to a sibling Billing::Invoice on the same customer_ref
|
|
14
|
+
# for one-off invoices that have no subscription.
|
|
15
|
+
class InvoiceHandlerBase < Billing::Webhooks::Handler
|
|
16
|
+
DEFAULT_STATUS = "open"
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
upsert_invoice
|
|
20
|
+
publish
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
protected
|
|
24
|
+
|
|
25
|
+
def upsert_invoice
|
|
26
|
+
return unless object_id && customer_ref && object_hash.is_a?(Hash)
|
|
27
|
+
|
|
28
|
+
invoice = Billing::Invoice.find_or_initialize_by(gateway_ref: object_id)
|
|
29
|
+
resolved_account_id = invoice.account_id || resolve_account_id_for_invoice
|
|
30
|
+
unless resolved_account_id
|
|
31
|
+
warn_upsert_failure(
|
|
32
|
+
StandardError.new("could not resolve account_id for customer_ref=#{customer_ref}")
|
|
33
|
+
)
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
invoice.account_id = resolved_account_id
|
|
38
|
+
invoice.customer_ref = customer_ref
|
|
39
|
+
invoice.subscription_ref = object_hash[:subscription] || object_hash["subscription"]
|
|
40
|
+
invoice.amount_cents = object_hash[:amount_paid] || object_hash["amount_paid"] ||
|
|
41
|
+
object_hash[:amount_due] || object_hash["amount_due"] || 0
|
|
42
|
+
invoice.currency = (object_hash[:currency] || object_hash["currency"] || "usd").to_s.upcase
|
|
43
|
+
invoice.status = invoice_status
|
|
44
|
+
invoice.paid_at = paid_at_for_status
|
|
45
|
+
invoice.save!
|
|
46
|
+
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique => e
|
|
47
|
+
warn_upsert_failure(e)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# First, try the subscription_ref → Subscription mirror.
|
|
51
|
+
# Fall back to any sibling Invoice on the same customer.
|
|
52
|
+
# Final fallback is a Subscription row on the same
|
|
53
|
+
# customer_ref (covers the case of a one-off invoice that
|
|
54
|
+
# arrives before any sibling Invoice).
|
|
55
|
+
def resolve_account_id_for_invoice
|
|
56
|
+
subscription_ref = object_hash[:subscription] || object_hash["subscription"]
|
|
57
|
+
if subscription_ref
|
|
58
|
+
id = Billing::Subscription.where(gateway_ref: subscription_ref).pick(:account_id)
|
|
59
|
+
return id if id
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
Billing::Invoice.where(customer_ref: customer_ref)
|
|
63
|
+
.where.not(account_id: nil)
|
|
64
|
+
.pick(:account_id) ||
|
|
65
|
+
Billing::Subscription.where(customer_ref: customer_ref)
|
|
66
|
+
.where.not(account_id: nil)
|
|
67
|
+
.pick(:account_id)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def invoice_status
|
|
71
|
+
self.class::INVOICE_STATUS || DEFAULT_STATUS
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def paid_at_for_status
|
|
75
|
+
invoice_status == "paid" ? Time.current : nil
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Billing
|
|
4
|
+
module Webhooks
|
|
5
|
+
module Handlers
|
|
6
|
+
# payment_intent.succeeded — a one-off payment cleared. No local
|
|
7
|
+
# row to upsert (PaymentIntents are not modelled locally — see
|
|
8
|
+
# Phase 3A scope review). Just publishes the canonical event so
|
|
9
|
+
# subscribers can react (notifications, audit log, etc).
|
|
10
|
+
class PaymentSucceededHandler < Billing::Webhooks::Handler
|
|
11
|
+
SEAMS_EVENT = "payment.succeeded.billing"
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
publish
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Billing
|
|
4
|
+
module Webhooks
|
|
5
|
+
module Handlers
|
|
6
|
+
# Stripe sends customer.subscription.deleted when the
|
|
7
|
+
# subscription has actually ended (period elapsed after a
|
|
8
|
+
# period-end cancel, or immediate cancellation). The local
|
|
9
|
+
# status flips to "canceled".
|
|
10
|
+
class SubscriptionDeletedHandler < SubscriptionHandlerBase
|
|
11
|
+
SEAMS_EVENT = "subscription.canceled.billing"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Billing
|
|
4
|
+
module Webhooks
|
|
5
|
+
module Handlers
|
|
6
|
+
# Base for the four customer.subscription.* handlers. Owns the
|
|
7
|
+
# upsert of Billing::Subscription so each leaf class just sets
|
|
8
|
+
# SEAMS_EVENT and inherits.
|
|
9
|
+
#
|
|
10
|
+
# Account resolution: for subscriptions created via the host's
|
|
11
|
+
# `Account#start_subscription!` flow, StartSubscriptionJob has
|
|
12
|
+
# already inserted a Billing::Subscription row with `account_id`
|
|
13
|
+
# set; the upsert here finds it via `gateway_ref` and preserves
|
|
14
|
+
# the existing `account_id`. For subscriptions created directly
|
|
15
|
+
# in the Stripe Dashboard (no local row pre-exists), the upsert
|
|
16
|
+
# falls back to looking up `account_id` from any sibling row
|
|
17
|
+
# under the same `customer_ref`. If neither path resolves,
|
|
18
|
+
# the upsert is skipped + a warning is logged — the host
|
|
19
|
+
# supports this case by reconciling the orphaned subscription
|
|
20
|
+
# via a sync task (see README "Stripe-initiated subscriptions").
|
|
21
|
+
class SubscriptionHandlerBase < Billing::Webhooks::Handler
|
|
22
|
+
def call
|
|
23
|
+
upsert_subscription
|
|
24
|
+
publish
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
protected
|
|
28
|
+
|
|
29
|
+
def upsert_subscription
|
|
30
|
+
return unless object_id && customer_ref && object_hash.is_a?(Hash)
|
|
31
|
+
|
|
32
|
+
subscription = Billing::Subscription.find_or_initialize_by(gateway_ref: object_id)
|
|
33
|
+
resolved_account_id = subscription.account_id || resolve_account_id_from_sibling
|
|
34
|
+
unless resolved_account_id
|
|
35
|
+
warn_upsert_failure(
|
|
36
|
+
StandardError.new("could not resolve account_id for customer_ref=#{customer_ref}")
|
|
37
|
+
)
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
subscription.account_id = resolved_account_id
|
|
42
|
+
subscription.customer_ref = customer_ref
|
|
43
|
+
subscription.plan_ref = plan_ref_from_object_hash
|
|
44
|
+
subscription.status = object_hash[:status] || object_hash["status"] || subscription.status || "incomplete"
|
|
45
|
+
subscription.current_period_end = current_period_end_at
|
|
46
|
+
subscription.save!
|
|
47
|
+
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique => e
|
|
48
|
+
warn_upsert_failure(e)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Look up account_id from any other Billing::Subscription row
|
|
52
|
+
# under the same Stripe customer. A customer almost always
|
|
53
|
+
# belongs to one Account post-Wave-9 (Account == Stripe
|
|
54
|
+
# customer), so the first sibling row's account_id is the
|
|
55
|
+
# correct answer.
|
|
56
|
+
def resolve_account_id_from_sibling
|
|
57
|
+
Billing::Subscription.where(customer_ref: customer_ref)
|
|
58
|
+
.where.not(account_id: nil)
|
|
59
|
+
.pick(:account_id)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def plan_ref_from_object_hash
|
|
63
|
+
first = first_subscription_item
|
|
64
|
+
return nil unless first
|
|
65
|
+
|
|
66
|
+
price = first[:price] || first["price"]
|
|
67
|
+
price.is_a?(Hash) ? (price[:id] || price["id"]) : nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Stripe moved current_period_end onto each subscription item
|
|
71
|
+
# in 2024; older API versions still set it at the root. Try
|
|
72
|
+
# root first, fall back to items.data[0].
|
|
73
|
+
# https://docs.stripe.com/api/subscriptions/object
|
|
74
|
+
def current_period_end_at
|
|
75
|
+
unix = object_hash[:current_period_end] ||
|
|
76
|
+
object_hash["current_period_end"]
|
|
77
|
+
unix ||= (item = first_subscription_item) &&
|
|
78
|
+
(item[:current_period_end] || item["current_period_end"])
|
|
79
|
+
unix && Time.at(unix)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def first_subscription_item
|
|
83
|
+
items = object_hash[:items] || object_hash["items"]
|
|
84
|
+
return nil unless items.is_a?(Hash)
|
|
85
|
+
|
|
86
|
+
first = (items[:data] || items["data"] || []).first
|
|
87
|
+
first if first.is_a?(Hash)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Billing
|
|
4
|
+
module Webhooks
|
|
5
|
+
module Handlers
|
|
6
|
+
# Fires ~3 days before a trialing subscription transitions to
|
|
7
|
+
# active. Hosts subscribe to send "your trial is ending" email.
|
|
8
|
+
# Verified against
|
|
9
|
+
# https://docs.stripe.com/api/events/types#event_types-customer.subscription.trial_will_end
|
|
10
|
+
class SubscriptionTrialWillEndHandler < SubscriptionHandlerBase
|
|
11
|
+
SEAMS_EVENT = "subscription.trial_will_end.billing"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<h1>Lifetime passes</h1>
|
|
2
|
+
|
|
3
|
+
<p><%%= link_to "Grant a new pass", billing.new_admin_lifetime_pass_path %></p>
|
|
4
|
+
|
|
5
|
+
<table>
|
|
6
|
+
<thead>
|
|
7
|
+
<tr>
|
|
8
|
+
<th>Account</th>
|
|
9
|
+
<th>Customer</th>
|
|
10
|
+
<th>Plan</th>
|
|
11
|
+
<th>Type</th>
|
|
12
|
+
<th>Granted at</th>
|
|
13
|
+
<th>Status</th>
|
|
14
|
+
<th></th>
|
|
15
|
+
</tr>
|
|
16
|
+
</thead>
|
|
17
|
+
<tbody>
|
|
18
|
+
<%% @passes.each do |pass| %>
|
|
19
|
+
<tr>
|
|
20
|
+
<td><%%= pass.account_id %></td>
|
|
21
|
+
<td><%%= pass.customer_ref %></td>
|
|
22
|
+
<td><%%= pass.plan_ref %></td>
|
|
23
|
+
<td><%%= pass.paid? ? "Paid" : "Granted" %></td>
|
|
24
|
+
<td><%%= pass.granted_at.utc.iso8601 %></td>
|
|
25
|
+
<td><%%= pass.revoked? ? "Revoked at #{pass.revoked_at.utc.iso8601}" : "Active" %></td>
|
|
26
|
+
<td>
|
|
27
|
+
<%% unless pass.revoked? %>
|
|
28
|
+
<%%= button_to "Revoke", billing.admin_lifetime_pass_path(pass),
|
|
29
|
+
method: :delete,
|
|
30
|
+
form: { onsubmit: "return confirm('Revoke this lifetime pass?')" } %>
|
|
31
|
+
<%% end %>
|
|
32
|
+
</td>
|
|
33
|
+
</tr>
|
|
34
|
+
<%% end %>
|
|
35
|
+
</tbody>
|
|
36
|
+
</table>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<h1>Grant a lifetime pass</h1>
|
|
2
|
+
|
|
3
|
+
<%%= form_with url: billing.admin_lifetime_passes_path, scope: nil, local: true do %>
|
|
4
|
+
<p>
|
|
5
|
+
<label>Account id (Accounts::Account#id, UUID):
|
|
6
|
+
<input type="text" name="account_id" required>
|
|
7
|
+
</label>
|
|
8
|
+
</p>
|
|
9
|
+
<p>
|
|
10
|
+
<label>Customer ref (Stripe customer id):
|
|
11
|
+
<input type="text" name="customer_ref" required>
|
|
12
|
+
</label>
|
|
13
|
+
</p>
|
|
14
|
+
<p>
|
|
15
|
+
<label>Plan:
|
|
16
|
+
<select name="plan_ref" required>
|
|
17
|
+
<option value=""></option>
|
|
18
|
+
<%% @plan_options.each do |plan| %>
|
|
19
|
+
<option value="<%%= plan.gateway_ref %>">
|
|
20
|
+
<%%= plan.name %> · <%%= plan.gateway_ref %>
|
|
21
|
+
<%% if (rem = plan.lifetime_inventory_remaining) %>
|
|
22
|
+
· <%%= rem %> remaining
|
|
23
|
+
<%% end %>
|
|
24
|
+
</option>
|
|
25
|
+
<%% end %>
|
|
26
|
+
</select>
|
|
27
|
+
</label>
|
|
28
|
+
</p>
|
|
29
|
+
<p>
|
|
30
|
+
<label>Notes:
|
|
31
|
+
<textarea name="notes" rows="3"></textarea>
|
|
32
|
+
</label>
|
|
33
|
+
</p>
|
|
34
|
+
<button type="submit">Grant lifetime pass</button>
|
|
35
|
+
<%% end %>
|
|
36
|
+
|
|
37
|
+
<p><%%= link_to "Back", billing.admin_lifetime_passes_path %></p>
|