Files
flatrender/backend/db/migrations/04_identity_gamification.sql
soroush.asadi 90ac0b81d1 feat: V2 microservices stack — backend services, gateway, JWT auth
Add full V2 architecture: identity, content, studio (.NET 10) and file,
render, notification, gateway (Go) services with vendored deps, plus DB
migrations, event/API contracts, and an init-db script.

Wire the Next.js frontend to the gateway: server-side JWT auth routes
(login/register/refresh/logout/me), gateway fetch helper, and session/
cookie/jwt helpers under src/lib.

Containerize the stack via docker-compose.v2.yml and per-service
Dockerfiles. Base images resolve through a Nexus mirror (Docker Hub) and
MCR directly; npm/NuGet pull from Nexus groups. Self-host fonts via
next/font/local to avoid Google Fonts (geo-blocked).

Add CI workflow and ignore .env.v2, *.stackdump, and .NET bin/obj.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 23:29:31 +03:30

150 lines
6.4 KiB
SQL

-- =====================================================================
-- IDENTITY SCHEMA — Part 4: Gamification (simplified)
-- =====================================================================
-- Quests, gifts, loyalty — leaner than V1 but still functional.
-- =====================================================================
SET search_path TO identity, public;
CREATE TYPE quest_type AS ENUM ('OneTime','Daily','Weekly','Onboarding','Milestone');
CREATE TYPE prize_type AS ENUM ('Balance','RenderSeconds','LoyaltyPoints','StorageGB','Plan','Discount');
CREATE TYPE gift_type AS ENUM ('Bonus','Referral','Compensation','Promotion','Achievement');
-- ---------------------------------------------------------------------
-- quests
-- ---------------------------------------------------------------------
CREATE TABLE quests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, -- NULL = global
title TEXT NOT NULL,
challenge TEXT,
why TEXT,
hint TEXT,
aphorism TEXT,
icon TEXT,
quest_type quest_type NOT NULL,
target_event TEXT NOT NULL, -- 'user.registered','project.created',...
target_count INT NOT NULL DEFAULT 1, -- how many times event must fire
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
prize_type prize_type NOT NULL,
prize_amount BIGINT NOT NULL, -- minor units or seconds/points
level_limit INT, -- minimum loyalty level
start_url TEXT,
post_action_name TEXT,
order_value INT NOT NULL DEFAULT 0,
starts_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_quests_active ON quests(quest_type, is_active);
CREATE TRIGGER tg_quests_updated_at
BEFORE UPDATE ON quests
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- user_quest_progress — incremental tracking
-- ---------------------------------------------------------------------
CREATE TABLE user_quest_progress (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
quest_id UUID NOT NULL REFERENCES quests(id) ON DELETE CASCADE,
current_count INT NOT NULL DEFAULT 0,
text_value TEXT, -- for input-based quests
is_completed BOOLEAN NOT NULL DEFAULT FALSE,
completed_at TIMESTAMPTZ,
prize_claimed BOOLEAN NOT NULL DEFAULT FALSE,
prize_claimed_at TIMESTAMPTZ,
period_start DATE, -- for Daily/Weekly resets
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (user_id, quest_id, period_start)
);
CREATE INDEX idx_quest_prog_user_open ON user_quest_progress(user_id) WHERE is_completed = FALSE;
CREATE TRIGGER tg_quest_prog_updated_at
BEFORE UPDATE ON user_quest_progress
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
-- ---------------------------------------------------------------------
-- gifts — admin-issued bonuses
-- ---------------------------------------------------------------------
CREATE TABLE gifts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT,
icon TEXT,
gift_type gift_type NOT NULL,
prize_type prize_type NOT NULL,
value BIGINT NOT NULL,
unit TEXT, -- 'seconds','IRR','points',...
assigned_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ---------------------------------------------------------------------
-- earned_gifts — issued to user (consumed via used_gifts)
-- ---------------------------------------------------------------------
CREATE TABLE earned_gifts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
gift_id UUID NOT NULL REFERENCES gifts(id) ON DELETE RESTRICT,
notification_id UUID, -- FK added later
source TEXT, -- 'quest','admin','referral','plan'
source_ref UUID, -- e.g. quest_id
earned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ,
is_used BOOLEAN NOT NULL DEFAULT FALSE,
used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_earned_gifts_user ON earned_gifts(user_id) WHERE is_used = FALSE;
-- ---------------------------------------------------------------------
-- used_gifts — claim log
-- ---------------------------------------------------------------------
CREATE TABLE used_gifts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
earned_gift_id UUID NOT NULL REFERENCES earned_gifts(id) ON DELETE RESTRICT,
used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_used_gifts_user ON used_gifts(user_id, used_at DESC);
-- ---------------------------------------------------------------------
-- avatars — preset avatar library
-- ---------------------------------------------------------------------
CREATE TABLE avatars (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code TEXT UNIQUE NOT NULL,
url TEXT NOT NULL,
description TEXT,
sort INT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);