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