package facebook // Repository provides GORM-based persistence for Facebook and Instagram channel data. // Reference: Chatwoot ActiveRecord for ChannelFacebook + ChannelInstagram with callbacks: // - ChannelFacebook: before_validation :ensure_valid_page_token, after_destroy :delete_facebook_page // - ChannelInstagram: similar lifecycle hooks via connected FB Page // // Design: Repository pattern abstracts GORM operations, matching gochat's // existing repository pattern used by Telegram and other channel providers. import ( "context" "fmt" channelmodel "github.com/gochat/gochat/internal/model/channel" "gorm.io/gorm" applogger "github.com/gochat/gochat/pkg/logger" ) // Repository handles Facebook/Instagram channel database operations. type Repository struct { db *gorm.DB } // NewRepository creates a Facebook/Instagram repository with GORM DB. func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } // === Facebook Channel CRUD === // CreateFacebook persists a new ChannelFacebook record. func (r *Repository) CreateFacebook(ctx context.Context, channel *channelmodel.ChannelFacebook) error { if err := r.db.WithContext(ctx).Create(channel).Error; err != nil { return fmt.Errorf("failed to create ChannelFacebook: %w", err) } applogger.L().Info("ChannelFacebook created", "id", channel.ID, "account_id", channel.AccountID, "page_id", channel.PageID, "page_name", channel.PageName, ) return nil } // UpdateFacebook modifies a ChannelFacebook record. func (r *Repository) UpdateFacebook(ctx context.Context, channel *channelmodel.ChannelFacebook) error { result := r.db.WithContext(ctx).Save(channel) if result.Error != nil { return fmt.Errorf("failed to update ChannelFacebook: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("ChannelFacebook not found (id=%d)", channel.ID) } applogger.L().Info("ChannelFacebook updated", "id", channel.ID) return nil } // UpdateFacebookPageAccessToken updates only the page_access_token field. func (r *Repository) UpdateFacebookPageAccessToken(ctx context.Context, channelID uint, newToken string) error { result := r.db.WithContext(ctx). Model(&channelmodel.ChannelFacebook{}). Where("id = ?", channelID). Update("page_access_token", newToken) if result.Error != nil { return fmt.Errorf("failed to update page_access_token: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("ChannelFacebook not found (id=%d)", channelID) } applogger.L().Info("ChannelFacebook page_access_token updated", "id", channelID) return nil } // GetFacebookByID retrieves a ChannelFacebook by its primary key. func (r *Repository) GetFacebookByID(ctx context.Context, id uint) (*channelmodel.ChannelFacebook, error) { var channel channelmodel.ChannelFacebook if err := r.db.WithContext(ctx).First(&channel, id).Error; err != nil { return nil, fmt.Errorf("failed to get ChannelFacebook by id: %w", err) } return &channel, nil } // GetFacebookByPageID retrieves a ChannelFacebook by its Facebook Page ID. func (r *Repository) GetFacebookByPageID(ctx context.Context, pageID string) (*channelmodel.ChannelFacebook, error) { var channel channelmodel.ChannelFacebook if err := r.db.WithContext(ctx). Where("page_id = ?", pageID). First(&channel).Error; err != nil { return nil, fmt.Errorf("failed to get ChannelFacebook by page_id: %w", err) } return &channel, nil } // DeleteFacebook removes a ChannelFacebook record. func (r *Repository) DeleteFacebook(ctx context.Context, id uint) error { result := r.db.WithContext(ctx).Delete(&channelmodel.ChannelFacebook{}, id) if result.Error != nil { return fmt.Errorf("failed to delete ChannelFacebook: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("ChannelFacebook not found (id=%d)", id) } applogger.L().Info("ChannelFacebook deleted", "id", id) return nil } // === Instagram Channel CRUD === // CreateInstagram persists a new ChannelInstagram record. func (r *Repository) CreateInstagram(ctx context.Context, channel *channelmodel.ChannelInstagram) error { if err := r.db.WithContext(ctx).Create(channel).Error; err != nil { return fmt.Errorf("failed to create ChannelInstagram: %w", err) } applogger.L().Info("ChannelInstagram created", "id", channel.ID, "account_id", channel.AccountID, "ig_account_id", channel.InstagramAccountID, "ig_username", channel.InstagramAccountName, ) return nil } // UpdateInstagram modifies a ChannelInstagram record. func (r *Repository) UpdateInstagram(ctx context.Context, channel *channelmodel.ChannelInstagram) error { result := r.db.WithContext(ctx).Save(channel) if result.Error != nil { return fmt.Errorf("failed to update ChannelInstagram: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("ChannelInstagram not found (id=%d)", channel.ID) } applogger.L().Info("ChannelInstagram updated", "id", channel.ID) return nil } // UpdateInstagramPageAccessToken updates only the page_access_token field. func (r *Repository) UpdateInstagramPageAccessToken(ctx context.Context, channelID uint, newToken string) error { result := r.db.WithContext(ctx). Model(&channelmodel.ChannelInstagram{}). Where("id = ?", channelID). Update("page_access_token", newToken) if result.Error != nil { return fmt.Errorf("failed to update page_access_token: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("ChannelInstagram not found (id=%d)", channelID) } applogger.L().Info("ChannelInstagram page_access_token updated", "id", channelID) return nil } // GetInstagramByID retrieves a ChannelInstagram by its primary key. func (r *Repository) GetInstagramByID(ctx context.Context, id uint) (*channelmodel.ChannelInstagram, error) { var channel channelmodel.ChannelInstagram if err := r.db.WithContext(ctx).First(&channel, id).Error; err != nil { return nil, fmt.Errorf("failed to get ChannelInstagram by id: %w", err) } return &channel, nil } // GetInstagramByAccountID retrieves a ChannelInstagram by Instagram Account ID. func (r *Repository) GetInstagramByAccountID(ctx context.Context, igAccountID string) (*channelmodel.ChannelInstagram, error) { var channel channelmodel.ChannelInstagram if err := r.db.WithContext(ctx). Where("instagram_account_id = ?", igAccountID). First(&channel).Error; err != nil { return nil, fmt.Errorf("failed to get ChannelInstagram by ig_account_id: %w", err) } return &channel, nil } // DeleteInstagram removes a ChannelInstagram record. func (r *Repository) DeleteInstagram(ctx context.Context, id uint) error { result := r.db.WithContext(ctx).Delete(&channelmodel.ChannelInstagram{}, id) if result.Error != nil { return fmt.Errorf("failed to delete ChannelInstagram: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("ChannelInstagram not found (id=%d)", id) } applogger.L().Info("ChannelInstagram deleted", "id", id) return nil } // === Cross-Channel Queries === // GetFacebookChannelsByAccount retrieves all ChannelFacebook records for an account. func (r *Repository) GetFacebookChannelsByAccount(ctx context.Context, accountID uint) ([]channelmodel.ChannelFacebook, error) { var channels []channelmodel.ChannelFacebook if err := r.db.WithContext(ctx). Where("account_id = ?", accountID). Find(&channels).Error; err != nil { return nil, fmt.Errorf("failed to list ChannelFacebook by account: %w", err) } return channels, nil } // GetInstagramChannelsByAccount retrieves all ChannelInstagram records for an account. func (r *Repository) GetInstagramChannelsByAccount(ctx context.Context, accountID uint) ([]channelmodel.ChannelInstagram, error) { var channels []channelmodel.ChannelInstagram if err := r.db.WithContext(ctx). Where("account_id = ?", accountID). Find(&channels).Error; err != nil { return nil, fmt.Errorf("failed to list ChannelInstagram by account: %w", err) } return channels, nil } // GetInstagramByConnectedFBPageID retrieves a ChannelInstagram by connected FB Page ID. // Used when processing Facebook webhooks that contain Instagram DM events. func (r *Repository) GetInstagramByConnectedFBPageID(ctx context.Context, fbPageID string) (*channelmodel.ChannelInstagram, error) { var channel channelmodel.ChannelInstagram if err := r.db.WithContext(ctx). Where("connected_fb_page_id = ?", fbPageID). First(&channel).Error; err != nil { return nil, fmt.Errorf("failed to get ChannelInstagram by connected_fb_page_id: %w", err) } return &channel, nil }