package accounts import ( "errors" "fmt" "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/param" "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)) platformGroup := r.Group("/:acctID/platforms/:platform", pub.Publish("/:acctID/platforms")) platformGroup.PUT("/order-index", response.Handler(as.setOrderOfPlatformOnAccountPage)) platformGroup.POST("/shops/mocks", response.Handler(as.createMockShop)) mockShops := platformGroup.Group("/shops/mocks/:shop-id") mockShops.POST("/listings", response.Handler(as.addMockListing)) mockShops.PUT("/listings/:listing-id", response.Handler(as.updateMockListing)) mockShops.DELETE("/listings/:listing-id", response.Handler(as.deleteMockListing)) 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 var ( orderIndex int platform accounts.Platform shopID string ) err := param.Path("orderIndex", param.Int(&orderIndex)). Form("platform", param.Platform(&platform)). Unmarshal(c) if err != nil { return nil, err } 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 var ( orderIndex int listingID string ) err := param.Path("orderIndex", param.Int(&orderIndex)). Form("listing-id", param.Text(&listingID)). Unmarshal(c) if err != nil { return nil, err } 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 var ( orderIndex int ) err := param.Path("orderIndex", param.Int(&orderIndex)). Unmarshal(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 } // 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 ( platform accounts.Platform orderIndex int ) err := param.Path("platform", param.Platform(&platform)). Form("orderIndex", param.Int(&orderIndex)). Unmarshal(c) if err != nil { return nil, err } // 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 } // POST /:acctID/platforms/:platform/shops/mocks func (s *accountSubrouter) createMockShop(c *gin.Context) (response.Response, error) { acctID := auth.GetIdentity(c).Account.AccountID // validate parameters var ( platform accounts.Platform name string ) err := param.Path("platform", param.Platform(&platform)). Form("name", param.Text(&name)). Unmarshal(c) if err != nil { return nil, err } s.log.Warnf("not implemented: mock store created: account_id = %d; platform = %s; name = %s", acctID, platform, name) // create the mock account id, err := s.accts.CreateMockShop(c, acctID, platform, name) if err != nil { return nil, fmt.Errorf("failed to save record: %w", err) } location := fmt.Sprintf("/ui/accounts/%d/platforms/%s/mock-shops/%s", acctID, lowerSnakeCase(platform), id) var res response.Response if c.GetHeader("HX-Request") == "true" { res = response.StatusCreated() } else { res = response.SeeOther(location) } return res.HXLocation(location), nil } // POST /:acctID/platforms/:platform/shops/mocks/:shop-id/listings func (s *accountSubrouter) addMockListing(c *gin.Context) (response.Response, error) { acctID := auth.GetIdentity(c).Account.AccountID var ( platform accounts.Platform shopID string name string sku string description string count int ) err := param.Path("platform", param.Platform(&platform)). Path("shop-id", param.Text(&shopID)). Form("name", param.Text(&name)). Form("sku", param.Text(&sku)). Form("description", param.Text(&description)). Form("count", param.Int(&count)). Unmarshal(c) if err != nil { return nil, err } _, err = s.accts.CreateMockListing( c, accounts.MockListing{ AccountShopListingIDs: accounts.NewAccountIDs(acctID). ShopID(platform, shopID). ListingID(""), Name: name, SKU: sku, Description: description, Count: int64(count), }, ) if err != nil { return nil, fmt.Errorf("failed to create new listing: %w", err) } return response.StatusCreated(), nil } // PUT /:acctID/platforms/:platform/shops/mocks/:shop-id/listings/:listing-id func (s *accountSubrouter) updateMockListing(c *gin.Context) (response.Response, error) { acctID := auth.GetIdentity(c).Account.AccountID var ( platform accounts.Platform shopID string listingID string name string sku string description string count int ) err := param.Path("platform", param.Platform(&platform)). Path("shop-id", param.Text(&shopID)). Path("listing-id", param.Text(&listingID)). Form("name", param.Text(&name)). Form("sku", param.Text(&sku)). Form("description", param.Text(&description)). Form("count", param.Int(&count)). Unmarshal(c) if err != nil { return nil, err } err = s.accts.UpdateMockListing( c, accounts.MockListing{ AccountShopListingIDs: accounts.NewAccountIDs(acctID). ShopID(platform, shopID). ListingID(listingID), Name: name, SKU: sku, Description: description, Count: int64(count), }, ) if err != nil { return nil, fmt.Errorf("failed to update listing: %w", err) } return response.StatusOK(), nil } // DELETE /:acctID/platforms/:platform/shops/mocks/:shop-id/listings/:listing-id func (s *accountSubrouter) deleteMockListing(c *gin.Context) (response.Response, error) { acctID := auth.GetIdentity(c).Account.AccountID var ( platform accounts.Platform shopID string listingID string ) err := param.Path("platform", param.Platform(&platform)). Path("shop-id", param.Text(&shopID)). Path("listing-id", param.Text(&listingID)). Unmarshal(c) if err != nil { return nil, err } err = s.accts.DeleteMockListing( c, accounts.NewAccountIDs(acctID). ShopID(platform, shopID). ListingID(listingID), ) if err != nil { return nil, fmt.Errorf("failed to delete listing: %w", err) } return response.StatusNoContent(), nil } func lowerSnakeCase(s accounts.Platform) string { return strings.ToLower(strings.Join(strings.Split(string(s), " "), "_")) }