SET search_path TO analytics, public; CREATE TABLE products ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, game_id bigint NOT NULL REFERENCES games(id), roblox_product_id bigint NOT NULL, name text, current_price_robux bigint CHECK(current_price_robux>=0), UNIQUE(game_id,id), UNIQUE(game_id,roblox_product_id) ); CREATE TABLE product_sales ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, game_id bigint NOT NULL REFERENCES games(id), product_id bigint NOT NULL, player_id bigint, session_id bigint, receipt_id text NOT NULL, purchased_at timestamptz NOT NULL, unit_price_robux bigint NOT NULL CHECK(unit_price_robux>=0), quantity integer NOT NULL DEFAULT 1 CHECK(quantity>0), properties jsonb NOT NULL DEFAULT '{}', UNIQUE(game_id,receipt_id), FOREIGN KEY(game_id,product_id) REFERENCES products(game_id,id), FOREIGN KEY(game_id,player_id) REFERENCES players(game_id,id), FOREIGN KEY(game_id,player_id,session_id) REFERENCES sessions(game_id,player_id,id), CHECK(session_id IS NULL OR player_id IS NOT NULL) ); CREATE INDEX sales_game_time ON product_sales(game_id,purchased_at DESC); CREATE INDEX sales_player_time ON product_sales(game_id,player_id,purchased_at DESC); CREATE INDEX sales_product_time ON product_sales(game_id,product_id,purchased_at DESC); CREATE OR REPLACE VIEW player_statistics AS WITH session_totals AS ( SELECT game_id,player_id,count(*)::int session_count, sum(extract(epoch FROM ended_at-started_at))::double precision playtime, avg(extract(epoch FROM ended_at-started_at))::double precision average_session, percentile_cont(.5) WITHIN GROUP(ORDER BY extract(epoch FROM ended_at-started_at))::double precision median_session, count(DISTINCT (started_at AT TIME ZONE 'UTC')::date) FILTER(WHERE started_at>=now()-interval '30 days')::int active_days FROM sessions GROUP BY game_id,player_id ), spend AS ( SELECT game_id,player_id,sum(unit_price_robux*quantity)::double precision robux,sum(quantity)::int products_bought FROM product_sales WHERE unit_price_robux>0 AND player_id IS NOT NULL GROUP BY game_id,player_id ) SELECT p.*,latest.data latest_data,latest.captured_at snapshot_at, coalesce(s.session_count,0) session_count,s.playtime,s.average_session,s.median_session,coalesce(s.active_days,0) active_days, coalesce(spend.robux,0) robux_recorded,coalesce(spend.products_bought,0) products_bought FROM players p LEFT JOIN session_totals s ON s.game_id=p.game_id AND s.player_id=p.id LEFT JOIN spend ON spend.game_id=p.game_id AND spend.player_id=p.id LEFT JOIN LATERAL(SELECT data,captured_at FROM player_snapshots ps WHERE ps.game_id=p.game_id AND ps.player_id=p.id ORDER BY captured_at DESC,id DESC LIMIT 1) latest ON true; CREATE VIEW session_day_playtime AS SELECT s.game_id,s.player_id,s.id session_id,d.day::date active_day, CASE WHEN s.ended_at IS NOT NULL THEN greatest(0,extract(epoch FROM least(s.ended_at,d.day+interval '1 day')-greatest(s.started_at,d.day)))::double precision END playtime FROM sessions s CROSS JOIN LATERAL generate_series(date_trunc('day',s.started_at),date_trunc('day',coalesce(s.ended_at,s.started_at)),interval '1 day') d(day); CREATE VIEW player_active_days AS SELECT game_id,player_id,active_day FROM session_day_playtime UNION SELECT game_id,player_id,(occurred_at AT TIME ZONE 'UTC')::date FROM events WHERE player_id IS NOT NULL UNION SELECT game_id,player_id,(purchased_at AT TIME ZONE 'UTC')::date FROM product_sales WHERE player_id IS NOT NULL; INSERT INTO schema_migrations(version) VALUES('002_monetization');