-- ===================================================================== -- NOTIFICATION SCHEMA — In-app, Push, Email, SMS, Telegram -- ===================================================================== SET search_path TO notification, public; CREATE TYPE notification_kind AS ENUM ( 'RenderCompleted','RenderFailed','RenderProgress', 'PlanExpiring','PlanExpired','PaymentSuccess','PaymentFailed', 'StorageWarning','StorageFull','ExportExpiring','ExportDeleted', 'GiftEarned','QuestCompleted','LevelUp', 'AccountSecurity','SystemAnnouncement','TenantInvite', 'Marketing','Other' ); CREATE TYPE notification_priority AS ENUM ('Low','Normal','High','Urgent'); CREATE TYPE delivery_channel AS ENUM ('InApp','Push','Email','SMS','Telegram','Webhook'); CREATE TYPE delivery_status_kind AS ENUM ( 'Pending','Sent','Delivered','Failed','Bounced','Suppressed' ); -- --------------------------------------------------------------------- -- notifications — in-app notification feed -- --------------------------------------------------------------------- CREATE TABLE notifications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, user_id UUID NOT NULL, notification_type notification_kind NOT NULL, priority notification_priority NOT NULL DEFAULT 'Normal', title TEXT NOT NULL, message TEXT NOT NULL, label TEXT, signature TEXT, icon TEXT, image TEXT, animation_demo TEXT, design TEXT, -- Link target action_url TEXT, action_text TEXT, -- Linked entities (sparse) render_job_id UUID, export_id UUID, payment_id UUID, gift_id UUID, earned_gift_id UUID, -- State is_emergency BOOLEAN NOT NULL DEFAULT FALSE, seen BOOLEAN NOT NULL DEFAULT FALSE, seen_at TIMESTAMPTZ, clicked BOOLEAN NOT NULL DEFAULT FALSE, clicked_at TIMESTAMPTZ, gift_used BOOLEAN NOT NULL DEFAULT FALSE, -- Lifecycle expire_date TIMESTAMPTZ, create_date TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ ); CREATE INDEX idx_notifs_user_feed ON notifications(user_id, created_at DESC) WHERE deleted_at IS NULL; CREATE INDEX idx_notifs_unread ON notifications(user_id) WHERE seen = FALSE AND deleted_at IS NULL; CREATE INDEX idx_notifs_tenant_type ON notifications(tenant_id, notification_type); CREATE TRIGGER tg_notifs_updated_at BEFORE UPDATE ON notifications FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); -- --------------------------------------------------------------------- -- readed_notifications — read receipts (kept for analytics) -- --------------------------------------------------------------------- CREATE TABLE readed_notifications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, notification_id UUID NOT NULL REFERENCES notifications(id) ON DELETE CASCADE, read_date TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (user_id, notification_id) ); CREATE INDEX idx_readed_user ON readed_notifications(user_id, read_date DESC); -- --------------------------------------------------------------------- -- notification_deliveries — outbound across channels -- --------------------------------------------------------------------- CREATE TABLE notification_deliveries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, user_id UUID NOT NULL, notification_id UUID REFERENCES notifications(id) ON DELETE SET NULL, channel delivery_channel NOT NULL, recipient TEXT NOT NULL, -- email/phone/push endpoint subject TEXT, body_text TEXT, body_html TEXT, template_id TEXT, -- reference to template engine template_vars JSONB, -- Provider tracking provider TEXT, -- 'web-push','smtp','kavenegar','telegram','firebase' provider_message_id TEXT, provider_response JSONB, status delivery_status_kind NOT NULL DEFAULT 'Pending', error_message TEXT, error_code TEXT, -- Retry attempt INT NOT NULL DEFAULT 1, max_attempts INT NOT NULL DEFAULT 3, next_retry_at TIMESTAMPTZ, -- Timing sent_at TIMESTAMPTZ, delivered_at TIMESTAMPTZ, failed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_deliveries_user ON notification_deliveries(user_id, created_at DESC); CREATE INDEX idx_deliveries_pending ON notification_deliveries(next_retry_at) WHERE status IN ('Pending','Failed') AND next_retry_at IS NOT NULL; CREATE INDEX idx_deliveries_channel ON notification_deliveries(channel, status); CREATE TRIGGER tg_deliveries_updated_at BEFORE UPDATE ON notification_deliveries FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); -- --------------------------------------------------------------------- -- notification_preferences — per-user opt-in per channel per type -- --------------------------------------------------------------------- CREATE TABLE notification_preferences ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, notification_type notification_kind NOT NULL, channel delivery_channel NOT NULL, enabled BOOLEAN NOT NULL DEFAULT TRUE, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (user_id, notification_type, channel) ); CREATE INDEX idx_notif_prefs_user ON notification_preferences(user_id); CREATE TRIGGER tg_notif_prefs_updated_at BEFORE UPDATE ON notification_preferences FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); -- --------------------------------------------------------------------- -- notification_templates — reusable templates per tenant -- --------------------------------------------------------------------- CREATE TABLE notification_templates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID, -- NULL = default code TEXT NOT NULL, -- 'render.completed.email' channel delivery_channel NOT NULL, locale TEXT NOT NULL DEFAULT 'fa', subject TEXT, body_text TEXT, body_html TEXT, push_title TEXT, push_body TEXT, push_icon TEXT, variables_schema JSONB, -- expected variables is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (tenant_id, code, channel, locale) ); CREATE INDEX idx_notif_tpl_lookup ON notification_templates(tenant_id, code, channel, locale) WHERE is_active = TRUE; CREATE TRIGGER tg_notif_tpl_updated_at BEFORE UPDATE ON notification_templates FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); -- --------------------------------------------------------------------- -- Wire back: earned_gifts.notification_id -- --------------------------------------------------------------------- ALTER TABLE identity.earned_gifts ADD CONSTRAINT fk_earned_gifts_notification FOREIGN KEY (notification_id) REFERENCES notification.notifications(id) ON DELETE SET NULL;