main pico / pkg / db / postgres / features.go
Antonio Mika  ·  2026-10-11
  1package postgres
  2
  3import (
  4	"database/sql"
  5	"fmt"
  6	"time"
  7
  8	"github.com/picosh/pico/pkg/db"
  9)
 10
 11func (me *PsqlDB) FindFeature(userID string, feature string) (*db.FeatureFlag, error) {
 12	ff := &db.FeatureFlag{}
 13	err := me.Db.Get(ff, `SELECT * FROM feature_flags WHERE user_id = $1 AND name = $2 ORDER BY expires_at DESC LIMIT 1`, userID, feature)
 14	if err != nil {
 15		return nil, err
 16	}
 17	return ff, nil
 18}
 19
 20func (me *PsqlDB) FindFeaturesByUser(userID string) ([]*db.FeatureFlag, error) {
 21	var features []*db.FeatureFlag
 22	// https://stackoverflow.com/a/16920077
 23	query := `SELECT DISTINCT ON (name) *
 24		FROM feature_flags
 25		WHERE user_id=$1
 26		ORDER BY name, expires_at DESC;`
 27	err := me.Db.Select(&features, query, userID)
 28	if err != nil {
 29		return nil, err
 30	}
 31	return features, nil
 32}
 33
 34func (me *PsqlDB) InsertFeature(userID, name string, expiresAt time.Time) (*db.FeatureFlag, error) {
 35	var featureID string
 36	err := me.Db.QueryRow(
 37		`INSERT INTO feature_flags (user_id, name, expires_at) VALUES ($1, $2, $3) RETURNING id;`,
 38		userID,
 39		name,
 40		expiresAt,
 41	).Scan(&featureID)
 42	if err != nil {
 43		return nil, err
 44	}
 45
 46	feature, err := me.FindFeature(userID, name)
 47	if err != nil {
 48		return nil, err
 49	}
 50
 51	return feature, nil
 52}
 53
 54func (me *PsqlDB) RemoveFeature(userID string, name string) error {
 55	_, err := me.Db.Exec(`DELETE FROM feature_flags WHERE user_id = $1 AND name = $2`, userID, name)
 56	return err
 57}
 58
 59func (me *PsqlDB) createFeatureExpiresAt(userID, name string) time.Time {
 60	ff, _ := me.FindFeature(userID, name)
 61	// if the feature flag has already expired we don't want to add a year to it since that will
 62	// not grant the user a full year
 63	if ff == nil || !ff.IsValid() {
 64		t := time.Now()
 65		return t.AddDate(1, 0, 0)
 66	}
 67	return ff.ExpiresAt.AddDate(1, 0, 0)
 68}
 69
 70func (me *PsqlDB) AddPicoPlusUser(username, email, paymentType, txId string) error {
 71	user, err := me.FindUserByName(username)
 72	if err != nil {
 73		return err
 74	}
 75
 76	tx, err := me.Db.Beginx()
 77	if err != nil {
 78		return err
 79	}
 80	defer func() {
 81		_ = tx.Rollback()
 82	}()
 83
 84	var paymentHistoryId sql.NullString
 85	if paymentType != "" {
 86		data := db.PaymentHistoryData{
 87			Notes: "",
 88			TxID:  txId,
 89		}
 90
 91		err := tx.QueryRow(
 92			`INSERT INTO payment_history (user_id, payment_type, amount, data) VALUES ($1, $2, 24 * 1000000, $3) RETURNING id;`,
 93			user.ID,
 94			paymentType,
 95			data,
 96		).Scan(&paymentHistoryId)
 97		if err != nil {
 98			return err
 99		}
100	}
101
102	plus := me.createFeatureExpiresAt(user.ID, "plus")
103	plusQuery := fmt.Sprintf(`INSERT INTO feature_flags (user_id, name, data, expires_at, payment_history_id)
104	VALUES ($1, 'plus', '{"storage_max":5000000000, "file_max":100000000, "email": "%s"}'::jsonb, $2, $3);`, email)
105	_, err = tx.Exec(plusQuery, user.ID, plus, paymentHistoryId)
106	if err != nil {
107		return err
108	}
109
110	return tx.Commit()
111}
112
113func (me *PsqlDB) AddFeatureUser(username, name string) error {
114	user, err := me.FindUserByName(username)
115	if err != nil {
116		return err
117	}
118
119	expiresAt := me.createFeatureExpiresAt(user.ID, name)
120	_, err = me.InsertFeature(user.ID, name, expiresAt)
121	return err
122}