package accounts import ( "errors" "fmt" "strconv" "strings" "github.com/gin-gonic/gin" "github.com/google/uuid" "ruben/inventory2/internal/consts" "ruben/inventory2/internal/domains/accounts" "ruben/inventory2/internal/logging" "ruben/inventory2/internal/server/auth" "ruben/inventory2/internal/server/response" "ruben/inventory2/internal/server/sse" ) type accountSubrouter struct { log *logging.Logger accts *accounts.Store pub *sse.UpdateNotificationPublisher } func Routes( r *gin.RouterGroup, logger *logging.Logger, accts *accounts.Store, pub *sse.UpdateNotificationPublisher, ) { as := &accountSubrouter{ log: logger, accts: accts, pub: pub, } r.POST("", response.Handler(as.createAccount)) r.PUT("/:acctID/platforms/:platform/order-index", pub.Publish("/:acctID/platforms"), response.Handler(as.setOrderOfPlatformOnAccountPage)) syncGroups := r.Group("/:acctID/inventory/sync-groups") syncGroups.POST("", pub.Publish("/:acctID/inventory/sync-groups"), response.Handler(as.saveNewSyncGroup)) draftListings := syncGroups.Group("/draft/listings", pub.Publish("/:acctID/inventory/sync-groups/draft/listings")) draftListings.POST("", response.Handler(as.createSyncGroupListingDraft)) draftListings.PUT("/:orderIndex/shop", response.Handler(as.setShopInSyncGroupListingDraft)) draftListings.PUT("/:orderIndex/listing", response.Handler(as.setListingInSyncGroupListingDraft)) draftListings.DELETE("/:orderIndex", response.Handler(as.deleteSyncGroupListingDraft)) } // POST / func (s *accountSubrouter) createAccount(c *gin.Context) (response.Response, error) { r := c.Request ctx := r.Context() // TODO: get email from the user's account profile (how?) - will need to flesh out the account creation process later on email := uuid.NewString() + "@example.com" userID := auth.GetIdentity(ctx).User.UserID _, err := s.accts.CreateAccount(ctx, userID, email) if err != nil { if errors.Is(err, consts.ErrConflict) { return nil, response.Conflict(). Msg("user already has an account") } return nil, response.Errorf("failed to create account: %w", err) } return response.StatusCreated(). // force a reload to ensure an sse connection is made. // user should automatically be redirected to the appropriate page based on their current location and/or login/account statuses. HXRefresh("true"), nil } // POST /:acctID/inventory/sync-groups/draft/listings func (s *accountSubrouter) createSyncGroupListingDraft(c *gin.Context) (response.Response, error) { r := c.Request ctx := r.Context() acctID := auth.GetIdentity(ctx).Account.AccountID if _, err := s.accts.CreateSyncGroupListingDraft(ctx, acctID); err != nil { return nil, response.Errorf("failed to create new listing draft: %w", err) } return response.StatusCreated(), nil } // PUT /:acctID/inventory/sync-groups/draft/listings/{orderIndex}/shop // @platform string // @shopID string func (s *accountSubrouter) setShopInSyncGroupListingDraft(c *gin.Context) (response.Response, error) { r := c.Request ctx := r.Context() acctID := auth.GetIdentity(ctx).Account.AccountID orderIndex, err := getOrderIndexForSyncGroupListingDraftFromPath(c) if err != nil { return nil, err } platformStr := r.FormValue("platform") platform, err := accounts.NewPlatform(platformStr) if err != nil { return nil, response.BadRequest(). Msgf("unrecognized platform: %s", platformStr) } shopID := r.FormValue("shop-id") if shopID == "" { return nil, response.BadRequest(). Msg("no shop-id provided") } if err := s.accts.SetShopInSyncGroupListingDraft(ctx, acctID, orderIndex, platform, shopID); err != nil { return nil, response.Errorf("failed to set shop: %w", response.ErrorFromConstant(err)) } return response.StatusOK(), nil } // PUT /:acctID/inventory/sync-groups/draft/listings/{orderIndex}/listing func (s *accountSubrouter) setListingInSyncGroupListingDraft(c *gin.Context) (response.Response, error) { r := c.Request ctx := r.Context() acctID := auth.GetIdentity(ctx).Account.AccountID orderIndex, err := getOrderIndexForSyncGroupListingDraftFromPath(c) if err != nil { return nil, err } listingID := r.FormValue("listing-id") if listingID == "" { return nil, response.BadRequest(). Msg("no listing-id provided") } if err := s.accts.SetListingInSyncGroupListingDraft(ctx, acctID, orderIndex, listingID); err != nil { return nil, response.Errorf("failed to set listing: %w", response.ErrorFromConstant(err)) } return response.StatusOK(), nil } // DELETE /:acctID/inventory/sync-groups/draft/listings/{orderIndex} func (s *accountSubrouter) deleteSyncGroupListingDraft(c *gin.Context) (response.Response, error) { r := c.Request ctx := r.Context() acctID := auth.GetIdentity(ctx).Account.AccountID orderIndex, err := getOrderIndexForSyncGroupListingDraftFromPath(c) if err != nil { return nil, err } if _, err := s.accts.DeleteSyncGroupListingDraft(ctx, acctID, orderIndex); err != nil { return nil, response.Errorf("failed to delete listing: %w", response.ErrorFromConstant(err)) } return response.StatusOK(), nil } // POST /:acctID/inventory/sync-groups func (s *accountSubrouter) saveNewSyncGroup(c *gin.Context) (response.Response, error) { r := c.Request ctx := r.Context() acctID := auth.GetIdentity(ctx).Account.AccountID if _, err := s.accts.SaveNewSyncGroup(ctx, acctID); err != nil { return nil, response.Errorf("failed to save new sync group: %w", response.ErrorFromConstant(err)) } return response.StatusCreated(), nil } func getOrderIndexForSyncGroupListingDraftFromPath(c *gin.Context) (int, error) { orderIndexStr := c.Param("orderIndex") if orderIndexStr == "" { return 0, response.NotFound(). Msg("no orderIndex found") } orderIndex, err := strconv.Atoi(orderIndexStr) if err != nil { return 0, response.NotFound(). Msgf("invalid order index: %s", orderIndexStr) } return orderIndex, nil } // PUT /:acctID/platforms/:platform/order-index" // this endpoint is called when dragging a platform tab in the accounts page. func (s *accountSubrouter) setOrderOfPlatformOnAccountPage(c *gin.Context) (response.Response, error) { acctID := auth.GetIdentity(c).Account.AccountID // validate parameters var orderIndex int if v, ok := c.GetPostForm("order-index"); !ok { return nil, response.BadRequest(). Msg("no order-index provided") } else if i, err := strconv.Atoi(v); err != nil { return nil, response.BadRequest(). Msgf("order-index must be a non-negative integer: %s", v) } else if i < 0 { return nil, response.BadRequest(). Msgf("order-index must be a non-negative integer: %s", v) } else { orderIndex = i } var platform accounts.Platform if v := c.Param("platform"); v == "" { return nil, response.BadRequest(). Msg("no platform provided") } else if p, err := accounts.NewPlatform(v); err != nil { return nil, response.BadRequest(). Wrap(err). Msgf("unrecognized platform: %v", v) } else { platform = p } // update the order platforms, prevIndex, err := s.accts.SetOrderOfPlatformOnAccountPage(c, acctID, platform, orderIndex) if err != nil { return nil, fmt.Errorf("failed to save record: %w", err) } // emit events on all platforms updated, so the tabs can all refresh (including their logic) platformEvents := make([]string, max(orderIndex, prevIndex)-min(orderIndex, prevIndex)) if orderIndex > prevIndex { for i := range orderIndex - prevIndex { p := platforms[prevIndex+i] platformEvents[i] = fmt.Sprintf("accounts_%d_platforms_%s_order-index", acctID, lowerSnakeCase(p)) } } else if orderIndex < prevIndex { for i := range prevIndex - orderIndex { p := platforms[orderIndex+1+i] platformEvents[i] = fmt.Sprintf("accounts_%d_platforms_%s_order-index", acctID, lowerSnakeCase(p)) } } if err := s.pub.Push(c, acctID, platformEvents...); err != nil { s.log.Errorf("failed to publish platform order-index events: %v", err) } return response.StatusNoContent(), nil } func lowerSnakeCase(s accounts.Platform) string { return strings.ToLower(strings.Join(strings.Split(string(s), " "), "_")) }