package accounts import ( "context" "errors" "fmt" "slices" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "ruben/inventory2/consts" ) type ( SyncGroup struct { SyncGroupIDs Listings []SyncGroupListing } SyncGroupListing struct { SyncGroupIDs AccountShopIDs ListingID string OrderIndex int } SyncGroupIDs struct { AccountIDs SyncGroupID int64 } // TODO: the id ACTUALLY is the account_id and order_index SyncGroupListingDraft struct { AccountShopIDs ListingID string OrderIndex int } ) func (db *Store) CreateSyncGroupListingDraft(ctx context.Context, acctID int64) (orderIndex int, err error) { rows, err := db.db.Query( ctx, ` WITH new_order_index AS ( SELECT COALESCE(MAX(order_index), -1) + 1 AS order_index FROM sync_group_listing_drafts WHERE account_id = @account_id ) INSERT INTO sync_group_listing_drafts ( account_id, order_index ) SELECT @account_id, order_index FROM new_order_index RETURNING order_index `, pgx.NamedArgs{ "account_id": acctID, }, ) if err != nil { return 0, fmt.Errorf("failed to perform query: %w", err) } if orderIndex, err = pgx.CollectExactlyOneRow(rows, pgx.RowTo[int]); err != nil { return 0, fmt.Errorf("failed to scan rows: %w", err) } return orderIndex, nil } func (db *Store) CreateMockSyncGroupListingDraft(ctx context.Context, acctID int64) (orderIndex int, err error) { rows, err := db.db.Query( ctx, ` WITH max_order_index(max_index) AS ( SELECT MAX(order_index) AS max_index FROM mock.sync_group_listing_draft_order_indexes ) INSERT INTO mock.sync_group_listing_draft_order_indexes ( account_id, order_index ) SELECT @account_id, COALESCE(max_index, -1) + 1 FROM max_order_index RETURNING order_index `, pgx.NamedArgs{ "account_id": acctID, }, ) if err != nil { return 0, fmt.Errorf("failed to perform query: %w", err) } if orderIndex, err = pgx.CollectExactlyOneRow(rows, pgx.RowTo[int]); err != nil { return 0, fmt.Errorf("failed to scan rows: %w", err) } return orderIndex, nil } func (db *Store) GetSyncGroupListingDraft(ctx context.Context, acctID int64, orderIndex int) (SyncGroupListingDraft, error) { rows, err := db.db.Query( ctx, ` SELECT platform, shop_id, listing_id FROM sync_group_listing_drafts WHERE account_id = @account_id AND order_index = @order_index `, pgx.NamedArgs{ "account_id": acctID, "order_index": orderIndex, }, ) if err != nil { return SyncGroupListingDraft{}, fmt.Errorf("failed to perform query: %w", err) } r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[struct { Platform *Platform Shop_id *string Listing_id *string Order_index *int }]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return SyncGroupListingDraft{}, consts.ErrNotFound } return SyncGroupListingDraft{}, fmt.Errorf("failed to scan rows: %w", err) } return SyncGroupListingDraft{ AccountShopIDs: AccountShopIDs{ AccountIDs: AccountIDs{ AccountID: acctID, }, Platform: deref(r.Platform), ShopID: deref(r.Shop_id), }, ListingID: deref(r.Listing_id), OrderIndex: orderIndex, }, nil } func (db *Store) GetMockSyncGroupListingDraft(ctx context.Context, acctID int64, orderIndex int) (SyncGroupListingDraft, error) { tx, err := db.db.BeginTx(ctx, pgx.TxOptions{ AccessMode: pgx.ReadOnly, }) if err != nil { return SyncGroupListingDraft{}, fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback(ctx) rows, err := tx.Query( ctx, ` SELECT true FROM mock.sync_group_listing_draft_order_indexes WHERE account_id = @account_id AND order_index = @order_index `, pgx.NamedArgs{ "account_id": acctID, "order_index": orderIndex, }, ) if err != nil { return SyncGroupListingDraft{}, fmt.Errorf("failed to scan rows: %w", err) } if _, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[bool]); err != nil { return SyncGroupListingDraft{}, fmt.Errorf("failed to scan rows: %w", err) } for _, info := range getAllMockShopSchemaInfos() { rows, err := tx.Query( ctx, fmt.Sprintf( ` SELECT shop_id, listing_id FROM %s WHERE account_id = @account_id AND order_index = @order_index `, info.syncGroupListingDraftsTable, ), pgx.NamedArgs{ "account_id": acctID, "order_index": orderIndex, }, ) if err != nil { return SyncGroupListingDraft{}, fmt.Errorf("failed to perform query: %w", err) } v, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[struct { Shop_id pgtype.Text Listing_id pgtype.Text }]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { continue } return SyncGroupListingDraft{}, fmt.Errorf("faild to scan row: %w", err) } return SyncGroupListingDraft{ AccountShopIDs: NewAccountIDs(acctID). ShopID(info.platform, v.Shop_id.String), ListingID: v.Listing_id.String, OrderIndex: orderIndex, }, nil } if err := tx.Commit(ctx); err != nil { return SyncGroupListingDraft{}, fmt.Errorf("failed to commit transaction: %w", err) } return SyncGroupListingDraft{ AccountShopIDs: NewAccountIDs(acctID). ShopID("", ""), OrderIndex: orderIndex, }, nil } func (db *Store) SetShopInSyncGroupListingDraft(ctx context.Context, acctID int64, orderIndex int, platform Platform, shopID string) error { tag, err := db.db.Exec( ctx, ` UPDATE sync_group_listing_drafts SET platform = @platform, shop_id = @shop_id, listing_id = NULL WHERE account_id = @account_id AND order_index = @order_index `, pgx.NamedArgs{ "account_id": acctID, "order_index": orderIndex, "platform": platform, "shop_id": shopID, }, ) if err != nil { return fmt.Errorf("failed to perform query: %w", err) } if tag.RowsAffected() != 1 { return consts.ErrNotFound } return nil } func (db *Store) SetShopInMockSyncGroupListingDraft(ctx context.Context, acctID int64, orderIndex int, platform Platform, shopID string) error { info, ok := getMockShopSchemaInfo(platform) if !ok { return fmt.Errorf("%w: unrecognized platform: %s", consts.ErrNotFound, platform) } tx, err := db.db.Begin(ctx) if err != nil { return fmt.Errorf("failed to start transaction: %w", err) } defer tx.Rollback(ctx) _, err = tx.Exec( ctx, ` DELETE FROM mock.sync_group_listing_draft_order_indexes WHERE account_id = @account_id AND order_index = @order_index RETURNING account_id, order_index `, pgx.NamedArgs{ "account_id": acctID, "order_index": orderIndex, }, ) if err != nil { return fmt.Errorf("failed to perform query deleting existing index: %w", err) } _, err = tx.Exec( ctx, ` INSERT INTO mock.sync_group_listing_draft_order_indexes ( account_id, order_index ) VALUES ( @account_id, @order_index ) `, pgx.NamedArgs{ "account_id": acctID, "order_index": orderIndex, }, ) if err != nil { // TODO: handle error where account doesn't exist? return fmt.Errorf("failed to perform query inserting new index: %w", err) } _, err = tx.Exec( ctx, fmt.Sprintf( ` INSERT INTO %s ( account_id, order_index, shop_id ) VALUES ( @account_id, @order_index, @shop_id ) `, info.syncGroupListingDraftsTable, ), pgx.NamedArgs{ "account_id": acctID, "order_index": orderIndex, "shop_id": shopID, }, ) if err != nil { return fmt.Errorf("failed to perform query inserting specific shop index: %w", err) } if err := tx.Commit(ctx); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } return nil } func (db *Store) SetListingInSyncGroupListingDraft(ctx context.Context, acctID int64, orderIndex int, listingID string) error { tag, err := db.db.Exec( ctx, ` UPDATE sync_group_listing_drafts SET listing_id = @listing_id WHERE account_id = @account_id AND order_index = @order_index `, pgx.NamedArgs{ "account_id": acctID, "order_index": orderIndex, "listing_id": listingID, }, ) if err != nil { return fmt.Errorf("failed to perform query: %w", err) } if tag.RowsAffected() != 1 { return consts.ErrNotFound } return nil } func (db *Store) SetListingInMockSyncGroupListingDraft(ctx context.Context, acctID int64, orderIndex int, listingID string) error { for _, info := range getAllMockShopSchemaInfos() { tag, err := db.db.Exec( ctx, fmt.Sprintf( ` UPDATE %s SET listing_id = @listing_id WHERE account_id = @account_id AND order_index = @order_index `, info.syncGroupListingDraftsTable, ), pgx.NamedArgs{ "account_id": acctID, "order_index": orderIndex, "listing_id": listingID, }, ) if err != nil { return fmt.Errorf("failed to perform query: %w", err) } if tag.RowsAffected() > 0 { return nil } } return consts.ErrNotFound } func (db *Store) DeleteSyncGroupListingDraft(ctx context.Context, acctID int64, orderIndex int) (numOfRows int, err error) { rows, err := db.db.Query( ctx, ` WITH deleted_draft AS ( DELETE FROM sync_group_listing_drafts WHERE account_id = @account_id AND order_index = @order_index RETURNING true AS found ) SELECT COUNT(*) as num_rows, COALESCE(dd.found, FALSE) as found FROM sync_group_listing_drafts ld LEFT JOIN deleted_draft dd ON TRUE WHERE account_id = @account_id GROUP BY ld.account_id, dd.found `, pgx.NamedArgs{ "account_id": acctID, "order_index": orderIndex, }, ) if err != nil { return 0, fmt.Errorf("failed to perform query: %w", err) } type Row struct { Found bool Num_rows int } r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return 0, consts.ErrNotFound } return 0, fmt.Errorf("failed to scan rows: %w", err) } if !r.Found { return 0, consts.ErrNotFound } return r.Num_rows, nil } func (db *Store) DeleteMockSyncGroupListingDraft(ctx context.Context, acctID int64, orderIndex int) error { tag, err := db.db.Exec( ctx, ` DELETE FROM mock.sync_group_listing_draft_order_indexes WHERE account_id = @account_id AND order_index = @order_index `, pgx.NamedArgs{ "account_id": acctID, "order_index": orderIndex, }, ) if err != nil { return fmt.Errorf("failed to perform query: %w", err) } if tag.RowsAffected() != 1 { return consts.ErrNotFound } return nil } func (db *Store) GetSyncGroupListingDrafts(ctx context.Context, acctID int64) ([]SyncGroupListingDraft, error) { rows, err := db.db.Query( ctx, ` SELECT order_index, platform, shop_id, listing_id FROM sync_group_listing_drafts WHERE account_id = @account_id ORDER BY order_index ASC `, pgx.NamedArgs{ "account_id": acctID, }, ) if err != nil { return nil, fmt.Errorf("failed to perform query: %w", err) } type Row struct { Order_index int Platform *Platform Shop_id *string Listing_id *string } rs, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[Row]) if err != nil { return nil, fmt.Errorf("failed to scan rows: %w", err) } listings := make([]SyncGroupListingDraft, len(rs)) for i, r := range rs { listings[i] = SyncGroupListingDraft{ AccountShopIDs: AccountShopIDs{ AccountIDs: AccountIDs{ AccountID: acctID, }, Platform: deref(r.Platform), ShopID: deref(r.Shop_id), }, ListingID: deref(r.Listing_id), OrderIndex: r.Order_index, } } slices.SortFunc(listings, func(a, b SyncGroupListingDraft) int { return a.OrderIndex - b.OrderIndex }) return listings, nil } func (db *Store) GetMockSyncGroupListingDrafts(ctx context.Context, acctID int64) ([]SyncGroupListingDraft, error) { tx, err := db.db.BeginTx(ctx, pgx.TxOptions{ AccessMode: pgx.ReadOnly, }) if err != nil { return nil, fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback(ctx) rows, err := tx.Query( ctx, ` SELECT order_index FROM mock.sync_group_listing_draft_order_indexes WHERE account_id = @account_id `, pgx.NamedArgs{ "account_id": acctID, }, ) if err != nil { return nil, fmt.Errorf("failed to perform query: %w", err) } indexes, err := pgx.CollectRows(rows, pgx.RowTo[int]) if err != nil { return nil, fmt.Errorf("failed to scan rows: %w", err) } listingsByOrderIndex := make(map[int]SyncGroupListingDraft, len(indexes)) for _, n := range indexes { listingsByOrderIndex[n] = SyncGroupListingDraft{ AccountShopIDs: NewAccountIDs(acctID). ShopID("", ""), OrderIndex: n, } } for _, info := range getAllMockShopSchemaInfos() { rows, err := tx.Query( ctx, fmt.Sprintf( ` SELECT shop_id, order_index, listing_id FROM %s WHERE account_id = @account_id `, info.syncGroupListingDraftsTable, ), pgx.NamedArgs{ "account_id": acctID, }, ) if err != nil { return nil, fmt.Errorf("failed to perform query: %w", err) } vs, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[struct { Shop_id pgtype.Text Order_index pgtype.Int8 Listing_id pgtype.Text }]) if err != nil { return nil, fmt.Errorf("failed to scan rows: %w", err) } for _, v := range vs { n := int(v.Order_index.Int64) l := listingsByOrderIndex[n] l.Platform = info.platform l.ShopID = v.Shop_id.String l.ListingID = v.Listing_id.String listingsByOrderIndex[n] = l } } listings := make([]SyncGroupListingDraft, 0, len(listingsByOrderIndex)) for _, v := range listingsByOrderIndex { listings = append(listings, v) } slices.SortFunc(listings, func(a, b SyncGroupListingDraft) int { return a.OrderIndex - b.OrderIndex }) if err := tx.Commit(ctx); err != nil { return nil, fmt.Errorf("failed to commit transaction: %w", err) } return listings, nil } func (db *Store) SaveNewSyncGroup(ctx context.Context, acctID int64) (SyncGroup, error) { txn, err := db.db.Begin(ctx) if err != nil { return SyncGroup{}, fmt.Errorf("failed to start transaction: %w", err) } defer txn.Rollback(ctx) rows, err := txn.Query( ctx, "SELECT COUNT(*) FROM sync_group_listing_drafts WHERE account_id = @account_id", pgx.NamedArgs{ "account_id": acctID, }, ) if err != nil { return SyncGroup{}, fmt.Errorf("failed to perform query: %w", err) } numDrafts, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[int]) if err != nil { return SyncGroup{}, fmt.Errorf("failed to scan rows: %w", err) } if numDrafts < 2 { return SyncGroup{}, fmt.Errorf("%w: insufficient listings: must be at least 2: %d", consts.ErrConflict, numDrafts) } rows, err = txn.Query( ctx, ` WITH deleted_sync_group_listing_drafts AS ( DELETE FROM sync_group_listing_drafts WHERE account_id = @account_id RETURNING order_index, platform, shop_id, listing_id ), new_sync_group AS ( INSERT INTO sync_groups ( account_id ) VALUES ( @account_id ) RETURNING sync_group_id ) INSERT INTO sync_groups_listings ( sync_group_id, order_index, platform, shop_id, listing_id ) SELECT sync_group_id, order_index, platform, shop_id, listing_id FROM new_sync_group JOIN deleted_sync_group_listing_drafts ON TRUE RETURNING sync_group_id, order_index, platform, shop_id, listing_id `, pgx.NamedArgs{ "account_id": acctID, }, ) if err != nil { return SyncGroup{}, fmt.Errorf("failed to perform query: %w", err) } rs, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[struct { Sync_group_id int64 Order_index int Platform Platform Shop_id string Listing_id string }]) if err != nil { return SyncGroup{}, fmt.Errorf("failed to scan rows: %w", err) } if err := txn.Commit(ctx); err != nil { return SyncGroup{}, fmt.Errorf("failed to commit transaction: %w", err) } listings := make([]SyncGroupListing, len(rs)) for i, r := range rs { listings[i] = SyncGroupListing{ SyncGroupIDs: SyncGroupIDs{ AccountIDs: AccountIDs{ AccountID: acctID, }, SyncGroupID: r.Sync_group_id, }, AccountShopIDs: AccountShopIDs{ AccountIDs: AccountIDs{ AccountID: acctID, }, Platform: r.Platform, ShopID: r.Shop_id, }, ListingID: r.Listing_id, OrderIndex: r.Order_index, } } slices.SortFunc(listings, func(a, b SyncGroupListing) int { return a.OrderIndex - b.OrderIndex }) return SyncGroup{ SyncGroupIDs: SyncGroupIDs{ AccountIDs: AccountIDs{ AccountID: acctID, }, SyncGroupID: listings[0].SyncGroupID, }, Listings: listings, }, nil } func deref[T any](ptr *T) T { if ptr == nil { var zero T return zero } return *ptr }