package accounts import ( "context" "errors" "fmt" "ruben/inventory2/internal/consts" "github.com/jackc/pgx/v5" ) type ( SyncGroup struct { SyncGroupIDs Listings []SyncGroupListing } SyncGroupListing struct { SyncGroupIDs AccountShopIDs ListingID string } SyncGroupIDs struct { AccountIDs SyncGroupID int64 } SyncGroupListingDraft struct { AccountShopIDs ListingID string } ) 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) 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 }]) 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), }, nil } func (db *Store) SetShopInSyncGroupListingDraft(ctx context.Context, acctID int64, orderIndex int, platform Platform, shopID string) error { tags, 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 tags.RowsAffected() != 1 { return consts.ErrNotFound } return nil } func (db *Store) SetListingInSyncGroupListingDraft(ctx context.Context, acctID int64, orderIndex int, listingID string) error { tags, 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 tags.RowsAffected() != 1 { return consts.ErrNotFound } return nil } func (db *Store) DeleteSyncGroupListingDraft(ctx context.Context, acctID int64, orderIndex int) (numOfRows int, err error) { rows, err := db.db.Query( ctx, ` WITH updated_drafts AS ( UPDATE sync_group_listing_drafts SET order_index = (order_index - 1) WHERE account_id = @account_id AND order_index > @order_index RETURNING order_index ), deleted_draft AS ( DELETE FROM sync_group_listing_drafts WHERE account_id = @account_id AND order_index = @order_index RETURNING true AS found ) SELECT COALESCE(dd.found, false) AS found, (COALESCE(MAX(ud.order_index), -1) + 1) AS num_rows FROM deleted_draft dd LEFT JOIN updated_drafts ud ON true GROUP BY ud.order_index, dd.found LIMIT 1 `, 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) 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 `, 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 _, r := range rs { listings[r.Order_index] = SyncGroupListingDraft{ AccountShopIDs: AccountShopIDs{ AccountIDs: AccountIDs{ AccountID: acctID, }, Platform: deref(r.Platform), ShopID: deref(r.Shop_id), }, ListingID: deref(r.Listing_id), } } return listings, nil } // TODO: test 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 _, r := range rs { listings[r.Order_index] = 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, } } 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 }