package repository import ( "dal-license-server/internal/model" "database/sql" "fmt" ) type LicenseRepo struct { db *sql.DB } func NewLicenseRepo(db *sql.DB) *LicenseRepo { return &LicenseRepo{db: db} } func (r *LicenseRepo) Create(l *model.License) (int64, error) { res, err := r.db.Exec(`INSERT INTO licenses (product_id, license_key, license_type, customer_name, customer_pib, customer_email, limits_json, features, expires_at, grace_days, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, l.ProductID, l.LicenseKey, l.LicenseType, l.CustomerName, l.CustomerPIB, l.CustomerEmail, string(l.Limits), string(l.Features), l.ExpiresAt, l.GraceDays, l.Notes) if err != nil { return 0, fmt.Errorf("create license: %w", err) } return res.LastInsertId() } func (r *LicenseRepo) GetByID(id int64) (*model.License, error) { l := &model.License{} var limits, features string var revokedReason, notes sql.NullString err := r.db.QueryRow(`SELECT l.id, l.product_id, l.license_key, l.license_type, l.customer_name, l.customer_pib, l.customer_email, l.limits_json, l.features, l.issued_at, l.expires_at, l.grace_days, l.active, l.revoked, l.revoked_at, l.revoked_reason, l.notes, l.created_at, l.updated_at, p.code, p.name, p.key_prefix FROM licenses l JOIN products p ON l.product_id = p.id WHERE l.id = ?`, id). Scan(&l.ID, &l.ProductID, &l.LicenseKey, &l.LicenseType, &l.CustomerName, &l.CustomerPIB, &l.CustomerEmail, &limits, &features, &l.IssuedAt, &l.ExpiresAt, &l.GraceDays, &l.Active, &l.Revoked, &l.RevokedAt, &revokedReason, ¬es, &l.CreatedAt, &l.UpdatedAt, &l.ProductCode, &l.ProductName, &l.ProductPrefix) if err != nil { return nil, fmt.Errorf("get license %d: %w", id, err) } l.Limits = []byte(limits) l.Features = []byte(features) if revokedReason.Valid { l.RevokedReason = revokedReason.String } if notes.Valid { l.Notes = notes.String } return l, nil } func (r *LicenseRepo) GetByKey(key string) (*model.License, error) { l := &model.License{} var limits, features string var revokedReason, notes sql.NullString err := r.db.QueryRow(`SELECT l.id, l.product_id, l.license_key, l.license_type, l.customer_name, l.customer_pib, l.customer_email, l.limits_json, l.features, l.issued_at, l.expires_at, l.grace_days, l.active, l.revoked, l.revoked_at, l.revoked_reason, l.notes, l.created_at, l.updated_at, p.code, p.name, p.key_prefix FROM licenses l JOIN products p ON l.product_id = p.id WHERE l.license_key = ?`, key). Scan(&l.ID, &l.ProductID, &l.LicenseKey, &l.LicenseType, &l.CustomerName, &l.CustomerPIB, &l.CustomerEmail, &limits, &features, &l.IssuedAt, &l.ExpiresAt, &l.GraceDays, &l.Active, &l.Revoked, &l.RevokedAt, &revokedReason, ¬es, &l.CreatedAt, &l.UpdatedAt, &l.ProductCode, &l.ProductName, &l.ProductPrefix) if err != nil { return nil, fmt.Errorf("get license by key: %w", err) } l.Limits = []byte(limits) l.Features = []byte(features) if revokedReason.Valid { l.RevokedReason = revokedReason.String } if notes.Valid { l.Notes = notes.String } return l, nil } func (r *LicenseRepo) List(productCode, status, search string) ([]model.LicenseWithActivation, error) { query := `SELECT l.id, l.product_id, l.license_key, l.license_type, l.customer_name, l.customer_pib, l.customer_email, l.limits_json, l.features, l.issued_at, l.expires_at, l.grace_days, l.active, l.revoked, l.revoked_at, l.revoked_reason, l.notes, l.created_at, l.updated_at, p.code, p.name, p.key_prefix, (SELECT COUNT(*) FROM activations a WHERE a.license_id = l.id AND a.is_active = TRUE) as active_activations FROM licenses l JOIN products p ON l.product_id = p.id WHERE 1=1` var args []interface{} if productCode != "" { query += " AND p.code = ?" args = append(args, productCode) } if search != "" { query += " AND (l.customer_name LIKE ? OR l.license_key LIKE ?)" args = append(args, "%"+search+"%", "%"+search+"%") } switch status { case "active": query += " AND l.active = TRUE AND l.revoked = FALSE AND (l.expires_at IS NULL OR l.expires_at > NOW())" case "expired": query += " AND l.expires_at IS NOT NULL AND l.expires_at <= NOW() AND l.revoked = FALSE" case "revoked": query += " AND l.revoked = TRUE" case "trial": query += " AND l.license_type = 'TRIAL'" } query += " ORDER BY l.created_at DESC LIMIT 200" rows, err := r.db.Query(query, args...) if err != nil { return nil, fmt.Errorf("list licenses: %w", err) } defer rows.Close() var licenses []model.LicenseWithActivation for rows.Next() { var lwa model.LicenseWithActivation var limits, features string var revokedReason, notes sql.NullString err := rows.Scan(&lwa.ID, &lwa.ProductID, &lwa.LicenseKey, &lwa.LicenseType, &lwa.CustomerName, &lwa.CustomerPIB, &lwa.CustomerEmail, &limits, &features, &lwa.IssuedAt, &lwa.ExpiresAt, &lwa.GraceDays, &lwa.Active, &lwa.Revoked, &lwa.RevokedAt, &revokedReason, ¬es, &lwa.CreatedAt, &lwa.UpdatedAt, &lwa.ProductCode, &lwa.ProductName, &lwa.ProductPrefix, &lwa.ActiveActivations) if err != nil { return nil, fmt.Errorf("scan license: %w", err) } lwa.Limits = []byte(limits) lwa.Features = []byte(features) if revokedReason.Valid { lwa.RevokedReason = revokedReason.String } if notes.Valid { lwa.Notes = notes.String } licenses = append(licenses, lwa) } return licenses, nil } func (r *LicenseRepo) Update(l *model.License) error { _, err := r.db.Exec(`UPDATE licenses SET customer_name=?, customer_pib=?, customer_email=?, limits_json=?, features=?, grace_days=?, expires_at=?, notes=? WHERE id=?`, l.CustomerName, l.CustomerPIB, l.CustomerEmail, string(l.Limits), string(l.Features), l.GraceDays, l.ExpiresAt, l.Notes, l.ID) return err } func (r *LicenseRepo) Revoke(id int64, reason string) error { _, err := r.db.Exec(`UPDATE licenses SET revoked=TRUE, revoked_at=NOW(), revoked_reason=?, active=FALSE WHERE id=?`, reason, id) return err } func (r *LicenseRepo) CountByProduct() ([]model.ProductStats, error) { rows, err := r.db.Query(`SELECT p.code, p.name, COUNT(*) as total, SUM(CASE WHEN l.active AND NOT l.revoked AND (l.expires_at IS NULL OR l.expires_at > NOW()) THEN 1 ELSE 0 END) as act, SUM(CASE WHEN l.expires_at IS NOT NULL AND l.expires_at <= NOW() AND NOT l.revoked THEN 1 ELSE 0 END) as exp, SUM(CASE WHEN l.expires_at IS NOT NULL AND l.expires_at <= NOW() AND DATE_ADD(l.expires_at, INTERVAL l.grace_days DAY) > NOW() AND NOT l.revoked THEN 1 ELSE 0 END) as grace, SUM(CASE WHEN l.license_type = 'TRIAL' THEN 1 ELSE 0 END) as trial, (SELECT COUNT(*) FROM activations a JOIN licenses l2 ON a.license_id=l2.id WHERE l2.product_id=p.id AND a.is_active=TRUE) as active_acts FROM licenses l JOIN products p ON l.product_id=p.id GROUP BY p.id, p.code, p.name`) if err != nil { return nil, err } defer rows.Close() var stats []model.ProductStats for rows.Next() { var s model.ProductStats rows.Scan(&s.ProductCode, &s.ProductName, &s.Total, &s.Active, &s.Expired, &s.InGrace, &s.Trial, &s.ActiveActivations) stats = append(stats, s) } return stats, nil } func (r *LicenseRepo) GetProducts() ([]model.Product, error) { rows, err := r.db.Query("SELECT id, code, name, key_prefix, default_limits, available_features, active, created_at FROM products WHERE active = TRUE") if err != nil { return nil, err } defer rows.Close() var products []model.Product for rows.Next() { var p model.Product var limits, features string rows.Scan(&p.ID, &p.Code, &p.Name, &p.KeyPrefix, &limits, &features, &p.Active, &p.CreatedAt) p.DefaultLimits = []byte(limits) p.AvailableFeatures = []byte(features) products = append(products, p) } return products, nil } func (r *LicenseRepo) GetProductByID(id int64) (*model.Product, error) { var p model.Product var limits, features string err := r.db.QueryRow("SELECT id, code, name, key_prefix, default_limits, available_features, active, created_at FROM products WHERE id = ?", id). Scan(&p.ID, &p.Code, &p.Name, &p.KeyPrefix, &limits, &features, &p.Active, &p.CreatedAt) if err != nil { return nil, err } p.DefaultLimits = []byte(limits) p.AvailableFeatures = []byte(features) return &p, nil } func (r *LicenseRepo) ExpiringIn(days int) ([]model.License, error) { rows, err := r.db.Query(`SELECT l.id, l.license_key, l.license_type, l.customer_name, l.expires_at, p.code, p.name, p.key_prefix FROM licenses l JOIN products p ON l.product_id=p.id WHERE l.active AND NOT l.revoked AND l.expires_at IS NOT NULL AND l.expires_at BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL ? DAY) ORDER BY l.expires_at`, days) if err != nil { return nil, err } defer rows.Close() var licenses []model.License for rows.Next() { var l model.License rows.Scan(&l.ID, &l.LicenseKey, &l.LicenseType, &l.CustomerName, &l.ExpiresAt, &l.ProductCode, &l.ProductName, &l.ProductPrefix) licenses = append(licenses, l) } return licenses, nil }