Migrate from Gin to Echo framework and add comprehensive integration tests
Major changes: - Migrate all handlers from Gin to Echo framework - Add new apperrors, echohelpers, and validator packages - Update middleware for Echo compatibility - Add ArchivedHandler to task categorization chain (archived tasks go to cancelled_tasks column) - Add 6 new integration tests: - RecurringTaskLifecycle: NextDueDate advancement for weekly/monthly tasks - MultiUserSharing: Complex sharing with user removal - TaskStateTransitions: All state transitions and kanban column changes - DateBoundaryEdgeCases: Threshold boundary testing - CascadeOperations: Residence deletion cascade effects - MultiUserOperations: Shared residence collaboration - Add single-purpose repository functions for kanban columns (GetOverdueTasks, GetDueSoonTasks, etc.) - Fix RemoveUser route param mismatch (userId -> user_id) - Fix determineExpectedColumn helper to correctly prioritize in_progress over overdue 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/i18n"
|
||||
"github.com/treytartt/casera-api/internal/apperrors"
|
||||
"github.com/treytartt/casera-api/internal/middleware"
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
"github.com/treytartt/casera-api/internal/services"
|
||||
@@ -23,91 +22,80 @@ func NewSubscriptionHandler(subscriptionService *services.SubscriptionService) *
|
||||
}
|
||||
|
||||
// GetSubscription handles GET /api/subscription/
|
||||
func (h *SubscriptionHandler) GetSubscription(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *SubscriptionHandler) GetSubscription(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
subscription, err := h.subscriptionService.GetSubscription(user.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, subscription)
|
||||
return c.JSON(http.StatusOK, subscription)
|
||||
}
|
||||
|
||||
// GetSubscriptionStatus handles GET /api/subscription/status/
|
||||
func (h *SubscriptionHandler) GetSubscriptionStatus(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *SubscriptionHandler) GetSubscriptionStatus(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
status, err := h.subscriptionService.GetSubscriptionStatus(user.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, status)
|
||||
return c.JSON(http.StatusOK, status)
|
||||
}
|
||||
|
||||
// GetUpgradeTrigger handles GET /api/subscription/upgrade-trigger/:key/
|
||||
func (h *SubscriptionHandler) GetUpgradeTrigger(c *gin.Context) {
|
||||
func (h *SubscriptionHandler) GetUpgradeTrigger(c echo.Context) error {
|
||||
key := c.Param("key")
|
||||
|
||||
trigger, err := h.subscriptionService.GetUpgradeTrigger(key)
|
||||
if err != nil {
|
||||
if errors.Is(err, services.ErrUpgradeTriggerNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.upgrade_trigger_not_found")})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, trigger)
|
||||
return c.JSON(http.StatusOK, trigger)
|
||||
}
|
||||
|
||||
// GetAllUpgradeTriggers handles GET /api/subscription/upgrade-triggers/
|
||||
func (h *SubscriptionHandler) GetAllUpgradeTriggers(c *gin.Context) {
|
||||
func (h *SubscriptionHandler) GetAllUpgradeTriggers(c echo.Context) error {
|
||||
triggers, err := h.subscriptionService.GetAllUpgradeTriggers()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, triggers)
|
||||
return c.JSON(http.StatusOK, triggers)
|
||||
}
|
||||
|
||||
// GetFeatureBenefits handles GET /api/subscription/features/
|
||||
func (h *SubscriptionHandler) GetFeatureBenefits(c *gin.Context) {
|
||||
func (h *SubscriptionHandler) GetFeatureBenefits(c echo.Context) error {
|
||||
benefits, err := h.subscriptionService.GetFeatureBenefits()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, benefits)
|
||||
return c.JSON(http.StatusOK, benefits)
|
||||
}
|
||||
|
||||
// GetPromotions handles GET /api/subscription/promotions/
|
||||
func (h *SubscriptionHandler) GetPromotions(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *SubscriptionHandler) GetPromotions(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
promotions, err := h.subscriptionService.GetActivePromotions(user.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, promotions)
|
||||
return c.JSON(http.StatusOK, promotions)
|
||||
}
|
||||
|
||||
// ProcessPurchase handles POST /api/subscription/purchase/
|
||||
func (h *SubscriptionHandler) ProcessPurchase(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *SubscriptionHandler) ProcessPurchase(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
var req services.ProcessPurchaseRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
|
||||
var subscription *services.SubscriptionResponse
|
||||
@@ -117,53 +105,48 @@ func (h *SubscriptionHandler) ProcessPurchase(c *gin.Context) {
|
||||
case "ios":
|
||||
// StoreKit 2 uses transaction_id, StoreKit 1 uses receipt_data
|
||||
if req.TransactionID == "" && req.ReceiptData == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.receipt_data_required")})
|
||||
return
|
||||
return apperrors.BadRequest("error.receipt_data_required")
|
||||
}
|
||||
subscription, err = h.subscriptionService.ProcessApplePurchase(user.ID, req.ReceiptData, req.TransactionID)
|
||||
case "android":
|
||||
if req.PurchaseToken == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.purchase_token_required")})
|
||||
return
|
||||
return apperrors.BadRequest("error.purchase_token_required")
|
||||
}
|
||||
subscription, err = h.subscriptionService.ProcessGooglePurchase(user.ID, req.PurchaseToken, req.ProductID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": i18n.LocalizedMessage(c, "message.subscription_upgraded"),
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"message": "message.subscription_upgraded",
|
||||
"subscription": subscription,
|
||||
})
|
||||
}
|
||||
|
||||
// CancelSubscription handles POST /api/subscription/cancel/
|
||||
func (h *SubscriptionHandler) CancelSubscription(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *SubscriptionHandler) CancelSubscription(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
subscription, err := h.subscriptionService.CancelSubscription(user.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": i18n.LocalizedMessage(c, "message.subscription_cancelled"),
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"message": "message.subscription_cancelled",
|
||||
"subscription": subscription,
|
||||
})
|
||||
}
|
||||
|
||||
// RestoreSubscription handles POST /api/subscription/restore/
|
||||
func (h *SubscriptionHandler) RestoreSubscription(c *gin.Context) {
|
||||
user := c.MustGet(middleware.AuthUserKey).(*models.User)
|
||||
func (h *SubscriptionHandler) RestoreSubscription(c echo.Context) error {
|
||||
user := c.Get(middleware.AuthUserKey).(*models.User)
|
||||
|
||||
var req services.ProcessPurchaseRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return apperrors.BadRequest("error.invalid_request")
|
||||
}
|
||||
|
||||
// Same logic as ProcessPurchase - validates receipt/token and restores
|
||||
@@ -178,12 +161,11 @@ func (h *SubscriptionHandler) RestoreSubscription(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": i18n.LocalizedMessage(c, "message.subscription_restored"),
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"message": "message.subscription_restored",
|
||||
"subscription": subscription,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user