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,355 @@
|
|
|
1
|
+
# Billing
|
|
2
|
+
|
|
3
|
+
> Subscription billing for a Seams-powered host. Stripe by default,
|
|
4
|
+
> swap-able to any gateway via the `Billing::Gateways::Abstract`
|
|
5
|
+
> contract.
|
|
6
|
+
|
|
7
|
+
**Requires:** by default, the `accounts` engine — billing rows
|
|
8
|
+
carry `account_id` and the `Billing::Billable` concern auto-includes
|
|
9
|
+
into `Billing.configuration.billable_class` (default
|
|
10
|
+
`"Accounts::Account"`). Hosts that prefer User-as-customer
|
|
11
|
+
override `billable_class` in `config/initializers/billing.rb` and
|
|
12
|
+
can run without the accounts engine.
|
|
13
|
+
|
|
14
|
+
## The Account-as-customer model
|
|
15
|
+
|
|
16
|
+
Post-Wave-9, the **Stripe customer represents an Account, not an
|
|
17
|
+
Identity (human)**. Subscriptions, invoices, and lifetime passes
|
|
18
|
+
belong to the tenant. The same Identity who is a member of two
|
|
19
|
+
Accounts has two independent billing relationships — even though
|
|
20
|
+
they're the same human.
|
|
21
|
+
|
|
22
|
+
Concrete: every billing table carries `account_id` (UUID, the
|
|
23
|
+
`Accounts::Account#id`) as its local foreign key. The Stripe
|
|
24
|
+
`customer_ref` (the `cus_*` id) is also stored on every row, so
|
|
25
|
+
webhook handlers can address rows either way. There is no
|
|
26
|
+
`user_id` / `host_user_id` column anywhere — those were removed in
|
|
27
|
+
this refactor.
|
|
28
|
+
|
|
29
|
+
`granted_by_identity_id` and `revoked_by_identity_id` on
|
|
30
|
+
`billing_lifetime_passes` are deliberate exceptions: they reference
|
|
31
|
+
an `Auth::Identity` (the human who pressed the "grant" or "revoke"
|
|
32
|
+
button), not an Account. The columns track human action, not
|
|
33
|
+
tenant-level data.
|
|
34
|
+
|
|
35
|
+
### Migration story for hosts on a prior wave
|
|
36
|
+
|
|
37
|
+
If your host already had Wave 8 billing in production with `user_id`
|
|
38
|
+
on `billing_subscriptions` etc, those columns are gone. Write a
|
|
39
|
+
host-side data migration that backfills `account_id` from your
|
|
40
|
+
existing User → Account mapping before adopting the new schema.
|
|
41
|
+
There is no automated migration shipped with seams — every host's
|
|
42
|
+
mapping is different.
|
|
43
|
+
|
|
44
|
+
## Wiring `Billing::Billable` into your tenant model
|
|
45
|
+
|
|
46
|
+
`Billing::Billable` provides the helpers that the host's tenant
|
|
47
|
+
model needs:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
account.billing_subscriptions # association
|
|
51
|
+
account.billing_invoices # association
|
|
52
|
+
account.billing_lifetime_passes # association
|
|
53
|
+
account.has_active_billing? # paying right now?
|
|
54
|
+
account.lifetime? # holds an active LTD?
|
|
55
|
+
account.has_lifetime_for?(plan_ref:) # specific LTD?
|
|
56
|
+
account.start_subscription!(plan_ref:, email:)
|
|
57
|
+
account.cancel_subscription!(subscription_ref:)
|
|
58
|
+
account.stripe_customer_ref!(email:) # lazy Stripe customer creation
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The engine wires the concern into `Accounts::Account` automatically
|
|
62
|
+
at boot via:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
# lib/billing/engine.rb (paraphrased)
|
|
66
|
+
config.to_prepare do
|
|
67
|
+
Billing.configuration.billable_class.constantize.include(Billing::Billable)
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Override the target class in `config/initializers/billing.rb` if your
|
|
72
|
+
tenant lives on a different model:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
Billing.configure do |c|
|
|
76
|
+
c.billable_class = "Workspaces::Workspace"
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Set `c.billable_class = nil` to opt out and `include Billing::Billable`
|
|
81
|
+
manually wherever it makes sense.
|
|
82
|
+
|
|
83
|
+
## Events emitted
|
|
84
|
+
|
|
85
|
+
All billing events publish the **same canonical payload shape** so
|
|
86
|
+
subscribers can read one format regardless of source:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
{ gateway: "stripe", # which adapter emitted it
|
|
90
|
+
livemode: true | false, # gateway's livemode flag
|
|
91
|
+
account_id: "uuid", # the Accounts::Account that owns this row
|
|
92
|
+
customer_ref: "cus_xxx", # gateway customer id
|
|
93
|
+
ref: "sub_xxx", # canonical id of the subject
|
|
94
|
+
object_id: "sub_xxx", # raw object id from the gateway
|
|
95
|
+
object: { ... } } # the gateway object as a hash (incl. account_id where applicable)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
| Event name | Subject | Emitted when |
|
|
99
|
+
| --- | --- | --- |
|
|
100
|
+
| `subscription.created.billing` | Subscription | StartSubscriptionJob succeeds, or `customer.subscription.created` webhook fires |
|
|
101
|
+
| `subscription.updated.billing` | Subscription | `customer.subscription.updated` webhook fires |
|
|
102
|
+
| `subscription.canceled.billing` | Subscription | CancelSubscriptionJob succeeds, or `customer.subscription.deleted` webhook fires |
|
|
103
|
+
| `subscription.trial_will_end.billing` | Subscription | `customer.subscription.trial_will_end` webhook fires (~3 days before trial end) |
|
|
104
|
+
| `invoice.created.billing` | Invoice | `invoice.created` webhook fires (status: draft) |
|
|
105
|
+
| `invoice.paid.billing` | Invoice | `invoice.paid` webhook fires |
|
|
106
|
+
| `invoice.failed.billing` | Invoice | `invoice.payment_failed` webhook fires |
|
|
107
|
+
| `invoice.finalized.billing` | Invoice | `invoice.finalized` webhook fires |
|
|
108
|
+
| `invoice.voided.billing` | Invoice | `invoice.voided` webhook fires |
|
|
109
|
+
| `payment.succeeded.billing` | PaymentIntent | `payment_intent.succeeded` webhook fires |
|
|
110
|
+
| `payment.failed.billing` | PaymentIntent | `payment_intent.payment_failed` webhook fires |
|
|
111
|
+
| `charge.refunded.billing` | Charge | `charge.refunded` webhook fires |
|
|
112
|
+
| `checkout.session_completed.billing` | CheckoutSession | `checkout.session.completed` webhook fires (subscription mode; LTD mode forks to lifetime.purchased.billing) |
|
|
113
|
+
| `lifetime.granted.billing` | LifetimePass | Admin grants a Lifetime Deal via `Billing::Lifetime::GrantPassService`. Includes `granted_by_identity_id`. |
|
|
114
|
+
| `lifetime.purchased.billing` | LifetimePass | Customer pays for an LTD via Stripe Checkout `mode: "payment"` |
|
|
115
|
+
| `lifetime.revoked.billing` | LifetimePass | `Billing::Lifetime::RevokePassService` is called. Includes `revoked_by_identity_id`. |
|
|
116
|
+
|
|
117
|
+
The webhook controller upserts a local `Billing::Subscription` /
|
|
118
|
+
`Billing::Invoice` row before publishing, so subscribers can resolve
|
|
119
|
+
either from `payload[:account_id]` directly or by querying the
|
|
120
|
+
local DB with `payload[:ref]`.
|
|
121
|
+
|
|
122
|
+
## Events consumed
|
|
123
|
+
|
|
124
|
+
This engine does not subscribe to any other engine's events by default.
|
|
125
|
+
Hosts often subscribe to `account.created.accounts` to create a Stripe
|
|
126
|
+
customer at tenant creation time.
|
|
127
|
+
|
|
128
|
+
## Exposed concerns
|
|
129
|
+
|
|
130
|
+
| Concern | Purpose |
|
|
131
|
+
| --- | --- |
|
|
132
|
+
| `Billing::Billable` | Mix into your tenant model (default `Accounts::Account`) for `start_subscription!` / `cancel_subscription!` / `lifetime?` / `has_lifetime_for?(plan_ref:)` / `has_active_billing?` helpers. The engine auto-includes it via `Billing.configuration.billable_class` at boot. |
|
|
133
|
+
|
|
134
|
+
## Lifetime Deals (LTD)
|
|
135
|
+
|
|
136
|
+
The engine ships native LTD support — one-time payment for permanent
|
|
137
|
+
access — alongside recurring subscriptions. Two flows:
|
|
138
|
+
|
|
139
|
+
1. **Public purchase** — Stripe Checkout with `mode: "payment"`. The
|
|
140
|
+
pricing page renders any `Billing::Plan` with `interval: "lifetime"`
|
|
141
|
+
under a "Buy once, own forever" section. POST
|
|
142
|
+
`/billing/checkout/lifetime?plan=<gateway_ref>` →
|
|
143
|
+
`Billing::Lifetime::CreateLifetimeSessionService` (which threads
|
|
144
|
+
the current Account's id into Stripe session metadata) →
|
|
145
|
+
redirect to Stripe → on `checkout.session.completed` (or
|
|
146
|
+
`checkout.session.async_payment_succeeded`) the webhook handler
|
|
147
|
+
creates the `Billing::LifetimePass` row (with `account_id` read
|
|
148
|
+
off the metadata) and publishes `lifetime.purchased.billing`.
|
|
149
|
+
|
|
150
|
+
2. **Private grant** — admin issues an LTD without a Stripe charge via
|
|
151
|
+
`Billing::Admin::LifetimePassesController` (mounted at
|
|
152
|
+
`/billing/admin/lifetime_passes`). Use for early adopters,
|
|
153
|
+
influencer giveaways, ToS-violation refund-then-re-grant. Calls
|
|
154
|
+
`Billing::Lifetime::GrantPassService` with the target Account's id
|
|
155
|
+
AND the Identity of the admin pressing the button. Publishes
|
|
156
|
+
`lifetime.granted.billing` with `granted_by_identity_id` set.
|
|
157
|
+
|
|
158
|
+
### Trade-off (read this before turning LTDs on)
|
|
159
|
+
|
|
160
|
+
LTDs lock you into supporting those users **indefinitely with no
|
|
161
|
+
recurring revenue**. They're a strong early-adopter / launch lever —
|
|
162
|
+
quick cash, fast feedback — but get expensive long-term as your
|
|
163
|
+
support load grows while LTD users contribute zero MRR. Cap inventory
|
|
164
|
+
on every LTD plan via `max_lifetime_units` (nil = unlimited):
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
Billing::Plan.create!(
|
|
168
|
+
gateway_ref: "price_lifetime_pro_2026",
|
|
169
|
+
name: "Pro Lifetime",
|
|
170
|
+
interval: "lifetime",
|
|
171
|
+
amount_cents: 249_00,
|
|
172
|
+
currency: "usd",
|
|
173
|
+
max_lifetime_units: 100 # only the first 100 buyers
|
|
174
|
+
)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The pricing page reads `Plan#lifetime_inventory_remaining` and
|
|
178
|
+
disables the "Buy lifetime" button when `lifetime_sold_out?`.
|
|
179
|
+
|
|
180
|
+
### Authorization model
|
|
181
|
+
|
|
182
|
+
The engine ships no admin gate — `Billing::Admin::LifetimePassesController`
|
|
183
|
+
mounts at `/billing/admin/...` and the host wires their own
|
|
184
|
+
`require_admin!` `before_action` (ActiveAdmin / Avo / your own
|
|
185
|
+
solution). Per the issue #2 4B scope decision, Seams doesn't ship its
|
|
186
|
+
own admin engine.
|
|
187
|
+
|
|
188
|
+
### Revoke
|
|
189
|
+
|
|
190
|
+
`Billing::Lifetime::RevokePassService.call(pass:, revoked_by:, notes:)`
|
|
191
|
+
soft-revokes via `revoked_at`. `revoked_by` is an Auth::Identity
|
|
192
|
+
(the human pressing the revoke button); pass `nil` for system-revoked
|
|
193
|
+
passes. The pass row stays in the DB so the audit trail survives.
|
|
194
|
+
Stripe refund (for paid LTDs) is the caller's responsibility — this
|
|
195
|
+
service updates the local row only. Publishes `lifetime.revoked.billing`.
|
|
196
|
+
|
|
197
|
+
## Gateways
|
|
198
|
+
|
|
199
|
+
| Gateway | Default | Configure via |
|
|
200
|
+
| --- | --- | --- |
|
|
201
|
+
| `Billing::Gateways::Stripe` | yes | `Billing.configure { \|c\| c.gateway = "Billing::Gateways::Stripe" }` |
|
|
202
|
+
|
|
203
|
+
To add a gateway, subclass `Billing::Gateways::Abstract` and implement
|
|
204
|
+
`#create_subscription`, `#cancel_subscription`, `#fetch_subscription`,
|
|
205
|
+
`#verify_webhook`. Then point `Billing.configuration.gateway` at the
|
|
206
|
+
new class.
|
|
207
|
+
|
|
208
|
+
## Webhook setup
|
|
209
|
+
|
|
210
|
+
Stripe will POST events to `/billing/webhooks/stripe`. The engine
|
|
211
|
+
verifies signatures via `Billing::Stripe::WebhookSignature`. Set:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
STRIPE_SECRET_KEY=sk_live_...
|
|
215
|
+
STRIPE_WEBHOOK_SECRET=whsec_...
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
In your Stripe dashboard, configure the webhook endpoint to send any
|
|
219
|
+
of the events listed below. Each maps to a handler class in
|
|
220
|
+
`app/services/billing/webhooks/handlers/`; missing handlers no-op
|
|
221
|
+
gracefully so subscribing to extras is safe.
|
|
222
|
+
|
|
223
|
+
| Stripe event | Handler | Canonical seams event |
|
|
224
|
+
| --- | --- | --- |
|
|
225
|
+
| `customer.subscription.created` | `SubscriptionCreatedHandler` | `subscription.created.billing` |
|
|
226
|
+
| `customer.subscription.updated` | `SubscriptionUpdatedHandler` | `subscription.updated.billing` |
|
|
227
|
+
| `customer.subscription.deleted` | `SubscriptionDeletedHandler` | `subscription.canceled.billing` |
|
|
228
|
+
| `customer.subscription.trial_will_end` | `SubscriptionTrialWillEndHandler` | `subscription.trial_will_end.billing`|
|
|
229
|
+
| `invoice.created` | `InvoiceCreatedHandler` | `invoice.created.billing` |
|
|
230
|
+
| `invoice.paid` | `InvoicePaidHandler` | `invoice.paid.billing` |
|
|
231
|
+
| `invoice.payment_failed` | `InvoicePaymentFailedHandler` | `invoice.failed.billing` |
|
|
232
|
+
| `invoice.finalized` | `InvoiceFinalizedHandler` | `invoice.finalized.billing` |
|
|
233
|
+
| `invoice.voided` | `InvoiceVoidedHandler` | `invoice.voided.billing` |
|
|
234
|
+
| `payment_intent.succeeded` | `PaymentSucceededHandler` | `payment.succeeded.billing` |
|
|
235
|
+
| `payment_intent.payment_failed` | `PaymentFailedHandler` | `payment.failed.billing` |
|
|
236
|
+
| `charge.refunded` | `ChargeRefundedHandler` | `charge.refunded.billing` |
|
|
237
|
+
| `checkout.session.completed` | `CheckoutSessionCompletedHandler` | (subscription path / LTD path) |
|
|
238
|
+
|
|
239
|
+
The handler base resolves `account_id` for each webhook by looking
|
|
240
|
+
up the local `Billing::Subscription` / `Billing::Invoice` row keyed
|
|
241
|
+
on the gateway's `customer_ref`. For brand-new Stripe-initiated
|
|
242
|
+
subscriptions (created in the Stripe Dashboard, not via
|
|
243
|
+
`Account#start_subscription!`), there's no local row to resolve —
|
|
244
|
+
the upsert logs a warning and skips. Reconcile those via a
|
|
245
|
+
host-side sync task that maps Stripe customers to Accounts.
|
|
246
|
+
|
|
247
|
+
Adding a new event type means registering a handler from your host —
|
|
248
|
+
no fork required:
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
Billing::Webhooks::EventRouter.register(
|
|
252
|
+
"customer.tax_id.created",
|
|
253
|
+
"MyApp::TaxIdCreatedHandler"
|
|
254
|
+
)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Default dispatch is synchronous so handler raises roll back the
|
|
258
|
+
`WebhookEvent` row and Stripe retries. Flip
|
|
259
|
+
`Billing.configuration.process_webhooks_async = true` to enqueue
|
|
260
|
+
`Billing::Webhooks::ProcessEventJob.perform_later` instead — Stripe
|
|
261
|
+
recommends responding in <100ms.
|
|
262
|
+
|
|
263
|
+
## Self-service controllers
|
|
264
|
+
|
|
265
|
+
| Controller | Action |
|
|
266
|
+
| --- | --- |
|
|
267
|
+
| `Billing::SubscriptionsController#index` | List the current Account's subscriptions |
|
|
268
|
+
| `#show` | Single subscription with cancel / change-plan controls|
|
|
269
|
+
| `#cancel` | Period-end cancel (immediate via `?immediate=1`) |
|
|
270
|
+
| `#reactivate` | Un-cancel a pending-cancellation subscription |
|
|
271
|
+
| `#change_plan` | Switch to a new price (proration configurable) |
|
|
272
|
+
| `Billing::InvoicesController#index/#show` | Read-only billing history |
|
|
273
|
+
|
|
274
|
+
The controllers expect `current_billing_account` to return the
|
|
275
|
+
current `Accounts::Account` — by default they read
|
|
276
|
+
`Accounts::Current.account` (set up by the accounts engine's
|
|
277
|
+
controller concern). Override `#current_billing_account` and
|
|
278
|
+
`#current_billing_customer_ref` in your host's `SubscriptionsController` /
|
|
279
|
+
`InvoicesController` if your tenant resolution is bound differently.
|
|
280
|
+
|
|
281
|
+
## Stripe API surface used
|
|
282
|
+
|
|
283
|
+
Every Stripe call has a doc URL cited inline in
|
|
284
|
+
`lib/billing/gateways/stripe.rb` and in each `Billing::Stripe::Client`
|
|
285
|
+
method:
|
|
286
|
+
|
|
287
|
+
| Stripe call | Docs URL |
|
|
288
|
+
| --- | --- |
|
|
289
|
+
| `POST /v1/customers` | https://docs.stripe.com/api/customers/create |
|
|
290
|
+
| `GET /v1/customers/search` | https://docs.stripe.com/api/customers/search |
|
|
291
|
+
| `POST /v1/subscriptions` | https://docs.stripe.com/api/subscriptions/create |
|
|
292
|
+
| `POST /v1/subscriptions/:id` | https://docs.stripe.com/api/subscriptions/update |
|
|
293
|
+
| `DELETE /v1/subscriptions/:id` | https://docs.stripe.com/api/subscriptions/cancel |
|
|
294
|
+
| `GET /v1/subscriptions/:id` | https://docs.stripe.com/api/subscriptions/retrieve |
|
|
295
|
+
| `GET /v1/invoices/:id` | https://docs.stripe.com/api/invoices/retrieve |
|
|
296
|
+
| `POST /v1/checkout/sessions` | https://docs.stripe.com/api/checkout/sessions/create |
|
|
297
|
+
| `POST /v1/billing_portal/sessions`| https://docs.stripe.com/api/customer_portal/sessions/create |
|
|
298
|
+
| Webhook signature verification | https://docs.stripe.com/webhooks/signatures |
|
|
299
|
+
|
|
300
|
+
## Verifying the Stripe Checkout flow against test mode
|
|
301
|
+
|
|
302
|
+
For end-to-end confidence in the Stripe wiring, run a real Checkout
|
|
303
|
+
session against Stripe's test mode — no mocks, no webmock.
|
|
304
|
+
|
|
305
|
+
1. Grab a test secret key + webhook secret from
|
|
306
|
+
https://dashboard.stripe.com/test/apikeys and
|
|
307
|
+
https://dashboard.stripe.com/test/webhooks (point the webhook at a
|
|
308
|
+
tunnelled URL via `stripe listen --forward-to localhost:3000/billing/webhooks/stripe`).
|
|
309
|
+
2. Set the env vars:
|
|
310
|
+
```bash
|
|
311
|
+
STRIPE_SECRET_KEY=sk_test_...
|
|
312
|
+
STRIPE_WEBHOOK_SECRET=whsec_... # from `stripe listen` output
|
|
313
|
+
```
|
|
314
|
+
3. Seed at least one Plan whose `gateway_ref` matches a Stripe test
|
|
315
|
+
price id.
|
|
316
|
+
4. Visit `/billing/plans`, click "Subscribe", complete Checkout with
|
|
317
|
+
the test card `4242 4242 4242 4242`.
|
|
318
|
+
5. Watch your logs — `customer.subscription.created` and
|
|
319
|
+
`invoice.paid` should arrive within seconds; the local
|
|
320
|
+
`Billing::Subscription` + `Billing::Invoice` rows should appear
|
|
321
|
+
with the current Account's id pinned to them.
|
|
322
|
+
6. Visit `/billing/subscriptions` — your new subscription should be
|
|
323
|
+
listed. Cancel + reactivate via the UI to exercise the full
|
|
324
|
+
service-object surface.
|
|
325
|
+
|
|
326
|
+
If webhook events do not arrive, check
|
|
327
|
+
`Billing::WebhookEvent.where(gateway: "stripe").order(created_at: :desc)`
|
|
328
|
+
— rows mean Stripe reached you but a handler raised; absence means
|
|
329
|
+
the signature verification or the URL is wrong.
|
|
330
|
+
|
|
331
|
+
## Gateway contract specs
|
|
332
|
+
|
|
333
|
+
The shared example `"a billing gateway"` lives at
|
|
334
|
+
`spec/support/shared_examples/a_billing_gateway.rb`. Every gateway
|
|
335
|
+
adapter (Stripe is the reference; Paddle / Adyen / your own) MUST
|
|
336
|
+
satisfy it:
|
|
337
|
+
|
|
338
|
+
```ruby
|
|
339
|
+
RSpec.describe Billing::Gateways::Paddle do
|
|
340
|
+
it_behaves_like "a billing gateway"
|
|
341
|
+
end
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
The contract checks that every method on `Billing::Gateways::Abstract`
|
|
345
|
+
exists on the subclass with the documented keyword arguments, and
|
|
346
|
+
that `verify_webhook` raises `Billing::WebhookError` on a bad
|
|
347
|
+
signature. Wiring-level correctness ("does Paddle actually charge
|
|
348
|
+
people?") needs an integration test against that gateway's test mode
|
|
349
|
+
— see the Stripe walk-through above for the pattern.
|
|
350
|
+
|
|
351
|
+
## Running the specs
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
bin/rails seams:test[billing]
|
|
355
|
+
```
|
data/lib/generators/seams/billing/templates/app/controllers/admin/lifetime_passes_controller.rb.tt
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Billing
|
|
4
|
+
module Admin
|
|
5
|
+
# Admin-side controller for issuing + revoking LifetimePasses
|
|
6
|
+
# without a Stripe charge. Use for early adopters, influencer
|
|
7
|
+
# giveaways, and ToS revocations.
|
|
8
|
+
#
|
|
9
|
+
# Authorization is the host's responsibility — Seams ships no
|
|
10
|
+
# admin engine (per the explicit scope decision in issue #2 4B).
|
|
11
|
+
# Mount this behind whatever admin gate the host uses (ActiveAdmin,
|
|
12
|
+
# Avo, your own #require_admin! before_action).
|
|
13
|
+
#
|
|
14
|
+
# Suggested host-side wiring:
|
|
15
|
+
#
|
|
16
|
+
# # config/routes.rb
|
|
17
|
+
# namespace :admin do
|
|
18
|
+
# resources :lifetime_passes, controller: "billing/admin/lifetime_passes",
|
|
19
|
+
# only: %i[index new create destroy]
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# # app/controllers/billing/admin/lifetime_passes_controller_decorator.rb
|
|
23
|
+
# Billing::Admin::LifetimePassesController.class_eval do
|
|
24
|
+
# before_action :require_admin!
|
|
25
|
+
# end
|
|
26
|
+
class LifetimePassesController < ApplicationController
|
|
27
|
+
def index
|
|
28
|
+
@passes = Billing::LifetimePass.order(granted_at: :desc).limit(100)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def new
|
|
32
|
+
@plan_options = Billing::Plan.active.lifetime
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def create
|
|
36
|
+
result = Billing::Lifetime::GrantPassService.call(
|
|
37
|
+
account_id: params.require(:account_id),
|
|
38
|
+
customer_ref: params.require(:customer_ref),
|
|
39
|
+
plan_ref: params.require(:plan_ref),
|
|
40
|
+
granted_by: current_admin_identity,
|
|
41
|
+
notes: params[:notes]
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if result.ok?
|
|
45
|
+
redirect_to billing.admin_lifetime_passes_path,
|
|
46
|
+
notice: "Lifetime pass issued."
|
|
47
|
+
else
|
|
48
|
+
redirect_to billing.new_admin_lifetime_pass_path,
|
|
49
|
+
alert: result.error
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def destroy
|
|
54
|
+
pass = Billing::LifetimePass.find(params[:id])
|
|
55
|
+
result = Billing::Lifetime::RevokePassService.call(
|
|
56
|
+
pass: pass,
|
|
57
|
+
revoked_by: current_admin_identity,
|
|
58
|
+
notes: params[:notes]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if result.ok?
|
|
62
|
+
redirect_to billing.admin_lifetime_passes_path,
|
|
63
|
+
notice: "Lifetime pass revoked."
|
|
64
|
+
else
|
|
65
|
+
redirect_to billing.admin_lifetime_passes_path,
|
|
66
|
+
alert: result.error
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# Override in the host. The default reads `current_user` if the
|
|
73
|
+
# host's admin gate uses the same `current_user` helper — for a
|
|
74
|
+
# post-Wave-9 host that means the Auth::Identity row currently
|
|
75
|
+
# signed in. Pre-Wave-9 hosts (host User present) override this
|
|
76
|
+
# to return the User; the GrantPassService coerces both shapes.
|
|
77
|
+
def current_admin_identity
|
|
78
|
+
return nil unless respond_to?(:current_user)
|
|
79
|
+
|
|
80
|
+
current_user
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Billing
|
|
4
|
+
class CheckoutController < ApplicationController
|
|
5
|
+
# POST /billing/checkout?plan=price_xxx
|
|
6
|
+
def create
|
|
7
|
+
plan = Billing::Plan.active.find_by!(gateway_ref: params[:plan])
|
|
8
|
+
|
|
9
|
+
result = Billing::Checkout::CreateSessionService.call(
|
|
10
|
+
customer_ref: customer_ref_or_create!,
|
|
11
|
+
plan_ref: plan.gateway_ref,
|
|
12
|
+
success_url: billing.checkout_success_url,
|
|
13
|
+
cancel_url: billing.plans_url
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if result.ok?
|
|
17
|
+
redirect_to result.url, allow_other_host: true, status: :see_other
|
|
18
|
+
else
|
|
19
|
+
redirect_to billing.plans_path, alert: result.error
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# GET /billing/checkout/success
|
|
24
|
+
def success
|
|
25
|
+
# Stripe will fire `checkout.session.completed` webhook; the
|
|
26
|
+
# subscription row gets created/updated by the webhook handler.
|
|
27
|
+
render :success
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# POST /billing/checkout/lifetime?plan=price_xxx
|
|
31
|
+
# LTD purchase flow — same shape as #create but uses Stripe's
|
|
32
|
+
# `mode: "payment"`. The webhook handler distinguishes the two via
|
|
33
|
+
# session.metadata.access_type.
|
|
34
|
+
def lifetime
|
|
35
|
+
plan = Billing::Plan.active.find_by!(gateway_ref: params[:plan])
|
|
36
|
+
|
|
37
|
+
result = Billing::Lifetime::CreateLifetimeSessionService.call(
|
|
38
|
+
account_id: current_billing_account.id,
|
|
39
|
+
customer_ref: customer_ref_or_create!,
|
|
40
|
+
plan_ref: plan.gateway_ref,
|
|
41
|
+
success_url: billing.checkout_success_url,
|
|
42
|
+
cancel_url: billing.plans_url
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if result.ok?
|
|
46
|
+
redirect_to result.url, allow_other_host: true, status: :see_other
|
|
47
|
+
else
|
|
48
|
+
redirect_to billing.plans_path, alert: result.error
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Resolves the Stripe customer id for the current Account. The
|
|
55
|
+
# host must implement `current_billing_account` (or set it from
|
|
56
|
+
# a before_action) — typically `Accounts::Current.account`.
|
|
57
|
+
# Hosts pre-Wave-9 can override the entire method to use their
|
|
58
|
+
# own user-keyed lookup.
|
|
59
|
+
def customer_ref_or_create!
|
|
60
|
+
account = current_billing_account
|
|
61
|
+
raise Billing::Error, "current_billing_account is not set" unless account
|
|
62
|
+
raise Billing::Error, "Account #{account.id} does not respond to stripe_customer_ref!" unless account.respond_to?(:stripe_customer_ref!)
|
|
63
|
+
|
|
64
|
+
account.stripe_customer_ref!(email: billing_contact_email_for(account))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Override in the host to point at the right contact email for
|
|
68
|
+
# the Stripe customer record. Default reads `current_user.email`
|
|
69
|
+
# if the host's auth concern wires one up — usually the right
|
|
70
|
+
# answer for B2C flows. B2B hosts often want the Account owner's
|
|
71
|
+
# email instead; override accordingly.
|
|
72
|
+
def billing_contact_email_for(_account)
|
|
73
|
+
return current_user.email_address if respond_to?(:current_user) && current_user.respond_to?(:email_address)
|
|
74
|
+
return current_user.email if respond_to?(:current_user) && current_user.respond_to?(:email)
|
|
75
|
+
|
|
76
|
+
raise Billing::Error,
|
|
77
|
+
"Override #billing_contact_email_for(account) to supply the Stripe customer email."
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Default implementation reads `Accounts::Current.account` if the
|
|
81
|
+
# accounts engine is installed. Override in the host if your
|
|
82
|
+
# Account is bound differently (e.g. via params, subdomain, etc).
|
|
83
|
+
def current_billing_account
|
|
84
|
+
return @current_billing_account if defined?(@current_billing_account)
|
|
85
|
+
|
|
86
|
+
@current_billing_account =
|
|
87
|
+
if defined?(Accounts::Current) && Accounts::Current.respond_to?(:account)
|
|
88
|
+
Accounts::Current.account
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Billing
|
|
4
|
+
# Read-only billing history for the current Account. The local
|
|
5
|
+
# Billing::Invoice rows are populated by the InvoiceHandlerBase
|
|
6
|
+
# webhook handlers as Stripe fires invoice.* events; SyncService
|
|
7
|
+
# can refresh on demand.
|
|
8
|
+
#
|
|
9
|
+
# No `download` action — Stripe hosts the PDF; link to the
|
|
10
|
+
# `hosted_invoice_url` Stripe returns on the invoice object. Saves
|
|
11
|
+
# us from being a redirect proxy for content we do not own.
|
|
12
|
+
#
|
|
13
|
+
# GET /billing/invoices → index (paginated history)
|
|
14
|
+
# GET /billing/invoices/:id → show
|
|
15
|
+
class InvoicesController < ApplicationController
|
|
16
|
+
before_action :require_invoice, only: %i[show]
|
|
17
|
+
|
|
18
|
+
def index
|
|
19
|
+
@invoices = scoped_invoices.order(created_at: :desc)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def show
|
|
23
|
+
# @invoice is set by require_invoice. Hosts that want a fresh
|
|
24
|
+
# read from Stripe before rendering can opt in:
|
|
25
|
+
#
|
|
26
|
+
# Billing::Invoices::SyncService.call(invoice_ref: @invoice.gateway_ref)
|
|
27
|
+
#
|
|
28
|
+
# The default render is the local DB row — webhook lag is
|
|
29
|
+
# usually <1s, so this is good enough for almost every UI.
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def scoped_invoices
|
|
35
|
+
Billing::Invoice.where(customer_ref: current_billing_customer_ref)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def require_invoice
|
|
39
|
+
@invoice = scoped_invoices.find_by(id: params[:id])
|
|
40
|
+
return if @invoice
|
|
41
|
+
|
|
42
|
+
redirect_to invoices_path, alert: "Invoice not found."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def current_billing_customer_ref
|
|
46
|
+
account = current_billing_account
|
|
47
|
+
return nil unless account
|
|
48
|
+
|
|
49
|
+
account.billing_subscriptions.pick(:customer_ref) ||
|
|
50
|
+
account.billing_invoices.pick(:customer_ref) ||
|
|
51
|
+
account.billing_lifetime_passes.pick(:customer_ref)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def current_billing_account
|
|
55
|
+
return @current_billing_account if defined?(@current_billing_account)
|
|
56
|
+
|
|
57
|
+
@current_billing_account =
|
|
58
|
+
if defined?(Accounts::Current) && Accounts::Current.respond_to?(:account)
|
|
59
|
+
Accounts::Current.account
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Billing
|
|
4
|
+
class PlansController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
@recurring_plans = Billing::Plan.active.recurring.order(:amount_cents)
|
|
7
|
+
# LTD plans grouped separately on the pricing page so the host
|
|
8
|
+
# can render them under a "buy once, own forever" section.
|
|
9
|
+
# `lifetime_inventory_remaining` is nil for unlimited; integer
|
|
10
|
+
# for capped (used by the view to show "X seats left").
|
|
11
|
+
@lifetime_plans = Billing::Plan.active.lifetime.order(:amount_cents)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Billing
|
|
4
|
+
class PortalController < ApplicationController
|
|
5
|
+
# POST /billing/portal
|
|
6
|
+
def create
|
|
7
|
+
account = current_billing_account
|
|
8
|
+
raise Billing::Error, "current_billing_account is not set" unless account
|
|
9
|
+
|
|
10
|
+
result = Billing::Portal::CreateSessionService.call(
|
|
11
|
+
customer_ref: account_customer_ref(account),
|
|
12
|
+
return_url: billing.plans_url
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
if result.ok?
|
|
16
|
+
redirect_to result.url, allow_other_host: true, status: :see_other
|
|
17
|
+
else
|
|
18
|
+
redirect_to billing.plans_path, alert: result.error
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# Reads the Stripe customer id off any existing billing row for
|
|
25
|
+
# this Account. Avoids a synchronous Stripe API call — the
|
|
26
|
+
# portal is only meaningful when the Account already has a
|
|
27
|
+
# subscription anyway.
|
|
28
|
+
def account_customer_ref(account)
|
|
29
|
+
account.billing_subscriptions.pick(:customer_ref) ||
|
|
30
|
+
account.billing_invoices.pick(:customer_ref) ||
|
|
31
|
+
account.billing_lifetime_passes.pick(:customer_ref) ||
|
|
32
|
+
raise(Billing::Error, "Account has no billing customer_ref yet — create a subscription or LTD first.")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# See CheckoutController#current_billing_account — same default.
|
|
36
|
+
def current_billing_account
|
|
37
|
+
return @current_billing_account if defined?(@current_billing_account)
|
|
38
|
+
|
|
39
|
+
@current_billing_account =
|
|
40
|
+
if defined?(Accounts::Current) && Accounts::Current.respond_to?(:account)
|
|
41
|
+
Accounts::Current.account
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|