package repository import ( "context" "encoding/json" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" "github.com/yuxingu/digital-psychology/apps/api/internal/model" ) // ReportRepo persists growth reports and access checks. type ReportRepo struct { Pool *pgxpool.Pool } // Create inserts a growth report. func (r *ReportRepo) Create(ctx context.Context, userID, profileID uuid.UUID, typ string, summary, detail json.RawMessage) (*model.GrowthReport, error) { rep := &model.GrowthReport{} err := r.Pool.QueryRow(ctx, ` INSERT INTO growth_reports(user_id, profile_id, type, summary, detail) VALUES ($1,$2,$3,$4,$5) RETURNING id, user_id, profile_id, type, summary, detail, created_at`, userID, profileID, typ, summary, detail, ).Scan(&rep.ID, &rep.UserID, &rep.ProfileID, &rep.Type, &rep.Summary, &rep.Detail, &rep.CreatedAt) return rep, err } // GetForUser loads a report owned by user. func (r *ReportRepo) GetForUser(ctx context.Context, userID, reportID uuid.UUID) (*model.GrowthReport, error) { rep := &model.GrowthReport{} err := r.Pool.QueryRow(ctx, ` SELECT id, user_id, profile_id, type, summary, detail, created_at FROM growth_reports WHERE id=$1 AND user_id=$2 AND deleted_at IS NULL`, reportID, userID, ).Scan(&rep.ID, &rep.UserID, &rep.ProfileID, &rep.Type, &rep.Summary, &rep.Detail, &rep.CreatedAt) return rep, err } // HasDeepAccess reports whether user purchased deep access for report. func (r *ReportRepo) HasDeepAccess(ctx context.Context, userID, reportID uuid.UUID) (bool, error) { var ok bool err := r.Pool.QueryRow(ctx, ` SELECT EXISTS( SELECT 1 FROM deep_accesses WHERE user_id=$1 AND report_id=$2 AND deleted_at IS NULL )`, userID, reportID).Scan(&ok) return ok, err } // HasActiveMembership checks growth membership. func (r *ReportRepo) HasActiveMembership(ctx context.Context, userID uuid.UUID) (bool, error) { var ok bool err := r.Pool.QueryRow(ctx, ` SELECT EXISTS( SELECT 1 FROM memberships WHERE user_id=$1 AND status='active' AND expires_at > now() AND deleted_at IS NULL )`, userID).Scan(&ok) return ok, err } // CreateOrder inserts an order. func (r *ReportRepo) CreateOrder(ctx context.Context, userID uuid.UUID, kind, plan string, reportID *uuid.UUID, amount int) (uuid.UUID, error) { var id uuid.UUID err := r.Pool.QueryRow(ctx, ` INSERT INTO orders(user_id, kind, plan, report_id, amount_cents, status) VALUES ($1,$2,$3,$4,$5,'created') RETURNING id`, userID, kind, nullIfEmpty(plan), reportID, amount, ).Scan(&id) return id, err } // PayMock marks order paid and grants entitlement. func (r *ReportRepo) PayMock(ctx context.Context, userID, orderID uuid.UUID) error { tx, err := r.Pool.Begin(ctx) if err != nil { return err } defer tx.Rollback(ctx) var kind string var reportID *uuid.UUID var plan *string err = tx.QueryRow(ctx, ` SELECT kind, report_id, plan FROM orders WHERE id=$1 AND user_id=$2 AND deleted_at IS NULL FOR UPDATE`, orderID, userID, ).Scan(&kind, &reportID, &plan) if err != nil { return err } if _, err := tx.Exec(ctx, `UPDATE orders SET status='paid', updated_at=now() WHERE id=$1`, orderID); err != nil { return err } if _, err := tx.Exec(ctx, ` INSERT INTO payments(order_id, channel, status) VALUES ($1,'mock','paid')`, orderID); err != nil { return err } switch kind { case "deep_access": if reportID == nil { return errMissingReport } if _, err := tx.Exec(ctx, ` INSERT INTO deep_accesses(user_id, report_id, order_id) VALUES ($1,$2,$3) ON CONFLICT (user_id, report_id) DO NOTHING`, userID, *reportID, orderID); err != nil { return err } case "membership": p := "month" if plan != nil && *plan != "" { p = *plan } days := 31 if p == "quarter" { days = 92 } else if p == "year" { days = 366 } if _, err := tx.Exec(ctx, ` INSERT INTO memberships(user_id, plan, status, expires_at, ask_quota_left) VALUES ($1,$2,'active', now() + ($3::text || ' days')::interval, 100) ON CONFLICT (user_id) DO UPDATE SET plan=EXCLUDED.plan, status='active', expires_at=EXCLUDED.expires_at, ask_quota_left=100, updated_at=now()`, userID, p, days); err != nil { return err } } return tx.Commit(ctx) } var errMissingReport = errString("report_id required for deep_access") type errString string func (e errString) Error() string { return string(e) } func nullIfEmpty(s string) *string { if s == "" { return nil } return &s }