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,269 @@
|
|
|
1
|
+
# Notifications
|
|
2
|
+
|
|
3
|
+
> In-app + email + SMS notifications, scheduled or recurring, for a
|
|
4
|
+
> Seams-powered host. STI delivery strategies, ice_cube schedules,
|
|
5
|
+
> swappable adapters.
|
|
6
|
+
|
|
7
|
+
**Requires:** soft requirements only.
|
|
8
|
+
- The `auth` engine: `Notifications::AuthSubscriber.owner_class_name`
|
|
9
|
+
resolves the recipient via `Auth::Identity` by default.
|
|
10
|
+
- The `accounts` and/or `billing` engines: the `BillingSubscriber`
|
|
11
|
+
resolves the recipient via `Billing.configuration.billable_class`
|
|
12
|
+
(default `"Accounts::Account"`). Without billing the subscriber
|
|
13
|
+
attaches no handlers; without accounts, set `billable_class` to
|
|
14
|
+
whatever class the host uses for the billing recipient.
|
|
15
|
+
|
|
16
|
+
## Model
|
|
17
|
+
|
|
18
|
+
`Notifications::Notification` is an STI base. Three concrete
|
|
19
|
+
subclasses, each implementing its own `#dispatch!`:
|
|
20
|
+
|
|
21
|
+
| Class | Channel | Dispatches via |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `Notifications::Strategies::InApp` | in-app | ActionCable broadcast to the per-recipient channel |
|
|
24
|
+
| `Notifications::Strategies::Email` | email | `Notifications.email_adapter.deliver(notification:)` |
|
|
25
|
+
| `Notifications::Strategies::Sms` | sms | `Notifications.sms_adapter.deliver(notification:)` |
|
|
26
|
+
|
|
27
|
+
Every Notification belongs to an `owner` (polymorphic), names a
|
|
28
|
+
`template` (an ERB filename), and carries an ice_cube schedule
|
|
29
|
+
serialised to a `schedule_data` jsonb column. `next_delivery_at` is
|
|
30
|
+
the indexed cache the recurring sweeper reads from.
|
|
31
|
+
|
|
32
|
+
### Polymorphic owner
|
|
33
|
+
|
|
34
|
+
The Notification's `owner` is polymorphic — an `owner_type` /
|
|
35
|
+
`owner_id` pair. After Wave 9 the canonical "human" is
|
|
36
|
+
`Auth::Identity`, but **any** ActiveRecord model can own
|
|
37
|
+
notifications: an Account (the tenant), a Membership (Identity in
|
|
38
|
+
Account), or a host-defined model. Hosts mix and match without any
|
|
39
|
+
schema changes:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
Notifications::Notification.create!(owner: identity, template: "welcome")
|
|
43
|
+
Notifications::Notification.create!(owner: account, template: "billing/invoice_paid")
|
|
44
|
+
Notifications::Notification.create!(owner: membership, template: "team/role_changed")
|
|
45
|
+
Notifications::Notification.create!(owner: project, template: "project/deadline")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Scheduling
|
|
49
|
+
|
|
50
|
+
The examples below assume the host has included
|
|
51
|
+
`Notifications::Notifiable` on `Auth::Identity` (or another model)
|
|
52
|
+
to pick up the `#notify` helper — see "Notifiable is optional"
|
|
53
|
+
below. Hosts that skip the concern call
|
|
54
|
+
`Notifications::Notification.create!(owner: ..., template: ...)`
|
|
55
|
+
directly.
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# Send right now (default if you skip schedule_config)
|
|
59
|
+
identity.notify(strategy: :email, template: "welcome")
|
|
60
|
+
|
|
61
|
+
# Send in 24h
|
|
62
|
+
identity.notify(
|
|
63
|
+
strategy: :email,
|
|
64
|
+
template: "trial_ending",
|
|
65
|
+
schedule_config: { starts_at: 1.day.from_now, frequency: "once" }
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Weekly digest
|
|
69
|
+
identity.notify(
|
|
70
|
+
strategy: :email,
|
|
71
|
+
template: "weekly_digest",
|
|
72
|
+
schedule_config: {
|
|
73
|
+
starts_at: Time.current.next_week,
|
|
74
|
+
frequency: "weekly",
|
|
75
|
+
interval: 1
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Monthly, capped at 12 occurrences
|
|
80
|
+
identity.notify(
|
|
81
|
+
strategy: :email,
|
|
82
|
+
template: "anniversary",
|
|
83
|
+
schedule_config: {
|
|
84
|
+
starts_at: Time.current,
|
|
85
|
+
frequency: "monthly",
|
|
86
|
+
count: 12
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Or assemble the schedule yourself for richer rules (exception dates,
|
|
92
|
+
"first Tuesday of the month", etc.):
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
sched = IceCube::Schedule.new(Time.current)
|
|
96
|
+
sched.add_recurrence_rule(
|
|
97
|
+
IceCube::Rule.weekly.day(:monday).hour_of_day(9)
|
|
98
|
+
)
|
|
99
|
+
notification = identity.notifications.create!(
|
|
100
|
+
type: "Notifications::Strategies::Email",
|
|
101
|
+
template: "monday_morning"
|
|
102
|
+
)
|
|
103
|
+
notification.schedule = sched
|
|
104
|
+
notification.save!
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Sweeper
|
|
108
|
+
|
|
109
|
+
`Notifications::SendDueNotificationsJob` is a plain `ApplicationJob`
|
|
110
|
+
that finds every `Notification.due` and enqueues a per-row
|
|
111
|
+
`SendNotificationJob`. Wire it into your queue's recurring
|
|
112
|
+
scheduler. With Rails 8's Solid Queue:
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
# config/recurring.yml
|
|
116
|
+
production:
|
|
117
|
+
notifications_dispatcher:
|
|
118
|
+
class: Notifications::SendDueNotificationsJob
|
|
119
|
+
schedule: every minute
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Events emitted
|
|
123
|
+
|
|
124
|
+
| Event name | Payload | Emitted when |
|
|
125
|
+
| --- | --- | --- |
|
|
126
|
+
| `notification.queued.notifications` | `{ id:, type:, owner_type:, owner_id: }` | `Notification#send!` begins |
|
|
127
|
+
| `notification.delivered.notifications` | `{ id:, type:, owner_type:, owner_id: }` | `dispatch!` succeeded + Delivery recorded |
|
|
128
|
+
| `notification.failed.notifications` | `{ id:, type:, error: }` | `dispatch!` raised |
|
|
129
|
+
|
|
130
|
+
Owner reference uses `owner_type` + `owner_id` (the polymorphic
|
|
131
|
+
columns) — Notifications are addressed at any model, so subscribers
|
|
132
|
+
need both halves to resolve a recipient.
|
|
133
|
+
|
|
134
|
+
## Events consumed
|
|
135
|
+
|
|
136
|
+
| Event name | Subscriber | What it does |
|
|
137
|
+
| --- | --- | --- |
|
|
138
|
+
| `identity.signed_up.auth` | `Notifications::AuthSubscriber` | Creates an InApp + Email welcome notification owned by the Auth::Identity (subject to NotificationPreference). |
|
|
139
|
+
| `subscription.created.billing` | `Notifications::BillingSubscriber` | InApp notification, template `billing/subscription_started`. |
|
|
140
|
+
| `subscription.updated.billing` | `Notifications::BillingSubscriber` | InApp notification, template `billing/subscription_updated`. |
|
|
141
|
+
| `subscription.canceled.billing` | `Notifications::BillingSubscriber` | InApp notification, template `billing/subscription_canceled`. |
|
|
142
|
+
| `invoice.paid.billing` | `Notifications::BillingSubscriber` | InApp notification, template `billing/invoice_paid`. |
|
|
143
|
+
| `invoice.failed.billing` | `Notifications::BillingSubscriber` | InApp notification, template `billing/invoice_failed`. |
|
|
144
|
+
| `lifetime.granted.billing` | `Notifications::BillingSubscriber` | InApp notification, template `billing/lifetime_granted`. |
|
|
145
|
+
| `lifetime.purchased.billing` | `Notifications::BillingSubscriber` | InApp notification, template `billing/lifetime_purchased`. |
|
|
146
|
+
|
|
147
|
+
The Billing subscriber resolves the recipient by reading
|
|
148
|
+
`Billing.configuration.billable_class` (default `Accounts::Account`)
|
|
149
|
+
and looking up the row by `account_id` carried on the billing
|
|
150
|
+
event payload. It only attaches when `Billing::Engine` is loaded.
|
|
151
|
+
Hosts that want a different recipient (e.g. a domain User on top
|
|
152
|
+
of `Auth::Identity`) override `Billing.configuration.billable_class`
|
|
153
|
+
in their initializer.
|
|
154
|
+
|
|
155
|
+
### Cross-engine dependencies
|
|
156
|
+
|
|
157
|
+
The subscribers in this engine resolve owners by reaching into other
|
|
158
|
+
engines' models. This dependency direction is intentional but worth
|
|
159
|
+
documenting explicitly:
|
|
160
|
+
|
|
161
|
+
| Subscriber | Reaches into | Why |
|
|
162
|
+
| --- | --- | --- |
|
|
163
|
+
| `Notifications::AuthSubscriber` | `Auth::Identity` | resolve owner from `identity_id` payload (the human who just signed up) |
|
|
164
|
+
| `Notifications::BillingSubscriber` | `Billing.configuration.billable_class` (default `Accounts::Account`) | resolve owner from `account_id` payload — the tenant the subscription / invoice / lifetime grant belongs to |
|
|
165
|
+
| `Notifications::CreateNotificationJob` | any AR class | resolve owner via `owner_class.constantize.find_by(id: owner_id)` |
|
|
166
|
+
|
|
167
|
+
If you want a different owner for the welcome notification (an
|
|
168
|
+
Account, a Membership, a host User), copy
|
|
169
|
+
`engines/notifications/app/subscribers/notifications/auth_subscriber.rb`
|
|
170
|
+
into your host, change `OWNER_CLASS_NAME`, then re-attach in
|
|
171
|
+
`config/initializers/notifications.rb`. The Notification table is
|
|
172
|
+
fully polymorphic — no schema changes needed.
|
|
173
|
+
|
|
174
|
+
### Subscribers enqueue, never block the publisher
|
|
175
|
+
|
|
176
|
+
`Seams::Events::Publisher` runs subscribers synchronously in the
|
|
177
|
+
publisher's thread. Every subscriber here therefore enqueues
|
|
178
|
+
`Notifications::CreateNotificationJob` (which does the actual DB
|
|
179
|
+
write + send) rather than calling `Notification.create!` inline.
|
|
180
|
+
Publishing should never wait on the bus.
|
|
181
|
+
|
|
182
|
+
## Exposed concerns
|
|
183
|
+
|
|
184
|
+
| Concern | Purpose |
|
|
185
|
+
| --- | --- |
|
|
186
|
+
| `Notifications::Notifiable` | OPTIONAL. Mix into any model that should expose `notifications` + `#notify(strategy:, template:, schedule_config:)` sugar. |
|
|
187
|
+
|
|
188
|
+
### Notifiable is optional
|
|
189
|
+
|
|
190
|
+
The `Notifications::Notification` row is polymorphic — every record
|
|
191
|
+
has an `owner_type` / `owner_id` pair, and `create!(owner: anything, template: ...)`
|
|
192
|
+
works with any ActiveRecord model. The `Notifiable` concern is just
|
|
193
|
+
sugar for the receiving side; you don't need it.
|
|
194
|
+
|
|
195
|
+
Three include patterns hosts can pick from:
|
|
196
|
+
|
|
197
|
+
1. **Wire onto `Auth::Identity`** (canonical post-Wave-9 host —
|
|
198
|
+
the "human" is `Auth::Identity`, no host User):
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
# config/initializers/notifications.rb
|
|
202
|
+
Rails.application.config.to_prepare do
|
|
203
|
+
Auth::Identity.include(Notifications::Notifiable)
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
2. **Wire onto a host User class** (hosts that keep their own User
|
|
208
|
+
alongside `Auth::Identity`):
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
class User < ApplicationRecord
|
|
212
|
+
include Notifications::Notifiable
|
|
213
|
+
end
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
3. **Don't include the concern at all.** Use
|
|
217
|
+
`Notifications::Notification.create!(owner: ..., template: ...)`
|
|
218
|
+
directly — the polymorphic owner column accepts any AR record.
|
|
219
|
+
|
|
220
|
+
Hosts including the concern on a non-Identity class (an Account, a
|
|
221
|
+
host User keyed by a different `identity_id`, etc.) should override
|
|
222
|
+
`#notification_preference_identity_id` so preference lookups key off
|
|
223
|
+
the right Identity.
|
|
224
|
+
|
|
225
|
+
## Adapters
|
|
226
|
+
|
|
227
|
+
| Interface | Default | Override via |
|
|
228
|
+
| --- | --- | --- |
|
|
229
|
+
| Email | `Notifications::Adapters::ActionMailer` | `Notifications.configure { \|c\| c.email_adapter = "MyApp::MailgunAdapter" }` |
|
|
230
|
+
| SMS | `Notifications::Adapters::NullSms` | `Notifications.configure { \|c\| c.sms_adapter = "MyApp::TwilioAdapter" }` |
|
|
231
|
+
|
|
232
|
+
To add a new adapter, subclass `Notifications::Adapters::Abstract`
|
|
233
|
+
and implement `#deliver(notification:)`. The adapter receives the
|
|
234
|
+
full Notification so it can read `recipient`, `template`,
|
|
235
|
+
`rendered_content`, `owner` — whatever the gateway needs.
|
|
236
|
+
|
|
237
|
+
## Templates
|
|
238
|
+
|
|
239
|
+
Notifications are rendered via ERB files looked up in this order:
|
|
240
|
+
|
|
241
|
+
1. `app/views/notifications/templates/<name>.erb` in the host
|
|
242
|
+
2. `app/views/notifications/templates/<name>.erb` in the engine
|
|
243
|
+
|
|
244
|
+
Drop a file in your host to override. The notification is exposed in
|
|
245
|
+
the template via the local variable `notification` — use
|
|
246
|
+
`notification.owner`, `notification.recipient`, etc.
|
|
247
|
+
|
|
248
|
+
## Preferences
|
|
249
|
+
|
|
250
|
+
`Notifications::NotificationPreference` lets an Identity opt out by
|
|
251
|
+
channel + notification_type. The table keys off `identity_id` (not
|
|
252
|
+
the polymorphic Notification owner) — channel preferences live with
|
|
253
|
+
the human, not with whatever model a notification happens to be
|
|
254
|
+
addressed at:
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
Notifications::NotificationPreference.enabled?(
|
|
258
|
+
identity_id: 42, channel: "email", notification_type: "weekly_digest"
|
|
259
|
+
) # => true (default) | false (if a row says enabled: false)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
The shipped `AuthSubscriber` consults this before creating the
|
|
263
|
+
Email Notification at signup.
|
|
264
|
+
|
|
265
|
+
## Running the specs
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
bin/rails seams:test[notifications]
|
|
269
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notifications
|
|
4
|
+
# Per-recipient ActionCable channel. Server-side code can broadcast
|
|
5
|
+
# a Turbo Stream to this channel when a new notification is created,
|
|
6
|
+
# so the bell icon updates in real time.
|
|
7
|
+
#
|
|
8
|
+
# Notifications::NotificationChannel.broadcast_to(
|
|
9
|
+
# identity, { unread_count: identity.notifications.unread.count }
|
|
10
|
+
# )
|
|
11
|
+
#
|
|
12
|
+
# Post-Wave-9 the canonical recipient is `Auth::Current.identity`.
|
|
13
|
+
# Hosts that keep a domain User on top of Auth::Identity can
|
|
14
|
+
# override `current_recipient` to point at that User instead.
|
|
15
|
+
class NotificationChannel < ActionCable::Channel::Base
|
|
16
|
+
def subscribed
|
|
17
|
+
return reject unless current_recipient
|
|
18
|
+
|
|
19
|
+
stream_for current_recipient
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# Resolves the recipient for the WebSocket connection. Tries
|
|
25
|
+
# `connection.current_identity` (the Wave-9 default exposed by
|
|
26
|
+
# `Auth::Authentication`) first, then falls back to
|
|
27
|
+
# `connection.current_user` for hosts that maintain a User model
|
|
28
|
+
# on top of Auth::Identity.
|
|
29
|
+
def current_recipient
|
|
30
|
+
return connection.current_identity if connection.respond_to?(:current_identity)
|
|
31
|
+
return connection.current_user if connection.respond_to?(:current_user)
|
|
32
|
+
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/generators/seams/notifications/templates/app/controllers/notifications_controller.rb.tt
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notifications
|
|
4
|
+
class NotificationsController < ApplicationController
|
|
5
|
+
before_action :set_notification, only: %i[show mark_as_read]
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@notifications = current_recipient
|
|
9
|
+
&.notifications
|
|
10
|
+
&.where(type: "Notifications::Strategies::InApp")
|
|
11
|
+
&.recent
|
|
12
|
+
&.limit(50) || []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def show
|
|
16
|
+
@notification.mark_as_read!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def mark_as_read
|
|
20
|
+
@notification.mark_as_read!
|
|
21
|
+
respond_to do |format|
|
|
22
|
+
format.html { redirect_to notifications_path }
|
|
23
|
+
format.turbo_stream
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def mark_all_as_read
|
|
28
|
+
current_recipient
|
|
29
|
+
&.notifications
|
|
30
|
+
&.where(type: "Notifications::Strategies::InApp")
|
|
31
|
+
&.unread
|
|
32
|
+
&.update_all(read_at: Time.current)
|
|
33
|
+
redirect_to notifications_path, notice: "All caught up"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def set_notification
|
|
39
|
+
@notification = current_recipient&.notifications&.find(params[:id])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Resolves the recipient whose notifications this controller
|
|
43
|
+
# exposes. Post-Wave-9 the canonical recipient is
|
|
44
|
+
# `Auth::Current.identity` (the signed-in human). Hosts that keep
|
|
45
|
+
# a domain User on top of Auth::Identity can override
|
|
46
|
+
# `current_recipient` (or expose `current_user` from their auth
|
|
47
|
+
# concern) to point at that User instead — the legacy
|
|
48
|
+
# `respond_to?(:current_user)` fallback below preserves Wave-8
|
|
49
|
+
# behaviour for hosts that haven't migrated.
|
|
50
|
+
def current_recipient
|
|
51
|
+
if defined?(Auth::Current) && Auth::Current.respond_to?(:identity) && Auth::Current.identity
|
|
52
|
+
return Auth::Current.identity
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
respond_to?(:current_user) ? current_user : nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/generators/seams/notifications/templates/app/controllers/preferences_controller.rb.tt
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notifications
|
|
4
|
+
class PreferencesController < ApplicationController
|
|
5
|
+
def show
|
|
6
|
+
@preferences = Notifications::NotificationPreference.where(identity_id: current_identity_id)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def update
|
|
10
|
+
# Whitelist the keys we accept. The form posts
|
|
11
|
+
# `preferences[<channel>:<type>] = "1" | "0"`. Allowed channels
|
|
12
|
+
# are the canonical CHANNELS list; type is either "any" (→ nil)
|
|
13
|
+
# or a NotificationPreference::TYPE-style identifier ([a-z0-9_]).
|
|
14
|
+
preference_params.each do |key, enabled|
|
|
15
|
+
channel, type = key.split(":", 2)
|
|
16
|
+
next unless Notifications::NotificationPreference::CHANNELS.include?(channel)
|
|
17
|
+
next unless type.nil? || type == "any" || type.match?(/\A[a-z0-9_]+\z/)
|
|
18
|
+
|
|
19
|
+
type = nil if type == "any"
|
|
20
|
+
pref = Notifications::NotificationPreference.find_or_initialize_by(
|
|
21
|
+
identity_id: current_identity_id,
|
|
22
|
+
channel: channel,
|
|
23
|
+
notification_type: type
|
|
24
|
+
)
|
|
25
|
+
pref.update!(enabled: enabled.to_s == "1")
|
|
26
|
+
end
|
|
27
|
+
redirect_to preferences_path, notice: "Preferences saved"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# `params[:preferences].to_h` raises ActionController::UnfilteredParameters
|
|
33
|
+
# in Rails default config. Use permit! after filtering to the
|
|
34
|
+
# `:preferences` key — the per-key channel/type validation above
|
|
35
|
+
# is the real safety net.
|
|
36
|
+
def preference_params
|
|
37
|
+
raw = params.require(:preferences)
|
|
38
|
+
raw.respond_to?(:permit!) ? raw.permit!.to_h : raw.to_h
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Resolves the signed-in identity's id from `Auth::Current.identity`
|
|
42
|
+
# (the Auth engine's per-request namespace). Gated on
|
|
43
|
+
# `defined?(Auth::Current)` so the controller is safe in hosts
|
|
44
|
+
# that don't ship the auth engine. Override in your host if you
|
|
45
|
+
# wire authentication differently.
|
|
46
|
+
def current_identity_id
|
|
47
|
+
if defined?(Auth::Current) && Auth::Current.respond_to?(:identity) && Auth::Current.identity
|
|
48
|
+
return Auth::Current.identity.id
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Notification bell. Subscribes to the per-user ActionCable channel
|
|
4
|
+
// (if available) and updates the unread count badge in real time.
|
|
5
|
+
//
|
|
6
|
+
// Drop-in: add `data-controller="notification-bell"` to the bell
|
|
7
|
+
// element. Pair with `data-notification-bell-target="count"` on the
|
|
8
|
+
// element that shows the unread number.
|
|
9
|
+
export default class extends Controller {
|
|
10
|
+
static targets = ["count"]
|
|
11
|
+
static values = { count: Number }
|
|
12
|
+
|
|
13
|
+
connect() {
|
|
14
|
+
if (typeof window.consumer === "undefined") return
|
|
15
|
+
|
|
16
|
+
this.subscription = window.consumer.subscriptions.create(
|
|
17
|
+
{ channel: "Notifications::NotificationChannel" },
|
|
18
|
+
{ received: (data) => this.update(data) }
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
disconnect() {
|
|
23
|
+
this.subscription?.unsubscribe()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
update({ unread_count }) {
|
|
27
|
+
if (typeof unread_count !== "number") return
|
|
28
|
+
this.countValue = unread_count
|
|
29
|
+
if (this.hasCountTarget) {
|
|
30
|
+
this.countTarget.textContent = unread_count
|
|
31
|
+
this.countTarget.style.display = unread_count > 0 ? "" : "none"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notifications
|
|
4
|
+
# Creates a Notification row and enqueues its send. Subscribers
|
|
5
|
+
# enqueue this job rather than doing the DB write inline — see
|
|
6
|
+
# Seams::Events::Publisher (subscribers run synchronously in the
|
|
7
|
+
# publisher's thread; never block the publisher).
|
|
8
|
+
class CreateNotificationJob < ApplicationJob
|
|
9
|
+
queue_as :notifications
|
|
10
|
+
|
|
11
|
+
STRATEGIES = {
|
|
12
|
+
"in_app" => "Notifications::Strategies::InApp",
|
|
13
|
+
"email" => "Notifications::Strategies::Email",
|
|
14
|
+
"sms" => "Notifications::Strategies::Sms"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def perform(owner_class:, owner_id:, template:, strategy:)
|
|
18
|
+
klass_name = STRATEGIES.fetch(strategy.to_s) do
|
|
19
|
+
raise ArgumentError, "Unknown strategy: #{strategy.inspect}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
owner = owner_class.constantize.find_by(id: owner_id)
|
|
23
|
+
return unless owner
|
|
24
|
+
|
|
25
|
+
notif = klass_name.constantize.new(owner: owner, template: template)
|
|
26
|
+
notif.schedule = IceCube::Schedule.new(Time.current)
|
|
27
|
+
notif.save!
|
|
28
|
+
notif.send_async
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notifications
|
|
4
|
+
# Recurring sweeper. Finds every Notification whose +next_delivery_at+
|
|
5
|
+
# has passed and enqueues a +SendNotificationJob+ for each. Per-row
|
|
6
|
+
# jobs let each notification retry / rate-limit independently.
|
|
7
|
+
#
|
|
8
|
+
# Wire into your queue's recurring scheduler. With Rails 8's Solid
|
|
9
|
+
# Queue, add to config/recurring.yml:
|
|
10
|
+
#
|
|
11
|
+
# production:
|
|
12
|
+
# notifications_dispatcher:
|
|
13
|
+
# class: Notifications::SendDueNotificationsJob
|
|
14
|
+
# schedule: every minute
|
|
15
|
+
class SendDueNotificationsJob < ApplicationJob
|
|
16
|
+
queue_as :notifications
|
|
17
|
+
|
|
18
|
+
def perform
|
|
19
|
+
Notifications::Notification.due.find_each(&:send_async)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notifications
|
|
4
|
+
# Sends a single Notification by id. Uses find_by so a row deleted
|
|
5
|
+
# between enqueue and execution silently no-ops instead of raising.
|
|
6
|
+
class SendNotificationJob < ApplicationJob
|
|
7
|
+
queue_as :notifications
|
|
8
|
+
|
|
9
|
+
def perform(notification_id)
|
|
10
|
+
Notifications::Notification.find_by(id: notification_id)&.send!
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notifications
|
|
4
|
+
# Engine-scoped mailer parent so NotificationMailer doesn't reach
|
|
5
|
+
# into the host's ::ApplicationMailer at autoload time. Hosts that
|
|
6
|
+
# want a layout add `layout "mailer"` here (or per mailer); we don't
|
|
7
|
+
# ship one because the dummy app has no `app/views/layouts/mailer.*`
|
|
8
|
+
# to render and forcing one would crash dummy specs.
|
|
9
|
+
class ApplicationMailer < ::ApplicationMailer
|
|
10
|
+
default from: -> { Notifications.configuration.default_from }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notifications
|
|
4
|
+
# Single mailer used by Notifications::Adapters::ActionMailer.
|
|
5
|
+
# Renders multipart/alternative (text + HTML) when both formats are
|
|
6
|
+
# present in the host/engine template lookup chain. Falls back to a
|
|
7
|
+
# single-part text email when only the text template exists.
|
|
8
|
+
class NotificationMailer < ApplicationMailer
|
|
9
|
+
def notify(notification)
|
|
10
|
+
@notification = notification
|
|
11
|
+
text_body = notification.rendered_content(format: :text)
|
|
12
|
+
html_body =
|
|
13
|
+
if notification.template_exists?(format: :html)
|
|
14
|
+
notification.rendered_content(format: :html)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
mail(to: notification.recipient, subject: notification.template.to_s.titleize) do |format|
|
|
18
|
+
format.text { render plain: text_body, layout: "notifications/mailer" }
|
|
19
|
+
format.html { render html: html_body.html_safe, layout: "notifications/mailer" } if html_body
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notifications
|
|
4
|
+
# One row per successful +Notification#send!+. Useful audit trail
|
|
5
|
+
# and the basis for "when did we last send?" queries that don't
|
|
6
|
+
# need to load the gateway.
|
|
7
|
+
class Delivery < ApplicationRecord
|
|
8
|
+
self.table_name = "notification_deliveries"
|
|
9
|
+
|
|
10
|
+
belongs_to :notification, class_name: "Notifications::Notification",
|
|
11
|
+
inverse_of: :deliveries
|
|
12
|
+
end
|
|
13
|
+
end
|