package postgres import ( "context" "time" "botpontos/internal/domain" ) func credentialCols(p string) string { return cols(p, "user_id", "api_email", "encrypted_password", "key_version", "last_validated_at", "last_error", "consecutive_failures", "last_failure_at", "updated_at") } type credentialScan struct { c domain.Credential } func (s *credentialScan) dest() []any { return []any{&s.c.UserID, &s.c.APIEmail, &s.c.EncryptedPassword, &s.c.KeyVersion, &s.c.LastValidatedAt, &s.c.LastError, &s.c.ConsecutiveFailures, &s.c.LastFailureAt, &s.c.UpdatedAt} } func (s *credentialScan) result() domain.Credential { return s.c } func readCredential(row scanner) (domain.Credential, error) { var cs credentialScan if err := row.Scan(cs.dest()...); err != nil { return domain.Credential{}, mapErr(err) } return cs.result(), nil } func (s *Store) GetCredential(ctx context.Context, userID int64) (domain.Credential, error) { return readCredential(s.pool.QueryRow(ctx, `SELECT `+credentialCols("")+` FROM point_credentials WHERE user_id = $1`, userID)) } // SaveCredential grava uma credencial já validada na PontoMais e zera o histórico de falhas. func (s *Store) SaveCredential(ctx context.Context, userID int64, email string, encrypted []byte, keyVersion int16, validatedAt time.Time) error { _, err := s.pool.Exec(ctx, ` INSERT INTO point_credentials (user_id, api_email, encrypted_password, key_version, last_validated_at) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (user_id) DO UPDATE SET api_email = EXCLUDED.api_email, encrypted_password = EXCLUDED.encrypted_password, key_version = EXCLUDED.key_version, last_validated_at = EXCLUDED.last_validated_at, last_error = NULL, consecutive_failures = 0, last_failure_at = NULL, updated_at = now()`, userID, email, encrypted, keyVersion, validatedAt) return mapErr(err) } // MarkCredentialValidated registra um teste bem-sucedido da credencial atual. func (s *Store) MarkCredentialValidated(ctx context.Context, userID int64, at time.Time) error { return expectOne(s.pool.Exec(ctx, ` UPDATE point_credentials SET last_validated_at = $2, last_error = NULL WHERE user_id = $1`, userID, at)) } // RecordCredentialFailure soma uma falha consecutiva e devolve o total (9.4). func (s *Store) RecordCredentialFailure(ctx context.Context, userID int64, message string, at time.Time) (int, error) { var failures int err := s.pool.QueryRow(ctx, ` UPDATE point_credentials SET consecutive_failures = consecutive_failures + 1, last_error = $2, last_failure_at = $3 WHERE user_id = $1 RETURNING consecutive_failures`, userID, message, at).Scan(&failures) return failures, mapErr(err) } // ResetCredentialFailures zera o contador após uma consulta bem-sucedida. func (s *Store) ResetCredentialFailures(ctx context.Context, userID int64) error { _, err := s.pool.Exec(ctx, ` UPDATE point_credentials SET consecutive_failures = 0, last_error = NULL WHERE user_id = $1 AND (consecutive_failures > 0 OR last_error IS NOT NULL)`, userID) return err } func (s *Store) ListCredentials(ctx context.Context) ([]domain.Credential, error) { rows, err := s.pool.Query(ctx, `SELECT `+credentialCols("")+` FROM point_credentials ORDER BY user_id`) return collect(rows, err, readCredential) } // UpdateCredentialCipher regrava a senha cifrada com outra chave (rotação). func (s *Store) UpdateCredentialCipher(ctx context.Context, userID int64, encrypted []byte, keyVersion int16) error { return expectOne(s.pool.Exec(ctx, ` UPDATE point_credentials SET encrypted_password = $2, key_version = $3, updated_at = now() WHERE user_id = $1`, userID, encrypted, keyVersion)) }