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,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notifications
|
|
4
|
+
# Represents a scheduled notification belonging to an +owner+.
|
|
5
|
+
#
|
|
6
|
+
# Concrete delivery types are STI subclasses under
|
|
7
|
+
# +Notifications::Strategies+ — +InApp+, +Email+, +Sms+ — each
|
|
8
|
+
# implementing its own +#dispatch!+. They share this base's schema,
|
|
9
|
+
# scheduling, and audit logic.
|
|
10
|
+
#
|
|
11
|
+
# Scheduling is delegated to ice_cube. The schedule is persisted as
|
|
12
|
+
# a json hash via +schedule_data+ and exposed through
|
|
13
|
+
# +schedule+/+schedule=+. +next_delivery_at+ is an indexed cache so
|
|
14
|
+
# the recurring sweeper hits one query.
|
|
15
|
+
class Notification < ApplicationRecord
|
|
16
|
+
self.table_name = "notifications"
|
|
17
|
+
|
|
18
|
+
belongs_to :owner, polymorphic: true
|
|
19
|
+
has_many :deliveries, class_name: "Notifications::Delivery",
|
|
20
|
+
dependent: :destroy, inverse_of: :notification
|
|
21
|
+
|
|
22
|
+
# Concrete STI subclasses permitted for creation. Anything else
|
|
23
|
+
# fails validation rather than booting an unknown subclass.
|
|
24
|
+
PERMITTED_TYPES = [
|
|
25
|
+
"Notifications::Strategies::InApp",
|
|
26
|
+
"Notifications::Strategies::Email",
|
|
27
|
+
"Notifications::Strategies::Sms"
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
FREQUENCIES = %w[once daily weekly monthly].freeze
|
|
31
|
+
|
|
32
|
+
# Templates resolve to file reads via ERB. Lock the value down so a
|
|
33
|
+
# row can't reach outside the templates dir or evaluate arbitrary
|
|
34
|
+
# files. Allowed: lowercase letters, digits, underscores, dashes,
|
|
35
|
+
# forward slashes (so subdirs like "billing/invoice_paid" work).
|
|
36
|
+
# No leading slash, no `..`, no consecutive slashes.
|
|
37
|
+
TEMPLATE_NAME_PATTERN = %r{\A[a-z0-9_\-]+(?:/[a-z0-9_\-]+)*\z}
|
|
38
|
+
|
|
39
|
+
validates :type, inclusion: { in: PERMITTED_TYPES }
|
|
40
|
+
validates :template, presence: true,
|
|
41
|
+
format: { with: TEMPLATE_NAME_PATTERN, message: "must be a slash-separated lowercase identifier" }
|
|
42
|
+
|
|
43
|
+
scope :due, -> { where(next_delivery_at: ..Time.current) }
|
|
44
|
+
scope :pending, -> { where.not(next_delivery_at: nil) }
|
|
45
|
+
scope :completed, -> { where(next_delivery_at: nil) }
|
|
46
|
+
scope :unread, -> { where(read_at: nil) }
|
|
47
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
48
|
+
|
|
49
|
+
# Accept short type names ("Email", "Sms", "InApp") in API params
|
|
50
|
+
# and normalise to fully-qualified class names. Falls back to the
|
|
51
|
+
# base class so the inclusion validation returns 422 instead of
|
|
52
|
+
# raising a 500.
|
|
53
|
+
def self.find_sti_class(type_name)
|
|
54
|
+
super(expand_type(type_name))
|
|
55
|
+
rescue ::ActiveRecord::SubclassNotFound
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.expand_type(type_name)
|
|
60
|
+
PERMITTED_TYPES.find { |t| t.demodulize == type_name } || type_name
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def type=(value)
|
|
64
|
+
super(self.class.expand_type(value.to_s))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Deserialises +schedule_data+ into an IceCube::Schedule instance.
|
|
68
|
+
def schedule
|
|
69
|
+
return nil if schedule_data.blank?
|
|
70
|
+
|
|
71
|
+
IceCube::Schedule.from_hash(schedule_data)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Assigns an IceCube::Schedule, persisting it as json and setting
|
|
75
|
+
# +next_delivery_at+ to the schedule's first occurrence.
|
|
76
|
+
def schedule=(sched)
|
|
77
|
+
self.schedule_data = sched&.to_hash
|
|
78
|
+
self.next_delivery_at = sched&.first
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Form-friendly schedule constructor. Accepts:
|
|
82
|
+
# { starts_at:, frequency: once|daily|weekly|monthly,
|
|
83
|
+
# interval: 1, count: 5, until: "2026-12-31" }
|
|
84
|
+
# so host code never has to import ice_cube.
|
|
85
|
+
def schedule_config=(config)
|
|
86
|
+
return if config.blank?
|
|
87
|
+
|
|
88
|
+
sched = IceCube::Schedule.new(Time.zone.parse(config[:starts_at].to_s))
|
|
89
|
+
rule = build_recurrence_rule(config)
|
|
90
|
+
sched.add_recurrence_rule(rule) if rule
|
|
91
|
+
self.schedule = sched
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Renders the ERB template named by +template+ in the requested
|
|
95
|
+
# +format+ (`:text` (default) or `:html`). Looks first in the host's
|
|
96
|
+
# app/views/notifications/templates/<name>.<format>.erb, then falls
|
|
97
|
+
# back to the engine's bundled templates. Hosts override by
|
|
98
|
+
# dropping a file. Raises Notifications::Error if no template
|
|
99
|
+
# exists at the requested format — callers that want to probe
|
|
100
|
+
# without raising should use #template_exists?(format:).
|
|
101
|
+
def rendered_content(format: :text)
|
|
102
|
+
path = find_template_path(format: format) ||
|
|
103
|
+
raise(Notifications::Error,
|
|
104
|
+
"Template #{template.inspect} (#{format}) not found under any allowed root")
|
|
105
|
+
|
|
106
|
+
ERB.new(File.read(path)).result_with_hash(notification: self)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def template_exists?(format: :text)
|
|
110
|
+
!find_template_path(format: format).nil?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def due?
|
|
114
|
+
next_delivery_at.present? && next_delivery_at <= Time.current
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def read?
|
|
118
|
+
read_at.present?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def mark_as_read!
|
|
122
|
+
update!(read_at: Time.current) unless read?
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def send_async
|
|
126
|
+
Notifications::SendNotificationJob.perform_later(id)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Synchronous delivery. Publishes lifecycle events OUTSIDE the
|
|
130
|
+
# transaction so subscribers don't run inside the AR transaction
|
|
131
|
+
# (a slow subscriber would hold table locks; a raise would roll
|
|
132
|
+
# back the delivery write).
|
|
133
|
+
# No-ops if not yet due (defensive against duplicate enqueue).
|
|
134
|
+
def send!
|
|
135
|
+
return unless due?
|
|
136
|
+
|
|
137
|
+
Seams::Events::Publisher.publish(
|
|
138
|
+
"notification.queued.notifications",
|
|
139
|
+
id: id, type: type, owner_type: owner_type, owner_id: owner_id
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
dispatch!
|
|
143
|
+
self.class.transaction do
|
|
144
|
+
deliveries.create!(sent_at: Time.current)
|
|
145
|
+
advance!
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
Seams::Events::Publisher.publish(
|
|
149
|
+
"notification.delivered.notifications",
|
|
150
|
+
id: id, type: type, owner_type: owner_type, owner_id: owner_id
|
|
151
|
+
)
|
|
152
|
+
rescue StandardError => e
|
|
153
|
+
Seams::Events::Publisher.publish(
|
|
154
|
+
"notification.failed.notifications", id: id, type: type, error: "#{e.class}: #{e.message}"
|
|
155
|
+
)
|
|
156
|
+
raise
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Advances +next_delivery_at+ to the next ice_cube occurrence
|
|
160
|
+
# after now. nil when no further occurrences exist (one-shots
|
|
161
|
+
# become "completed").
|
|
162
|
+
def advance!
|
|
163
|
+
update!(next_delivery_at: schedule&.next_occurrence)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
FREQUENCY_BUILDERS = {
|
|
167
|
+
"daily" => ->(interval) { IceCube::Rule.daily(interval) },
|
|
168
|
+
"weekly" => ->(interval) { IceCube::Rule.weekly(interval) },
|
|
169
|
+
"monthly" => ->(interval) { IceCube::Rule.monthly(interval) }
|
|
170
|
+
}.freeze
|
|
171
|
+
private_constant :FREQUENCY_BUILDERS
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def build_recurrence_rule(config)
|
|
176
|
+
builder = FREQUENCY_BUILDERS[config[:frequency].to_s]
|
|
177
|
+
return unless builder
|
|
178
|
+
|
|
179
|
+
interval = [config[:interval].to_i, 1].max
|
|
180
|
+
apply_rule_constraints(builder.call(interval), config)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def apply_rule_constraints(rule, config)
|
|
184
|
+
rule = rule.count(config[:count].to_i) if config[:count].present?
|
|
185
|
+
rule = rule.until(Time.zone.parse(config[:until].to_s)) if config[:until].present?
|
|
186
|
+
rule
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Each strategy implements its own dispatch.
|
|
190
|
+
def dispatch!
|
|
191
|
+
raise NotImplementedError, "#{self.class} must implement #dispatch!"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
PERMITTED_FORMATS = %i[text html].freeze
|
|
195
|
+
|
|
196
|
+
def find_template_path(format: :text)
|
|
197
|
+
raise ArgumentError, "format must be :text or :html" unless PERMITTED_FORMATS.include?(format)
|
|
198
|
+
|
|
199
|
+
# Realpath check — even though TEMPLATE_NAME_PATTERN already
|
|
200
|
+
# rejects "..", a defence-in-depth `realpath` confirms the
|
|
201
|
+
# resolved file lives under one of the two whitelisted roots.
|
|
202
|
+
host_root = Rails.root.join("app/views/notifications/templates")
|
|
203
|
+
engine_root = Notifications::Engine.root.join("app/views/notifications/templates")
|
|
204
|
+
|
|
205
|
+
[host_root, engine_root].each do |root|
|
|
206
|
+
candidate = root.join("#{template}.#{format}.erb")
|
|
207
|
+
next unless candidate.exist?
|
|
208
|
+
|
|
209
|
+
resolved = candidate.realpath
|
|
210
|
+
return resolved if resolved.to_s.start_with?(root.realpath.to_s)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# No template at this format. Caller decides whether to fall back
|
|
214
|
+
# (e.g. mailer trying :html then :text) or raise.
|
|
215
|
+
nil
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notifications
|
|
4
|
+
# Per-Identity channel + notification-type toggle. Absence of a row
|
|
5
|
+
# means "use defaults" (everything enabled). Presence with
|
|
6
|
+
# enabled: false means the Identity opted out.
|
|
7
|
+
#
|
|
8
|
+
# Keyed by `identity_id` (not the polymorphic Notification owner):
|
|
9
|
+
# channel preferences belong with the human, not with whatever
|
|
10
|
+
# other model a Notification happens to be addressed at (an
|
|
11
|
+
# Account, a Membership, a host-defined model).
|
|
12
|
+
class NotificationPreference < ApplicationRecord
|
|
13
|
+
self.table_name = "notification_preferences"
|
|
14
|
+
|
|
15
|
+
# Mirrors the keys of Notifications::Notifiable::STRATEGY_CLASSES
|
|
16
|
+
# but kept as plain strings here because the column stores strings.
|
|
17
|
+
CHANNELS = %w[in_app email sms].freeze
|
|
18
|
+
|
|
19
|
+
validates :identity_id, :channel, presence: true
|
|
20
|
+
validates :channel, inclusion: { in: CHANNELS }
|
|
21
|
+
validates :identity_id, uniqueness: { scope: %i[channel notification_type] }
|
|
22
|
+
|
|
23
|
+
def self.enabled?(identity_id:, channel:, notification_type: nil)
|
|
24
|
+
pref = find_by(identity_id: identity_id, channel: channel, notification_type: notification_type) ||
|
|
25
|
+
find_by(identity_id: identity_id, channel: channel, notification_type: nil)
|
|
26
|
+
pref ? pref.enabled : true
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notifications
|
|
4
|
+
module Strategies
|
|
5
|
+
# Email notification. +#dispatch!+ delegates to the configured
|
|
6
|
+
# email adapter, which ultimately renders the +template+ ERB
|
|
7
|
+
# against the owner and posts to the gateway (ActionMailer by
|
|
8
|
+
# default; swap with +Notifications.configuration.email_adapter+).
|
|
9
|
+
#
|
|
10
|
+
# The polymorphic +owner+ supplies the recipient address. We try
|
|
11
|
+
# +#email_address+ first (Rails 8's `has_secure_password` convention,
|
|
12
|
+
# used by some host User models), then +#email+ (Auth::Identity and
|
|
13
|
+
# most host User models). If the owner includes
|
|
14
|
+
# +Notifications::Notifiable+, +#email_notification_recipient+ wins.
|
|
15
|
+
# Hosts whose owner exposes a different attribute should override
|
|
16
|
+
# +#recipient+ on the strategy or set +recipient+ explicitly when
|
|
17
|
+
# creating the Notification.
|
|
18
|
+
class Email < Notification
|
|
19
|
+
def recipient
|
|
20
|
+
super.presence || resolve_email_from_owner
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def dispatch!
|
|
26
|
+
Notifications.email_adapter.deliver(notification: self)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def resolve_email_from_owner
|
|
30
|
+
return nil unless owner
|
|
31
|
+
|
|
32
|
+
owner.try(:email_notification_recipient) ||
|
|
33
|
+
owner.try(:email_address) ||
|
|
34
|
+
owner.try(:email)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notifications
|
|
4
|
+
module Strategies
|
|
5
|
+
# In-app notification. The +Notification+ row IS the delivery —
|
|
6
|
+
# the bell view renders pending rows for the recipient and
|
|
7
|
+
# +#mark_as_read!+ flips +read_at+. +#dispatch!+ broadcasts on
|
|
8
|
+
# the per-recipient ActionCable channel so connected clients
|
|
9
|
+
# update without polling. No-op when ActionCable isn't booted.
|
|
10
|
+
class InApp < Notification
|
|
11
|
+
def recipient
|
|
12
|
+
super.presence || owner&.id&.to_s
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def dispatch!
|
|
18
|
+
return unless defined?(Notifications::NotificationChannel) && defined?(::ActionCable)
|
|
19
|
+
|
|
20
|
+
Notifications::NotificationChannel.broadcast_to(
|
|
21
|
+
owner, id: id, template: template, body: rendered_content
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notifications
|
|
4
|
+
module Strategies
|
|
5
|
+
# SMS notification. +#dispatch!+ delegates to the configured SMS
|
|
6
|
+
# adapter (NullSms by default — logs and drops; swap with
|
|
7
|
+
# +Notifications.configuration.sms_adapter+).
|
|
8
|
+
#
|
|
9
|
+
# The polymorphic +owner+ supplies the recipient phone number.
|
|
10
|
+
# If the owner includes +Notifications::Notifiable+,
|
|
11
|
+
# +#sms_notification_recipient+ wins; otherwise we fall back to
|
|
12
|
+
# +#phone+. Hosts whose owner exposes a different attribute
|
|
13
|
+
# should override +#recipient+ on the strategy or set +recipient+
|
|
14
|
+
# explicitly when creating the Notification.
|
|
15
|
+
class Sms < Notification
|
|
16
|
+
def recipient
|
|
17
|
+
super.presence || resolve_phone_from_owner
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def dispatch!
|
|
23
|
+
Notifications.sms_adapter.deliver(notification: self)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def resolve_phone_from_owner
|
|
27
|
+
return nil unless owner
|
|
28
|
+
|
|
29
|
+
owner.try(:sms_notification_recipient) || owner.try(:phone)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notifications
|
|
4
|
+
# Consumes Auth events. On +identity.signed_up.auth+:
|
|
5
|
+
# 1. Resolves the recipient by `payload[:identity_id]`. The owner
|
|
6
|
+
# of the welcome notification is the Auth::Identity itself
|
|
7
|
+
# (the human who just signed up).
|
|
8
|
+
# 2. Enqueues Notifications::CreateNotificationJob — which creates
|
|
9
|
+
# InApp + (preferences-permitting) Email rows out-of-band.
|
|
10
|
+
#
|
|
11
|
+
# Notifications are NEVER created inline inside the publisher's
|
|
12
|
+
# thread (see Seams::Events::Publisher docstring); the job is the
|
|
13
|
+
# boundary.
|
|
14
|
+
#
|
|
15
|
+
# Hosts that want a different owner (an Account, a Membership, or
|
|
16
|
+
# a host-defined User) should override this subscriber — copy this
|
|
17
|
+
# file into their host engine, change `OWNER_CLASS_NAME` and the
|
|
18
|
+
# resolution rule, and re-attach. The Notification table accepts
|
|
19
|
+
# any polymorphic owner; the choice is purely about which model
|
|
20
|
+
# the welcome notification "belongs to" in the bell partial / index.
|
|
21
|
+
#
|
|
22
|
+
# +.attach!+ is idempotent across Rails autoreload via
|
|
23
|
+
# +Seams::Events::Publisher.attach_class+ — registering the class by
|
|
24
|
+
# NAME (a String) rather than capturing a closure means each dispatch
|
|
25
|
+
# re-resolves the constant, so edits to +handle_signed_up+ are picked
|
|
26
|
+
# up without a server restart.
|
|
27
|
+
class AuthSubscriber
|
|
28
|
+
SUBSCRIBER_KEY = :notifications_auth_subscriber
|
|
29
|
+
OWNER_CLASS_NAME = "Auth::Identity"
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
def attach!
|
|
33
|
+
Seams::Events::Publisher.attach_class(
|
|
34
|
+
SUBSCRIBER_KEY,
|
|
35
|
+
"identity.signed_up.auth",
|
|
36
|
+
class_name: "Notifications::AuthSubscriber",
|
|
37
|
+
method_name: :handle_signed_up
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def handle_signed_up(payload)
|
|
44
|
+
identity_id = payload[:identity_id]
|
|
45
|
+
return unless identity_id
|
|
46
|
+
|
|
47
|
+
Notifications::CreateNotificationJob.perform_later(
|
|
48
|
+
owner_class: OWNER_CLASS_NAME,
|
|
49
|
+
owner_id: identity_id,
|
|
50
|
+
template: "welcome",
|
|
51
|
+
strategy: "in_app"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return unless email_enabled?(identity_id)
|
|
55
|
+
|
|
56
|
+
Notifications::CreateNotificationJob.perform_later(
|
|
57
|
+
owner_class: OWNER_CLASS_NAME,
|
|
58
|
+
owner_id: identity_id,
|
|
59
|
+
template: "welcome",
|
|
60
|
+
strategy: "email"
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def email_enabled?(identity_id)
|
|
65
|
+
Notifications::NotificationPreference.enabled?(
|
|
66
|
+
identity_id: identity_id, channel: "email", notification_type: "welcome"
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notifications
|
|
4
|
+
# Consumes Billing events and enqueues a Notifications::CreateNotificationJob
|
|
5
|
+
# (which creates the InApp Notification out-of-band — never inline,
|
|
6
|
+
# see Seams::Events::Publisher docstring).
|
|
7
|
+
#
|
|
8
|
+
# Every subscribed event publishes the same canonical Wave-9 payload
|
|
9
|
+
# shape:
|
|
10
|
+
# { gateway:, livemode:, account_id:, customer_ref:, ref:, object_id:, object: }
|
|
11
|
+
#
|
|
12
|
+
# `account_id` is the UUID of the Accounts::Account (the billing
|
|
13
|
+
# tenant) the event belongs to. Notifications attaches the resulting
|
|
14
|
+
# Notification row to that Account directly — the recipient model is
|
|
15
|
+
# whatever class the host configured as the billing tenant
|
|
16
|
+
# (`Billing.configuration.billable_class`, default
|
|
17
|
+
# "Accounts::Account"). One human Identity may belong to many
|
|
18
|
+
# Accounts; per-Account billing notifications belong with the tenant,
|
|
19
|
+
# not with the human.
|
|
20
|
+
#
|
|
21
|
+
# +.attach!+ is idempotent across Rails autoreload via
|
|
22
|
+
# +Seams::Events::Publisher.attach_class+ — each event registers its
|
|
23
|
+
# subscriber by class NAME (a String), so dispatch re-resolves the
|
|
24
|
+
# constant on every call. Edits to a +handle_*+ method take effect
|
|
25
|
+
# without a server restart.
|
|
26
|
+
class BillingSubscriber
|
|
27
|
+
SUBSCRIBER_KEY = :notifications_billing_subscriber
|
|
28
|
+
SUBSCRIBER_CLASS_NAME = "Notifications::BillingSubscriber"
|
|
29
|
+
|
|
30
|
+
# (event_name => [handler_method, template]). Handler methods are
|
|
31
|
+
# one-line wrappers around #enqueue — kept separate (rather than a
|
|
32
|
+
# single block-capturing handler) so each registration goes through
|
|
33
|
+
# +attach_class+ and survives Rails autoreload.
|
|
34
|
+
EVENT_HANDLERS = {
|
|
35
|
+
"subscription.created.billing" => [:handle_subscription_created, "billing/subscription_started"],
|
|
36
|
+
"subscription.updated.billing" => [:handle_subscription_updated, "billing/subscription_updated"],
|
|
37
|
+
"subscription.canceled.billing" => [:handle_subscription_canceled, "billing/subscription_canceled"],
|
|
38
|
+
"invoice.paid.billing" => [:handle_invoice_paid, "billing/invoice_paid"],
|
|
39
|
+
"invoice.failed.billing" => [:handle_invoice_failed, "billing/invoice_failed"],
|
|
40
|
+
# Lifetime Deal events (issue #2 section 3A.LTD)
|
|
41
|
+
"lifetime.granted.billing" => [:handle_lifetime_granted, "billing/lifetime_granted"],
|
|
42
|
+
"lifetime.purchased.billing" => [:handle_lifetime_purchased, "billing/lifetime_purchased"]
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
def attach!
|
|
47
|
+
EVENT_HANDLERS.each do |event, (method_name, _template)|
|
|
48
|
+
Seams::Events::Publisher.attach_class(
|
|
49
|
+
SUBSCRIBER_KEY,
|
|
50
|
+
event,
|
|
51
|
+
class_name: SUBSCRIBER_CLASS_NAME,
|
|
52
|
+
method_name: method_name
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Owner class the billing subscriber attaches the Notification row
|
|
58
|
+
# to. Reads from `Billing.configuration.billable_class` so hosts
|
|
59
|
+
# with a custom tenant model (a host-defined `Workspace`, etc.) get
|
|
60
|
+
# the right `owner_type` without overriding this subscriber. Falls
|
|
61
|
+
# back to the canonical `"Accounts::Account"` if billing isn't
|
|
62
|
+
# configured.
|
|
63
|
+
def owner_class_name
|
|
64
|
+
return "Accounts::Account" unless defined?(Billing) && Billing.respond_to?(:configuration)
|
|
65
|
+
|
|
66
|
+
Billing.configuration.billable_class.to_s.presence || "Accounts::Account"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def handle_subscription_created(payload)
|
|
72
|
+
enqueue(payload, EVENT_HANDLERS.fetch("subscription.created.billing").last)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def handle_subscription_updated(payload)
|
|
76
|
+
enqueue(payload, EVENT_HANDLERS.fetch("subscription.updated.billing").last)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def handle_subscription_canceled(payload)
|
|
80
|
+
enqueue(payload, EVENT_HANDLERS.fetch("subscription.canceled.billing").last)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def handle_invoice_paid(payload)
|
|
84
|
+
enqueue(payload, EVENT_HANDLERS.fetch("invoice.paid.billing").last)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def handle_invoice_failed(payload)
|
|
88
|
+
enqueue(payload, EVENT_HANDLERS.fetch("invoice.failed.billing").last)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def handle_lifetime_granted(payload)
|
|
92
|
+
enqueue(payload, EVENT_HANDLERS.fetch("lifetime.granted.billing").last)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def handle_lifetime_purchased(payload)
|
|
96
|
+
enqueue(payload, EVENT_HANDLERS.fetch("lifetime.purchased.billing").last)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Wave 9: every canonical billing payload carries `account_id:`
|
|
100
|
+
# (the UUID of the Accounts::Account that owns the billing row)
|
|
101
|
+
# right next to `customer_ref:`. We address the notification at
|
|
102
|
+
# the tenant directly — no host-User lookup needed.
|
|
103
|
+
def enqueue(payload, template)
|
|
104
|
+
account_id = payload[:account_id]
|
|
105
|
+
if account_id.nil? || account_id.to_s.empty?
|
|
106
|
+
# Visible signal — a silent miss here looks identical to "no
|
|
107
|
+
# recipient matched", which makes "billing notifications never
|
|
108
|
+
# fire" hard to debug. Log loudly once per call.
|
|
109
|
+
Seams::Observability.adapter.warn(
|
|
110
|
+
"notifications.billing_subscriber.skip",
|
|
111
|
+
engine: "notifications",
|
|
112
|
+
reason: "billing event missing account_id",
|
|
113
|
+
customer_ref: payload[:customer_ref]
|
|
114
|
+
)
|
|
115
|
+
return
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
Notifications::CreateNotificationJob.perform_later(
|
|
119
|
+
owner_class: owner_class_name,
|
|
120
|
+
owner_id: account_id,
|
|
121
|
+
template: template,
|
|
122
|
+
strategy: "in_app"
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
data/lib/generators/seams/notifications/templates/app/views/layouts/notifications/mailer.html.erb.tt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<title><%%= @notification&.template.to_s.titleize %></title>
|
|
8
|
+
<style>
|
|
9
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #222; line-height: 1.5; max-width: 560px; margin: 24px auto; padding: 0 16px; }
|
|
10
|
+
p { margin: 0 0 12px; }
|
|
11
|
+
.meta { color: #888; font-size: 12px; border-top: 1px solid #eee; padding-top: 12px; margin-top: 24px; }
|
|
12
|
+
a { color: #1a73e8; }
|
|
13
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<%%= yield %>
|
|
17
|
+
|
|
18
|
+
<p class="meta">
|
|
19
|
+
Sent via Seams Notifications.
|
|
20
|
+
</p>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<%%# Notification bell partial. Render in the host layout: %>
|
|
2
|
+
<%%# <%%= render "notifications/bell", recipient: current_user %> %>
|
|
3
|
+
<%%
|
|
4
|
+
count = recipient&.notifications&.where(type: "Notifications::Strategies::InApp")&.unread&.count || 0
|
|
5
|
+
%>
|
|
6
|
+
|
|
7
|
+
<a href="<%%= notifications.notifications_path %>"
|
|
8
|
+
class="notifications-bell"
|
|
9
|
+
data-controller="notification-bell"
|
|
10
|
+
data-notification-bell-count-value="<%%= count %>">
|
|
11
|
+
<span class="notifications-bell-icon">🔔</span>
|
|
12
|
+
<%% if count.positive? %>
|
|
13
|
+
<span class="notifications-bell-count" data-notification-bell-target="count"><%%= count %></span>
|
|
14
|
+
<%% end %>
|
|
15
|
+
</a>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<h1>Notifications</h1>
|
|
2
|
+
|
|
3
|
+
<%% if @notifications.unread.exists? %>
|
|
4
|
+
<%%= button_to "Mark all as read", mark_all_as_read_notifications_path, method: :patch %>
|
|
5
|
+
<%% end %>
|
|
6
|
+
|
|
7
|
+
<ul>
|
|
8
|
+
<%% @notifications.each do |n| %>
|
|
9
|
+
<li class="<%%= 'unread' unless n.read? %>">
|
|
10
|
+
<strong><%%= n.template.to_s.titleize %></strong>
|
|
11
|
+
<p><%%= n.rendered_content %></p>
|
|
12
|
+
<%%= time_tag n.created_at %>
|
|
13
|
+
</li>
|
|
14
|
+
<%% end %>
|
|
15
|
+
</ul>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<%%# Default billing/lifetime_granted HTML — fires when an admin %>
|
|
2
|
+
<%%# privately grants a Lifetime Deal (no Stripe charge). Override at %>
|
|
3
|
+
<%%# app/views/notifications/templates/billing/lifetime_granted.html.erb. %>
|
|
4
|
+
<p>A lifetime pass has been granted to your account. You have permanent
|
|
5
|
+
access to this product — no further payment required.</p>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<%%# Default billing/lifetime_granted template — fires when an admin %>
|
|
2
|
+
<%%# privately grants a Lifetime Deal (no Stripe charge). Override at %>
|
|
3
|
+
<%%# app/views/notifications/templates/billing/lifetime_granted.erb. %>
|
|
4
|
+
A lifetime pass has been granted to your account. You have permanent
|
|
5
|
+
access to this product — no further payment required.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<%%# Default billing/lifetime_purchased HTML — fires when a customer %>
|
|
2
|
+
<%%# pays for a Lifetime Deal via Stripe Checkout. Override at %>
|
|
3
|
+
<%%# app/views/notifications/templates/billing/lifetime_purchased.html.erb. %>
|
|
4
|
+
<p>Thank you for your lifetime purchase. You have permanent access to this
|
|
5
|
+
product — no recurring billing, ever.</p>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<%%# Default billing/lifetime_purchased template — fires when a customer %>
|
|
2
|
+
<%%# pays for a Lifetime Deal via Stripe Checkout. Override at %>
|
|
3
|
+
<%%# app/views/notifications/templates/billing/lifetime_purchased.erb. %>
|
|
4
|
+
Thank you for your lifetime purchase. You have permanent access to this
|
|
5
|
+
product — no recurring billing, ever.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<%%# Default billing/subscription_canceled HTML. Override at %>
|
|
2
|
+
<%%# app/views/notifications/templates/billing/subscription_canceled.html.erb. %>
|
|
3
|
+
<p>Your subscription has been canceled. You'll continue to have access through
|
|
4
|
+
the end of your current billing period.</p>
|