dev day #90 🥳

This commit is contained in:
Tomas Dvorak
2025-11-12 20:31:37 +01:00
parent 8762bde4bf
commit f3db65d350
103 changed files with 4053 additions and 2189 deletions
+69 -1
View File
@@ -21,6 +21,29 @@ type EngagementController struct {
Email email.EmailService
}
// parseMetaTime tries to parse time from metadata value which can be string (RFC3339 or YYYY-MM-DD) or numeric unix seconds.
func parseMetaTime(v interface{}) time.Time {
switch t := v.(type) {
case string:
s := strings.TrimSpace(t)
if s == "" { return time.Time{} }
if ts, err := time.Parse(time.RFC3339, s); err == nil { return ts }
if ts, err := time.Parse("2006-01-02T15:04", s); err == nil { return ts }
if ts, err := time.Parse("2006-01-02", s); err == nil { return ts }
case float64:
// JSON numbers decode to float64
if t <= 0 { return time.Time{} }
return time.Unix(int64(t), 0)
case int64:
if t <= 0 { return time.Time{} }
return time.Unix(t, 0)
case int:
if t <= 0 { return time.Time{} }
return time.Unix(int64(t), 0)
}
return time.Time{}
}
func NewEngagementController(db *gorm.DB, es email.EmailService) *EngagementController {
return &EngagementController{DB: db, Email: es}
}
@@ -224,7 +247,31 @@ func (ec *EngagementController) GetRewards(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load rewards"})
return
}
c.JSON(http.StatusOK, items)
// Filter by optional validity window in metadata (valid_from, valid_to). Also accept legacy expires_at as valid_to.
now := time.Now()
filtered := make([]models.RewardItem, 0, len(items))
for _, it := range items {
// Mandatory unlock reward is always available
if strings.EqualFold(strings.TrimSpace(it.Type), "avatar_upload_unlock") {
filtered = append(filtered, it)
continue
}
var startPtr, endPtr *time.Time
if it.Metadata != nil {
if v, ok := it.Metadata["valid_from"]; ok {
if ts := parseMetaTime(v); !ts.IsZero() { startPtr = &ts }
}
if v, ok := it.Metadata["valid_to"]; ok {
if ts := parseMetaTime(v); !ts.IsZero() { endPtr = &ts }
} else if v2, ok2 := it.Metadata["expires_at"]; ok2 { // alias
if ts := parseMetaTime(v2); !ts.IsZero() { endPtr = &ts }
}
}
if startPtr != nil && now.Before(*startPtr) { continue }
if endPtr != nil && now.After(*endPtr) { continue }
filtered = append(filtered, it)
}
c.JSON(http.StatusOK, filtered)
}
// POST /api/v1/engagement/redeem (auth)
@@ -252,6 +299,27 @@ func (ec *EngagementController) Redeem(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Reward is not active"})
return
}
// Check validity window (metadata.valid_from/valid_to or expires_at)
if item.Metadata != nil {
var startPtr, endPtr *time.Time
if v, ok := item.Metadata["valid_from"]; ok {
if ts := parseMetaTime(v); !ts.IsZero() { startPtr = &ts }
}
if v, ok := item.Metadata["valid_to"]; ok {
if ts := parseMetaTime(v); !ts.IsZero() { endPtr = &ts }
} else if v2, ok2 := item.Metadata["expires_at"]; ok2 {
if ts := parseMetaTime(v2); !ts.IsZero() { endPtr = &ts }
}
now := time.Now()
if startPtr != nil && now.Before(*startPtr) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Reward is not currently available"})
return
}
if endPtr != nil && now.After(*endPtr) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Reward validity has ended"})
return
}
}
if item.Stock == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Out of stock"})
return