Compare commits

...

53 Commits

Author SHA1 Message Date
cf3671ea13 added validate 2025-02-25 14:47:46 +03:00
c22d9ecdd7 optimize cfg 2025-02-25 13:13:00 +03:00
5e260c79bd - 2024-10-24 18:19:27 +03:00
70f9a40a75 - 2024-10-24 18:17:57 +03:00
495ef84e61 added bitrix client method for create tag custom field type 2024-10-24 16:34:07 +03:00
d539b6bda4 skip fields if type file todo 2024-10-24 11:55:16 +03:00
c905095d4c some upd 2024-10-23 17:54:45 +03:00
a1b06a268f added supporting file type fields 2024-10-23 17:33:15 +03:00
1a70f30c4f update todo 2024-10-23 15:04:44 +03:00
806f202f0c all fields for addinf is Mandatory 2024-10-23 12:51:30 +03:00
63c0845d7a update after testing 2024-10-23 12:21:14 +03:00
f4b739015e now bitrix is strange need test tomorrow 2024-10-22 23:59:45 +03:00
b0fdafa3b4 now bitrix is strange need test tomorrow 2024-10-22 23:48:50 +03:00
9f99282f71 added todo for lead 2024-10-21 21:49:14 +03:00
b6d4ab132f update after testing lead contruct 2024-10-21 20:21:37 +03:00
8d6eda4bc9 added logic for create lead or deal 2024-10-21 19:56:45 +03:00
5912cb1528 added rework logic from amo for check duplicate contacts 2024-10-21 14:44:41 +03:00
970963278d added full main logic for create deal 2024-10-20 15:42:00 +03:00
91922628ad added init post deals worker to app 2024-10-18 17:54:14 +03:00
4000e61b90 prepare for finish deals worker 2024-10-18 17:11:28 +03:00
0bda592bbd update after test and add scope check 2024-10-18 12:19:58 +03:00
ed8959d942 update after true account testing 2024-10-18 01:00:41 +03:00
ec0e0cde43 update after test 2024-10-14 14:08:28 +03:00
7e8e882bfc rework rule logic from amo 2024-10-11 17:17:43 +03:00
9a7b3f26ea upddate with bitrix common 2024-10-11 10:26:28 +03:00
f45aa08206 added local deployment 2024-10-10 10:48:36 +03:00
530aed07e8 - 2024-09-30 10:51:24 +03:00
27f1265317 re-write queue_updater from amo to bitrix entity 2024-09-29 17:57:21 +03:00
578be84bb7 - 2024-09-27 17:38:15 +03:00
2d76b4bafd - 2024-09-27 16:47:08 +03:00
4c7c02bf15 - 2024-09-27 14:24:30 +03:00
64d8340035 - 2024-09-26 17:31:13 +03:00
9db181dfdf normalizade steps getting!!!!!!! 2024-09-26 17:21:38 +03:00
772df884b7 - 2024-09-26 15:43:25 +03:00
103c6caaf2 - 2024-09-26 13:56:17 +03:00
00fa7f10af - 2024-09-26 13:52:30 +03:00
c824f10dc0 - 2024-09-26 11:19:51 +03:00
e038b66109 added another entity type for statuses 2024-09-26 10:32:41 +03:00
aee8cdd1ee added interface for reflect formatting and added 3 methods, create deal,company,contact with it reflect method 2024-09-24 17:36:29 +03:00
7c6521b8b3 added method client for create company 2024-09-23 19:01:13 +03:00
65e334c6b6 added client method for create deal, and reflect got formating request data 2024-09-23 17:46:46 +03:00
a08a2f2ac0 added client method for added fields 2024-09-23 12:31:38 +03:00
25a43006d3 added getting current user client method 2024-09-22 15:52:26 +03:00
79dadbd93b added client method for get steps 2024-09-22 15:12:17 +03:00
70294255c8 added getting pipelines from bitrix 2024-09-22 12:31:07 +03:00
ddd462347d added getting pipelines from bitrix 2024-09-22 12:30:02 +03:00
c8257b1ea4 added fields response struct 2024-09-22 11:50:55 +03:00
42413a877f added all methods for get user fields 2024-09-22 11:13:04 +03:00
5b06e730ab some progress 2024-09-20 18:08:51 +03:00
345228a19b some progress 2024-09-20 17:41:33 +03:00
0ccee75e14 added GetUserList method client 2024-09-19 17:41:20 +03:00
c9dec5db34 start imp 2024-09-16 18:15:02 +03:00
7cb9694fb3 start imp 2024-09-16 18:14:36 +03:00
53 changed files with 5846 additions and 0 deletions

163
.gitignore vendored Normal file

@ -0,0 +1,163 @@
# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,goland,go
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,goland,go
### Go ###
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
### GoLand ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
.idea
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### GoLand Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/
# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml
# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$
# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml
# Azure Toolkit for IntelliJ plugin
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
.idea/**/azureSettings.xml
### VisualStudioCode ###
.vscode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,goland,go
main
.golangci.yml

35
cmd/main.go Normal file

@ -0,0 +1,35 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/app"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/initialize"
"syscall"
"go.uber.org/zap"
// import for automatically updating linter rules
_ "penahub.gitlab.yandexcloud.net/devops/linters/golang.git/pkg/dummy"
)
func main() {
logger, err := zap.NewProduction()
if err != nil {
fmt.Printf("Failed to initialize logger: %v\n", err)
os.Exit(1)
}
config, err := initialize.LoadConfig()
if err != nil {
logger.Fatal("Failed to load config", zap.Error(err))
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
if err = app.Run(ctx, *config, logger); err != nil {
logger.Fatal("App exited with error", zap.Error(err))
}
}

86
cmd/validator/main.go Normal file

@ -0,0 +1,86 @@
package main
import (
"context"
"errors"
"gitea.pena/PenaSide/common/validate"
"github.com/caarlos0/env/v8"
"log"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/initialize"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal"
)
func main() {
cfg, err := loadConfig()
if err != nil {
log.Fatalf("error loading config: %v", err)
}
err = validateNotEmpty(cfg)
if err != nil {
log.Fatalf("error validating config: %v", err)
}
err = validate.ValidateKafka([]string{cfg.KafkaBrokers}, cfg.KafkaTopicQueueBitrix)
if err != nil {
log.Fatalf("error validating Kafka: %v", err)
}
err = validate.ValidateRedis(cfg.RedisHost, cfg.RedisPassword, cfg.RedisDB)
if err != nil {
log.Fatalf("error validating Redis: %v", err)
}
err = validate.ValidateEncryptKeys(&cfg.ExternalCfg.EncryptCommon)
if err != nil {
log.Fatalf("error validating EncryptKeys: %v", err)
}
_, err = dal.NewBitrixDal(context.TODO(), cfg.PostgresURL)
if err != nil {
log.Fatalf("error connecting to postgres: %v", err)
}
return
}
func loadConfig() (initialize.Config, error) {
var cfg initialize.Config
if err := env.Parse(&cfg); err != nil {
return cfg, err
}
return cfg, nil
}
func validateNotEmpty(cfg initialize.Config) error {
if cfg.ClientHttpURL == "" {
return errors.New("client http url cannot be empty")
}
if cfg.BitrixIntegrationID == "" {
return errors.New("bitrix integration id cannot be empty")
}
if cfg.QuizIntegrationsRedirectURL == "" {
return errors.New("quiz integrations redirect url cannot be empty")
}
if cfg.OauthReturnURL == "" {
return errors.New("oauth returning url cannot be empty")
}
if cfg.BitrixIntegrationSecret == "" {
return errors.New("bitrix integration secret cannot be empty")
}
if cfg.ExternalCfg.EncryptCommon.PrivKey == "" {
return errors.New("no private key provided")
}
if cfg.ExternalCfg.EncryptCommon.PubKey == "" {
return errors.New("no public key provided")
}
return nil
}

@ -0,0 +1,51 @@
version: '3.8'
services:
zookeeper:
image: wurstmeister/zookeeper
ports:
- "2181:2181"
kafka:
image: wurstmeister/kafka
ports:
- "9092:9092"
environment:
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
KAFKA_CREATE_TOPICS: "test-topic:1:1"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
bitrix-postgres:
image: postgres
environment:
POSTGRES_PASSWORD: Redalert2
POSTGRES_USER: squiz
POSTGRES_DB: squiz
volumes:
- bitrix-postgres:/var/lib/postgresql/data
ports:
- 35432:5432
healthcheck:
test: pg_isready -U squiz
interval: 2s
timeout: 2s
retries: 10
bitrix-redis:
image: redis:latest
restart: always
ports:
- "6379:6379"
command: [ "redis-server", "--appendonly", "yes", "--requirepass", "admin" ]
volumes:
bitrix-postgres:

64
go.mod Normal file

@ -0,0 +1,64 @@
module penahub.gitlab.yandexcloud.net/backend/quiz/bitrix
go 1.23.2
toolchain go1.23.4
require (
gitea.pena/PenaSide/common v0.0.0-20250103085335-91ea31fee517
github.com/caarlos0/env/v8 v8.0.0
github.com/go-redis/redis/v8 v8.11.5
github.com/gofiber/fiber/v2 v2.52.5
github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.10.9
github.com/rs/xid v1.6.0
github.com/twmb/franz-go v1.18.0
go.uber.org/zap v1.27.0
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20241024141027-0c6f373d187c
penahub.gitlab.yandexcloud.net/devops/linters/golang.git v0.0.0-20240829220549-d35409b619a3
)
require (
github.com/ClickHouse/clickhouse-go v1.5.4 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minio-go/v7 v7.0.81 // indirect
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pioz/faker v1.7.3 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/tealeg/xlsx v1.0.5 // indirect
github.com/themakers/hlog v0.0.0-20191205140925-235e0e4baddf // indirect
github.com/twmb/franz-go/pkg/kmsg v1.9.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
go.mongodb.org/mongo-driver v1.13.1 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240202120244-c4ef330cfe5d // indirect
)

193
go.sum Normal file

@ -0,0 +1,193 @@
gitea.pena/PenaSide/common v0.0.0-20250103085335-91ea31fee517 h1:EgBe8VcdPwmxbSzYLndncP+NmR73uYuXxkTeDlEttEE=
gitea.pena/PenaSide/common v0.0.0-20250103085335-91ea31fee517/go.mod h1:91EuBCgcqgJ6mG36n2pds8sPwwfaJytLWOzY3h2YFKU=
github.com/ClickHouse/clickhouse-go v1.5.4 h1:cKjXeYLNWVJIx2J1K6H2CqyRmfwVJVY1OV1coaaFcI0=
github.com/ClickHouse/clickhouse-go v1.5.4/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/bkaradzic/go-lz4 v1.0.0 h1:RXc4wYsyz985CkXXeX04y4VnZFGG8Rd43pRaHsOXAKk=
github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4=
github.com/caarlos0/env/v8 v8.0.0 h1:POhxHhSpuxrLMIdvTGARuZqR4Jjm8AYmoi/JKlcScs0=
github.com/caarlos0/env/v8 v8.0.0/go.mod h1:7K4wMY9bH0esiXSSHlfHLX5xKGQMnkH5Fk4TDSSSzfo=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY/k9Os/UFtwERei2/XzGemhpGnBKNg=
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.81 h1:SzhMN0TQ6T/xSBu6Nvw3M5M8voM+Ht8RH3hE8S7zxaA=
github.com/minio/minio-go/v7 v7.0.81/go.mod h1:84gmIilaX4zcvAWWzJ5Z1WI5axN+hAbM5w25xf8xvC0=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pioz/faker v1.7.3 h1:Tez8Emuq0UN+/d6mo3a9m/9ZZ/zdfJk0c5RtRatrceM=
github.com/pioz/faker v1.7.3/go.mod h1:xSpay5w/oz1a6+ww0M3vfpe40pSIykeUPeWEc3TvVlc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tealeg/xlsx v1.0.5 h1:+f8oFmvY8Gw1iUXzPk+kz+4GpbDZPK1FhPiQRd+ypgE=
github.com/tealeg/xlsx v1.0.5/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM=
github.com/themakers/hlog v0.0.0-20191205140925-235e0e4baddf h1:TJJm6KcBssmbWzplF5lzixXl1RBAi/ViPs1GaSOkhwo=
github.com/themakers/hlog v0.0.0-20191205140925-235e0e4baddf/go.mod h1:1FsorU3vnXO9xS9SrhUp8fRb/6H/Zfll0rPt1i4GWaA=
github.com/twmb/franz-go v1.18.0 h1:25FjMZfdozBywVX+5xrWC2W+W76i0xykKjTdEeD2ejw=
github.com/twmb/franz-go v1.18.0/go.mod h1:zXCGy74M0p5FbXsLeASdyvfLFsBvTubVqctIaa5wQ+I=
github.com/twmb/franz-go/pkg/kmsg v1.9.0 h1:JojYUph2TKAau6SBtErXpXGC7E3gg4vGZMv9xFU/B6M=
github.com/twmb/franz-go/pkg/kmsg v1.9.0/go.mod h1:CMbfazviCyY6HM0SXuG5t9vOwYDHRCSrJJyBAe5paqg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk=
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240202120244-c4ef330cfe5d h1:gbaDt35HMDqOK84WYmDIlXMI7rstUcRqNttaT6Kx1do=
penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240202120244-c4ef330cfe5d/go.mod h1:lTmpjry+8evVkXWbEC+WMOELcFkRD1lFMc7J09mOndM=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20241024141027-0c6f373d187c h1:wViTf9OITkoGW/H5zB9kNIxDeEAPaoPOXZRhgtRTDU4=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20241024141027-0c6f373d187c/go.mod h1:uOuosXduBzd2WbLH6TDZO7ME7ZextulA662oZ6OsoB0=
penahub.gitlab.yandexcloud.net/devops/linters/golang.git v0.0.0-20240829220549-d35409b619a3 h1:sf6e2mp582L3i/FMDd2q6QuWm1njRXzYpIX0SipsvM4=
penahub.gitlab.yandexcloud.net/devops/linters/golang.git v0.0.0-20240829220549-d35409b619a3/go.mod h1:i7M72RIpkSjcQtHID6KKj9RT/EYZ1rxS6tIPKWa/BSY=

161
internal/app/app.go Normal file

@ -0,0 +1,161 @@
package app
import (
"context"
"errors"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/brokers"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/controllers"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/initialize"
http "penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/server"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/service"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/workers/data_updater"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/workers/limiter"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/workers/post_deals_worker"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/workers/queueUpdater"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/workers_methods"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/pkg/bitrixClient"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/pkg/closer"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal"
"time"
"go.uber.org/zap"
)
func Run(ctx context.Context, config initialize.Config, logger *zap.Logger) error {
defer func() {
if r := recover(); r != nil {
logger.Error("Recovered from a panic", zap.Any("error", r))
}
}()
logger.Info("App started", zap.Any("config", config))
ctx, cancel := context.WithCancel(ctx)
defer cancel()
shutdownGroup := closer.NewCloserGroup()
kafka, err := initialize.KafkaConsumerInit(ctx, config)
if err != nil {
logger.Error("error init kafka consumer", zap.Error(err))
return err
}
producer := brokers.NewProducer(brokers.ProducerDeps{
KafkaClient: kafka,
Logger: logger,
})
bitrixRepo, err := dal.NewBitrixDal(ctx, config.PostgresURL)
if err != nil {
logger.Error("error init bitrix repo in common repo", zap.Error(err))
return err
}
// https://apidocs.bitrix24.ru/limits.html
rateLimiter := limiter.NewRateLimiter(ctx, 50, 2*time.Second)
bitrixClientApi := bitrixClient.NewBitrixClient(bitrixClient.BitrixDeps{
Logger: logger,
RedirectionURL: config.OauthReturnURL,
IntegrationID: config.BitrixIntegrationID,
IntegrationSecret: config.BitrixIntegrationSecret,
RateLimiter: rateLimiter,
})
svc := service.NewService(service.Deps{
Repository: bitrixRepo,
Logger: logger,
BitrixClient: bitrixClientApi,
Producer: producer,
Config: config,
Encrypt: &config.ExternalCfg.EncryptCommon,
})
cntrlDeps := controllers.Deps{
Service: svc,
Logger: logger,
RedirectURL: config.QuizIntegrationsRedirectURL,
Encrypt: &config.ExternalCfg.EncryptCommon,
}
controller := controllers.NewController(cntrlDeps)
webhookController := controllers.NewWebhookController(cntrlDeps)
workerMethods := workers_methods.NewWorkersMethods(workers_methods.Deps{
Repo: bitrixRepo,
BitrixClient: bitrixClientApi,
Logger: logger,
})
dataUpdater := data_updater.NewDataUpdaterWC(data_updater.Deps{
Logger: logger,
Producer: producer,
})
queUpdater := queueUpdater.NewQueueUpdater(queueUpdater.Deps{
Logger: logger,
KafkaClient: kafka,
Methods: workerMethods,
})
dealsPoster := post_deals_worker.NewDealsWC(post_deals_worker.Deps{
BitrixRepo: bitrixRepo,
BitrixClient: bitrixClientApi,
Logger: logger,
})
//
//fieldsPoster := post_fields_worker.NewPostFieldsWC(post_fields_worker.Deps{
// AmoRepo: amoRepo,
// AmoClient: amoClient,
// RedisRepo: redisRepo,
// Logger: logger,
//})
//
go dataUpdater.Start(ctx)
go queUpdater.Start(ctx)
go dealsPoster.Start(ctx)
//go fieldsPoster.Start(ctx)
server := http.NewServer(http.ServerConfig{
Controllers: []http.Controller{
controller,
webhookController,
},
})
go func() {
if err := server.Start(config.ClientHttpURL); err != nil {
logger.Error("Server startup error", zap.Error(err))
cancel()
}
}()
server.ListRoutes()
shutdownGroup.Add(closer.CloserFunc(server.Shutdown))
shutdownGroup.Add(closer.CloserFunc(bitrixRepo.Close))
//shutdownGroup.Add(closer.CloserFunc(redisRepo.Close))
shutdownGroup.Add(closer.CloserFunc(rateLimiter.Stop))
shutdownGroup.Add(closer.CloserFunc(dataUpdater.Stop))
shutdownGroup.Add(closer.CloserFunc(queUpdater.Stop))
shutdownGroup.Add(closer.CloserFunc(dealsPoster.Stop))
//shutdownGroup.Add(closer.CloserFunc(fieldsPoster.Stop))
<-ctx.Done()
timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer timeoutCancel()
if err := shutdownGroup.Call(timeoutCtx); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
logger.Error("Shutdown timed out", zap.Error(err))
} else {
logger.Error("Failed to shutdown services gracefully", zap.Error(err))
}
return err
}
logger.Info("Application has stopped")
return nil
}

@ -0,0 +1,45 @@
package brokers
import (
"context"
"encoding/json"
"github.com/twmb/franz-go/pkg/kgo"
"go.uber.org/zap"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/models"
)
type ProducerDeps struct {
KafkaClient *kgo.Client
Logger *zap.Logger
}
type Producer struct {
kafkaClient *kgo.Client
logger *zap.Logger
}
func NewProducer(deps ProducerDeps) *Producer {
return &Producer{
logger: deps.Logger,
kafkaClient: deps.KafkaClient,
}
}
func (p *Producer) ToKafkaUpdate(ctx context.Context, message models.KafkaMessage) error {
bytes, err := json.Marshal(message)
if err != nil {
p.logger.Error("error marshal message to kafka", zap.Error(err))
return err
}
responses := p.kafkaClient.ProduceSync(ctx, &kgo.Record{Value: bytes})
for _, response := range responses {
if response.Err != nil {
p.logger.Error("failed to send message on update kafka", zap.Error(response.Err))
return response.Err
}
}
return nil
}

@ -0,0 +1,47 @@
package controllers
import (
"errors"
"github.com/gofiber/fiber/v2"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/middleware"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/pj_errors"
)
func (c *Controller) GetFieldsWithPagination(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
req, err := extractParams(ctx)
if err != nil {
return err
}
response, err := c.service.GetFieldsWithPagination(ctx.Context(), req, accountID)
if err != nil {
switch {
case errors.Is(err, pj_errors.ErrNotFound):
return ctx.Status(fiber.StatusNotFound).SendString("fields for this user not found")
default:
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
}
return ctx.Status(fiber.StatusOK).JSON(response)
}
func (c *Controller) UpdateListCustom(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
//accountID := "64f2cd7a7047f28fdabf6d9e"
err := c.service.UpdateListCustom(ctx.Context(), accountID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
return ctx.SendStatus(fiber.StatusOK)
}

@ -0,0 +1,81 @@
package controllers
import (
"gitea.pena/PenaSide/common/encrypt"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/service"
)
type Deps struct {
Service *service.Service
Logger *zap.Logger
Encrypt *encrypt.Encrypt
RedirectURL string
}
type Controller struct {
service *service.Service
logger *zap.Logger
}
func NewController(deps Deps) *Controller {
return &Controller{
service: deps.Service,
logger: deps.Logger,
}
}
func (c *Controller) Register(router fiber.Router) {
router.Patch("/users", c.UpdateListUsers)
router.Get("/users", c.GettingUserWithPagination)
router.Delete("/account", c.SoftDeleteAccount)
router.Get("/account", c.GetCurrentAccount)
router.Post("/account", c.ConnectAccount)
router.Get("/steps", c.GetStepsWithPagination)
router.Patch("/steps", c.UpdateListSteps)
router.Patch("/pipelines", c.UpdateListPipelines)
router.Get("/pipelines", c.GetPipelinesWithPagination)
router.Patch("/rules/:quizID", c.ChangeQuizSettings)
router.Post("/rules/:quizID", c.SetQuizSettings)
router.Get("/rules/:quizID", c.GettingQuizRules)
//router.Get("/tags", c.GetTagsWithPagination)
//router.Patch("/tags", c.UpdateListTags)
router.Get("/fields", c.GetFieldsWithPagination)
router.Patch("/fields", c.UpdateListCustom)
router.Get("/", c.Bitrix)
}
func (c *Controller) Name() string {
return "bitrix"
}
type WebhookController struct {
service *service.Service
logger *zap.Logger
encrypt *encrypt.Encrypt
redirectURL string
}
func NewWebhookController(deps Deps) *WebhookController {
return &WebhookController{
service: deps.Service,
logger: deps.Logger,
encrypt: deps.Encrypt,
redirectURL: deps.RedirectURL,
}
}
func (c *WebhookController) Register(router fiber.Router) {
router.Get("/create", c.WebhookCreate)
//router.Delete("/delete", c.WebhookDelete)
}
func (c *WebhookController) Name() string {
return "webhook"
}
// todo check для чего
func (c *Controller) Bitrix(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusOK).JSON("OK")
}

@ -0,0 +1,37 @@
package controllers
import (
"github.com/gofiber/fiber/v2"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"strconv"
)
func extractParams(ctx *fiber.Ctx) (*model.PaginationReq, error) {
pageStr := ctx.Query("page")
sizeStr := ctx.Query("size")
page := 1
size := 25
if pageStr != "" {
pageNew, err := strconv.Atoi(pageStr)
if err != nil {
return nil, ctx.Status(fiber.StatusBadRequest).SendString("Invalid page parameter")
}
page = pageNew
}
if sizeStr != "" {
sizeNew, err := strconv.Atoi(sizeStr)
if err != nil {
return nil, ctx.Status(fiber.StatusBadRequest).SendString("Invalid size parameter")
}
size = sizeNew
}
req := model.PaginationReq{
Page: page,
Size: int32(size),
}
return &req, nil
}

@ -0,0 +1,47 @@
package controllers
import (
"errors"
"github.com/gofiber/fiber/v2"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/middleware"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/pj_errors"
)
func (c *Controller) UpdateListPipelines(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
//accountID := "64f2cd7a7047f28fdabf6d9e"
err := c.service.UpdateListPipelines(ctx.Context(), accountID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
return ctx.SendStatus(fiber.StatusOK)
}
func (c *Controller) GetPipelinesWithPagination(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
req, err := extractParams(ctx)
if err != nil {
return err
}
response, err := c.service.GetPipelinesWithPagination(ctx.Context(), req, accountID)
if err != nil {
switch {
case errors.Is(err, pj_errors.ErrNotFound):
return ctx.Status(fiber.StatusNotFound).SendString("pipelines for this user not found")
default:
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
}
return ctx.Status(fiber.StatusOK).JSON(response)
}

@ -0,0 +1,110 @@
package controllers
import (
"errors"
"fmt"
"github.com/gofiber/fiber/v2"
"github.com/lib/pq"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/middleware"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/pj_errors"
"strconv"
)
func (c *Controller) ChangeQuizSettings(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
quizID := ctx.Params("quizID")
if quizID == "" {
return ctx.Status(fiber.StatusBadRequest).SendString("quizID is nil")
}
quizIDInt, err := strconv.Atoi(quizID)
if err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("failed convert quizID to int")
}
//accountID := "64f2cd7a7047f28fdabf6d9e"
var request model.BitrixRulesReq
if err := ctx.BodyParser(&request); err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request payload"})
}
err = c.service.ChangeQuizSettings(ctx.Context(), &request, accountID, quizIDInt)
if err != nil {
switch {
case errors.Is(err, pj_errors.ErrNotFound):
return ctx.Status(fiber.StatusNotFound).SendString("rule not found")
default:
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
}
return ctx.SendStatus(fiber.StatusOK)
}
func (c *Controller) SetQuizSettings(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
quizID := ctx.Params("quizID")
if quizID == "" {
return ctx.Status(fiber.StatusBadRequest).SendString("quizID is nil")
}
quizIDInt, err := strconv.Atoi(quizID)
if err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("failed convert quizID to int")
}
//accountID := "64f2cd7a7047f28fdabf6d9e"
var request model.BitrixRulesReq
if err := ctx.BodyParser(&request); err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request payload"})
}
err = c.service.SetQuizSettings(ctx.Context(), &request, accountID, quizIDInt)
if err != nil {
pqErr, ok := err.(*pq.Error)
if ok && pqErr.Code == "23505" {
return ctx.Status(fiber.StatusInternalServerError).SendString(fmt.Sprintf("quiz settings already exist for accountID %s and quizID %d", accountID, quizIDInt))
}
switch {
case errors.Is(err, pj_errors.ErrNotFound):
return ctx.Status(fiber.StatusNotFound).SendString("not found user for this rule")
default:
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
}
return ctx.SendStatus(fiber.StatusOK)
}
func (c *Controller) GettingQuizRules(ctx *fiber.Ctx) error {
quizID := ctx.Params("quizID")
if quizID == "" {
return ctx.Status(fiber.StatusBadRequest).SendString("quizID is nil")
}
quizIDInt, err := strconv.Atoi(quizID)
if err != nil {
return ctx.Status(fiber.StatusBadRequest).SendString("failed convert quizID to int")
}
response, err := c.service.GettingQuizRules(ctx.Context(), quizIDInt)
if err != nil {
switch {
case errors.Is(err, pj_errors.ErrNotFound):
return ctx.Status(fiber.StatusNotFound).SendString("rule not found")
default:
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
}
return ctx.Status(fiber.StatusOK).JSON(response)
}

@ -0,0 +1,47 @@
package controllers
import (
"errors"
"github.com/gofiber/fiber/v2"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/middleware"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/pj_errors"
)
func (c *Controller) GetStepsWithPagination(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
req, err := extractParams(ctx)
if err != nil {
return err
}
response, err := c.service.GetStepsWithPagination(ctx.Context(), req, accountID)
if err != nil {
switch {
case errors.Is(err, pj_errors.ErrNotFound):
return ctx.Status(fiber.StatusNotFound).SendString("steps for this user not found")
default:
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
}
return ctx.Status(fiber.StatusOK).JSON(response)
}
func (c *Controller) UpdateListSteps(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
//accountID := "64f2cd7a7047f28fdabf6d9e"
err := c.service.UpdateListSteps(ctx.Context(), accountID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
return ctx.SendStatus(fiber.StatusOK)
}

@ -0,0 +1,40 @@
package controllers
//func (c *Controller) GetTagsWithPagination(ctx *fiber.Ctx) error {
// accountID, ok := middleware.GetAccountId(ctx)
// if !ok {
// return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
// }
//
// req, err := extractParams(ctx)
// if err != nil {
// return err
// }
//
// response, err := c.service.GetTagsWithPagination(ctx.Context(), req, accountID)
// if err != nil {
// switch {
// case errors.Is(err, pj_errors.ErrNotFound):
// return ctx.Status(fiber.StatusNotFound).SendString("tags for this user not found")
// default:
// return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
// }
// }
// return ctx.Status(fiber.StatusOK).JSON(response)
//}
//
//func (c *Controller) UpdateListTags(ctx *fiber.Ctx) error {
// accountID, ok := middleware.GetAccountId(ctx)
// if !ok {
// return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
// }
//
// //accountID := "654a8909725f47e926f0bebc"
//
// err := c.service.UpdateListTags(ctx.Context(), accountID)
// if err != nil {
// return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
// }
//
// return ctx.SendStatus(fiber.StatusOK)
//}

@ -0,0 +1,92 @@
package controllers
import (
"errors"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/middleware"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/pj_errors"
)
func (c *Controller) UpdateListUsers(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
//accountID := "654a8909725f47e926f0bebc"
err := c.service.UpdateListUsers(ctx.Context(), accountID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
return ctx.SendStatus(fiber.StatusOK)
}
func (c *Controller) GettingUserWithPagination(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
req, err := extractParams(ctx)
if err != nil {
return err
}
response, err := c.service.GettingUserWithPagination(ctx.Context(), req, accountID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
return ctx.Status(fiber.StatusOK).JSON(response)
}
func (c *Controller) SoftDeleteAccount(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
err := c.service.SoftDeleteAccount(ctx.Context(), accountID)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
return ctx.SendStatus(fiber.StatusOK)
}
func (c *Controller) GetCurrentAccount(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
response, err := c.service.GetCurrentAccount(ctx.Context(), accountID)
if err != nil {
switch {
case errors.Is(err, pj_errors.ErrNotFound):
return ctx.Status(fiber.StatusNotFound).SendString("user not found")
default:
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
}
return ctx.Status(fiber.StatusOK).JSON(response)
}
func (c *Controller) ConnectAccount(ctx *fiber.Ctx) error {
accountID, ok := middleware.GetAccountId(ctx)
if !ok {
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
}
//accountID := "64f2cd7a7047f28fdabf6d9e"
response, err := c.service.ConnectAccount(ctx.Context(), accountID)
if err != nil {
c.logger.Error("error connect account", zap.Error(err))
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
return ctx.Status(fiber.StatusOK).JSON(response)
}

@ -0,0 +1,90 @@
package controllers
import (
"fmt"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/service"
)
// https://dev.1c-bitrix.ru/learning/course/index.php?COURSE_ID=99&LESSON_ID=2486
func (c *WebhookController) WebhookCreate(ctx *fiber.Ctx) error {
code := ctx.Query("code") // первый авторизационный код
domain := ctx.Query("domain") // домен портала, на котором происходит авторизация
state := ctx.Query("state") // значение, переданное в первом запросе
scope := ctx.Query("scope") // список прав доступа к REST API
memberID := ctx.Query("member_id") // уникальный идентификатор портала - id битрикса главного
serverDomain := ctx.Query("server_domain") // домен сервера авторизации
if code == "" || domain == "" || memberID == "" || serverDomain == "" {
c.logger.Error("Missing required fields", zap.String("code", code), zap.String("domain", domain), zap.String("member_id", memberID), zap.String("server_domain", serverDomain))
return ctx.Status(fiber.StatusBadRequest).SendString("Missing required fields")
}
if state == "" {
return ctx.Status(fiber.StatusBadRequest).SendString("State cannot be empty")
}
accountID, err := c.encrypt.DecryptStr([]byte(state))
if err != nil {
c.logger.Error("Error deserializing Protobuf message", zap.Error(err))
return ctx.Status(fiber.StatusInternalServerError).SendString("Failed to process state parameter")
}
if accountID == "" {
c.logger.Error("AccountID is missing from state")
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid state parameter")
}
req := service.ParamsWebhookCreate{
Code: code,
Domain: domain,
AccountID: accountID,
MemberID: memberID,
Scope: scope,
ServerDomain: serverDomain,
}
err = c.service.WebhookCreate(ctx.Context(), req)
if err != nil {
c.logger.Error("Error creating webhook", zap.Error(err))
return ctx.Status(fiber.StatusInternalServerError).SendString(fmt.Sprintf("Internal Server Error: %v", err.Error()))
}
return ctx.Redirect(c.redirectURL)
}
//// todo проверить надо есть ли такое вообще
//func (c *WebhookController) WebhookDelete(ctx *fiber.Ctx) error {
// clientUUID := ctx.Query("client_uuid")
// signature := ctx.Query("signature")
// amoIDStr := ctx.Query("account_id")
//
// fmt.Println(clientUUID)
// fmt.Println(signature)
// fmt.Println(amoIDStr)
//
// if clientUUID == "" || signature == "" || amoIDStr == "" {
// return ctx.Status(fiber.StatusBadRequest).SendString("some nil values")
// }
//
// amoID, err := strconv.Atoi(amoIDStr)
// if err != nil {
// return ctx.Status(fiber.StatusBadRequest).SendString("invalid account_id type")
// }
//
// if !c.verify.CheckIntegrationID(clientUUID) {
// return ctx.Status(fiber.StatusUnauthorized).SendString("invalid hook signature")
// }
//
// if !c.verify.VerifySignature(clientUUID, signature, amoID) {
// return ctx.Status(fiber.StatusUnauthorized).SendString("invalid hook signature")
// }
//
// err = c.service.WebhookDelete(ctx.Context(), amoID)
// if err != nil {
// return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
// }
//
// return ctx.SendStatus(fiber.StatusOK)
//}

@ -0,0 +1,42 @@
package initialize
import (
"gitea.pena/PenaSide/common/encrypt"
"github.com/caarlos0/env/v8"
"github.com/joho/godotenv"
"log"
)
type Config struct {
ClientHttpURL string `env:"CLIENT_HTTP_URL" envDefault:"10.8.0.18:1492"`
PostgresURL string `env:"POSTGRES_URL" envDefault:"host=localhost port=35432 user=squiz password=Redalert2 dbname=squiz sslmode=disable"`
KafkaBrokers string `env:"KAFKA_BROKERS" envDefault:"localhost:9092"`
KafkaTopicQueueBitrix string `env:"KAFKA_TOPIC_QUEUE_BITRIX" envDefault:"test-topic"`
KafkaGroup string `env:"KAFKA_GROUP" envDefault:"bitrixCRM"`
RedisHost string `env:"REDIS_HOST" envDefault:"localhost:6379"`
RedisPassword string `env:"REDIS_PASSWORD" envDefault:"admin"`
RedisDB int `env:"REDIS_DB" envDefault:"2"`
BitrixIntegrationID string `env:"BITRIX_INTEGRATION_ID" envDefault:"app.670bd825e44c52.61826940"` // код интеграции
QuizIntegrationsRedirectURL string `env:"QUIZ_INTEGRATIONS_REDIRECT_URL" envDefault:"https://squiz.pena.digital/integrations"`
// секрет интеграции
BitrixIntegrationSecret string `env:"BITRIX_INTEGRATION_SECRET" envDefault:"Ki0MElZXS6dE6tRsGxixri2jmxbxF2Xa4qQpBPziGdAvvLAHJx"`
// урл на который будет возвращен пользователь после авторизации это webhook/create get
OauthReturnURL string `env:"OAUTH_RETURN_URL" envDefault:"https://squiz.pena.digital/squiz/amocrm/oauth"`
ExternalCfg ExternalCfg
}
type ExternalCfg struct {
// публичный и приватные ключи для енкрипта и декрипта стейта который передаем в битрикс а потом он приходит
EncryptCommon encrypt.Encrypt
}
func LoadConfig() (*Config, error) {
if err := godotenv.Load(); err != nil {
log.Print("No .env file found")
}
var config Config
if err := env.Parse(&config); err != nil {
return nil, err
}
return &config, nil
}

@ -0,0 +1,27 @@
package initialize
import (
"context"
"github.com/twmb/franz-go/pkg/kgo"
"time"
)
func KafkaConsumerInit(ctx context.Context, config Config) (*kgo.Client, error) {
kafkaClient, err := kgo.NewClient(
kgo.SeedBrokers(config.KafkaBrokers),
kgo.ConsumerGroup(config.KafkaGroup),
kgo.DefaultProduceTopic(config.KafkaTopicQueueBitrix),
kgo.ConsumeTopics(config.KafkaTopicQueueBitrix),
kgo.ConsumeResetOffset(kgo.NewOffset().AfterMilli(time.Now().UnixMilli())),
)
if err != nil {
return nil, err
}
err = kafkaClient.Ping(ctx)
if err != nil {
return nil, err
}
return kafkaClient, nil
}

@ -0,0 +1,21 @@
package initialize
import (
"context"
"github.com/go-redis/redis/v8"
)
func Redis(ctx context.Context, cfg Config) (*redis.Client, error) {
rdb := redis.NewClient(&redis.Options{
Addr: cfg.RedisHost,
Password: cfg.RedisPassword,
DB: cfg.RedisDB,
})
status := rdb.Ping(ctx)
if err := status.Err(); err != nil {
return nil, err
}
return rdb, nil
}

@ -0,0 +1,18 @@
package models
type CompanyReq struct {
Fields CompanyFields `json:"fields"`
}
type CompanyFields struct {
Title string `json:"TITLE"`
AssignedByID int `json:"ASSIGNED_BY_ID,"`
Opened string `json:"OPENED"`
LeadID int32 `json:"LEAD_ID"`
UtmSource string `json:"UTM_SOURCE"`
UtmMedium string `json:"UTM_MEDIUM"`
UtmCampaign string `json:"UTM_CAMPAIGN"`
UtmContent string `json:"UTM_CONTENT"`
UtmTerm string `json:"UTM_TERM"`
ContactID int32 `json:"CONTACT_ID"`
}

@ -0,0 +1,21 @@
package models
type CreateContactReq struct {
Fields ContactFields `json:"fields"`
}
type ContactFields struct {
Name string `json:"NAME"`
SecondName string `json:"SECOND_NAME"`
LastName string `json:"LAST_NAME"`
TypeID string `json:"TYPE_ID"`
SourceID string `json:"SOURCE_ID"`
Opened string `json:"OPENED"`
CompanyIDs []int32 `json:"COMPANY_IDS"`
LeadID int32 `json:"LEAD_ID"`
UtmSource string `json:"UTM_SOURCE"`
UtmMedium string `json:"UTM_MEDIUM"`
UtmCampaign string `json:"UTM_CAMPAIGN"`
UtmContent string `json:"UTM_CONTENT"`
UtmTerm string `json:"UTM_TERM"`
}

@ -0,0 +1,82 @@
package models
type CreatingDealReq struct {
Fields CreateDealFields `json:"fields"`
//Params struct {
// RegisterSonetEvent string `json:"REGISTER_SONET_EVENT"` // Зарегистрировать ли событие добавления сделки в живой ленте. Возможные значения: Y/N def - Y
//} `json:"params"`
}
type CreateDealFields struct {
Title string `json:"TITLE"` // название
TypeID string `json:"TYPE_ID"` // шаг сделки только с "ENTITY_ID":"DEAL_TYPE","STATUS_ID":"SALE"
StageID string `json:"STAGE_ID"` // стадия сделки, шаг "ENTITY_ID":"DEAL_STAGE","STATUS_ID":"NEW"
CompanyID int32 `json:"COMPANY_ID"` // id компании
ContactIDs []int32 `json:"CONTACT_IDS"` // id контакта
Opened string `json:"OPENED"` // открыта или нет Y/N
AssignedByID int `json:"ASSIGNED_BY_ID"` // ответсвенный
CategoryID int32 `json:"CATEGORY_ID"` // воронка id воронки
SourceID string `json:"SOURCE_ID"` // тип источника, шаг "ENTITY_ID":"SOURCE","STATUS_ID":"CALL"
UtmSource string `json:"UTM_SOURCE"` // Рекламная система (Google-Adwords и другие)
UtmMedium string `json:"UTM_MEDIUM"` // Тип трафика. Возможные значения:CPC — объявления, CPM — баннеры
UtmCampaign string `json:"UTM_CAMPAIGN"` // Обозначение рекламной кампании
UtmContent string `json:"UTM_CONTENT"` // Содержание кампании. Например, для контекстных объявлений
UtmTerm string `json:"UTM_TERM"` // Условие поиска кампании. Например, ключевые слова контекстной рекламы
// todo умаю пока не нужны
//IsRecurring string `json:"IS_RECURRING"` // Является ли сделка регулярной (Y/N)
//IsReturnCustomer string `json:"IS_RETURN_CUSTOMER"` // Является ли сделка повторной (Y/N)
//IsRepeatedApproach string `json:"IS_REPEATED_APPROACH"` // Является ли сделка повторным обращением (Y/N)
//Probability int `json:"PROBABILITY"` // Вероятность успеха сделки, %
//CurrencyID string `json:"CURRENCY_ID"` // Идентификатор валюты сделки
//Opportunity float64 `json:"OPPORTUNITY"` // Сумма сделки
//IsManualOpportunity string `json:"IS_MANUAL_OPPORTUNITY"` // Включен ли ручной расчет суммы (Y/N)
//TaxValue float64 `json:"TAX_VALUE"` // Сумма налога
//BeginDate string `json:"BEGINDATE"` // Дата начала сделки
//CloseDate string `json:"CLOSEDATE"` // Дата завершения сделки
//Closed string `json:"CLOSED"` // Сделка закрыта (Y/N)
//Comments string `json:"COMMENTS"` // Комментарии к сделке
//SourceDescription string `json:"SOURCE_DESCRIPTION"` // Описание источника
//AdditionalInfo string `json:"ADDITIONAL_INFO"` // Дополнительная информация
//LocationID string `json:"LOCATION_ID"` // Местоположение клиента
//OriginatorID string `json:"ORIGINATOR_ID"` // Идентификатор источника данных
//OriginID string `json:"ORIGIN_ID"` // Идентификатор элемента в источнике данных
//Trace string `json:"TRACE"` // Информация для сквозной аналитики
//UfCrmCustomField []string `json:"UF_CRM_..."` // Пользовательские поля CRM
//ParentID string `json:"PARENT_ID_..."` // Поля связей со смарт-процессами
}
//func (c *CreatingDealReq) FormattingToMap(fieldAnswer map[string]string) map[string]map[string]interface{} {
// resultFields := make(map[string]interface{})
// result := make(map[string]map[string]interface{})
//
// fields := reflect.ValueOf(c.Fields)
// fieldType := reflect.TypeOf(c.Fields)
//
// for i := 0; i < fields.NumField(); i++ {
// field := fields.Field(i)
// fieldName := fieldType.Field(i).Tag.Get("json")
//
// switch field.Kind() {
// case reflect.String:
// resultFields[fieldName] = field.String()
// case reflect.Int32, reflect.Int:
// resultFields[fieldName] = field.Int()
// case reflect.Slice:
// if field.Type().Elem().Kind() == reflect.Int32 {
// resultFields[fieldName] = field.Interface().([]int32)
// }
// }
// }
//
// for key, value := range fieldAnswer {
// resultFields[key] = value
// }
// result["fields"] = resultFields
//
// return result
//}
type MultiResp struct {
ID int32 `json:"result"`
}

@ -0,0 +1,20 @@
package models
type CreatingLeadReq struct {
Fields CreateLeadFields `json:"fields"`
}
type CreateLeadFields struct {
Title string `json:"TITLE"` // название
CompanyID int32 `json:"COMPANY_ID"` // id компании
ContactIDs []int32 `json:"CONTACT_IDS"` // id контакта
Opened string `json:"OPENED"` // открыта или нет Y/N
AssignedByID int `json:"ASSIGNED_BY_ID"` // ответсвенный
SourceID string `json:"SOURCE_ID"` // тип источника, шаг "ENTITY_ID":"SOURCE","STATUS_ID":"CALL"
StatusID string `json:"STATUS_ID"` // Идентификатор стадии лида.
UtmSource string `json:"UTM_SOURCE"` // Рекламная система (Google-Adwords и другие)
UtmMedium string `json:"UTM_MEDIUM"` // Тип трафика. Возможные значения:CPC — объявления, CPM — баннеры
UtmCampaign string `json:"UTM_CAMPAIGN"` // Обозначение рекламной кампании
UtmContent string `json:"UTM_CONTENT"` // Содержание кампании. Например, для контекстных объявлений
UtmTerm string `json:"UTM_TERM"` // Условие поиска кампании. Например, ключевые слова контекстной рекламы
}

@ -0,0 +1,64 @@
package models
type CreateWebHookReq struct {
ClientID string `json:"client_id"` // id интеграции
ClientSecret string `json:"client_secret"` // Секрет интеграции
GrantType string `json:"grant_type"` // Тип авторизационных данных (для кода авторизации authorization_code)
Code string `json:"code"` // Полученный код авторизации
}
type CreateWebHookResp struct {
AccessToken string `json:"access_token"` // access token в формате JWT
ClientEndpoint string `json:"client_endpoint"`
Domain string `json:"domain"`
ExpiresIn int64 `json:"expires_in"` // время жизни токена в секундах
MemberID string `json:"member_id"` // ид пользователя
RefreshToken string `json:"refresh_token"` // токен для обновления access Token
Scope string `json:"scope"`
}
type UpdateWebHookReq struct {
ClientID string `json:"client_id"` // id интеграции
ClientSecret string `json:"client_secret"` // Секрет интеграции
GrantType string `json:"grant_type"` // Тип авторизационных данных (для кода авторизации authorization_code) refresh_token tut
RefreshToken string `json:"refresh_token"` // Refresh токен
}
type WebHookRequest interface {
SetClientID(str string)
SetClientSecret(str string)
GetGrantType() string
GetToken() string
}
func (req *CreateWebHookReq) SetClientID(str string) {
req.ClientID = str
}
func (req *CreateWebHookReq) SetClientSecret(str string) {
req.ClientSecret = str
}
func (req *CreateWebHookReq) GetGrantType() string {
return req.GrantType
}
func (req *CreateWebHookReq) GetToken() string {
return req.Code
}
func (req *UpdateWebHookReq) SetClientID(str string) {
req.ClientID = str
}
func (req *UpdateWebHookReq) SetClientSecret(str string) {
req.ClientSecret = str
}
func (req *UpdateWebHookReq) GetGrantType() string {
return req.GrantType
}
func (req *UpdateWebHookReq) GetToken() string {
return req.RefreshToken
}

@ -0,0 +1,65 @@
package models
import (
"github.com/rs/xid"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"strings"
)
type FieldsResponse struct {
Result []Fields `json:"result"`
Total int `json:"total"`
}
type Fields struct {
ID string `json:"ID"`
EntityID model.FieldsType `json:"ENTITY_ID"`
FieldName string `json:"FIELD_NAME"`
UserTypeID model.CustomFieldsType `json:"USER_TYPE_ID"`
Sort string `json:"SORT"`
Multiple string `json:"MULTIPLE"`
Mandatory string `json:"MANDATORY"`
ShowFilter string `json:"SHOW_FILTER"`
ShowInList string `json:"SHOW_IN_LIST"`
EditInList string `json:"EDIT_IN_LIST"`
IsSearchable string `json:"IS_SEARCHABLE"`
EditFormLabel string `json:"EDIT_FORM_LABEL"`
ListColumnLabel string `json:"LIST_COLUMN_LABEL"`
ListFilterLabel string `json:"LIST_FILTER_LABEL"`
ErrorMessage string `json:"ERROR_MESSAGE"`
HelpMessage string `json:"HELP_MESSAGE"`
}
type AddFields struct {
FieldName string `json:"FIELD_NAME"`
EditFormLabel string `json:"EDIT_FORM_LABEL"`
ListColumnLabel string `json:"LIST_COLUMN_LABEL"`
UserTypeID model.CustomFieldsType `json:"USER_TYPE_ID"` // Тип поля
XMLID string `json:"XML_ID"`
Settings map[string]interface{} `json:"SETTINGS"`
Mandatory string `json:"MANDATORY"`
}
func (a *AddFields) GenFieldName() {
guid := xid.New()
guidGen := guid.String()
// todo и так и так работает обдумать стоит
// https://dev.1c-bitrix.ru/rest_help/crm/cdeals/crm_deal_userfield_add.php
//a.FieldName = guidGen[:13]
//a.XMLID = guidGen[:13]
a.FieldName = strings.ToUpper(guidGen)
a.XMLID = strings.ToUpper(guidGen)
a.Mandatory = "Y"
}
type FileField struct {
FileData []string `json:"fileData"`
}
type UserFieldTypeAddReq struct {
UserTypeID string `json:"USER_TYPE_ID"`
Handler string `json:"HANDLER"`
Title string `json:"TITLE"`
Description string `json:"DESCRIPTION"`
}

@ -0,0 +1,17 @@
package models
import "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
type Category struct {
ID int32 `json:"id"`
Name string `json:"name"`
Sort int32 `json:"sort"`
EntityTypeId model.IntegerEntityType `json:"entityTypeId"`
IsDefault model.BitrixIsDefault `json:"isDefault"`
}
type CategoryResponse struct {
Result struct {
Categories []Category `json:"categories"`
} `json:"result"`
}

@ -0,0 +1,18 @@
package models
type StepsResponse struct {
Result []Steps `json:"result"`
}
type Steps struct {
ID string `json:"ID"`
EntityID string `json:"ENTITY_ID"`
StatusID string `json:"STATUS_ID"`
Name string `json:"NAME"`
NameInit string `json:"NAME_INIT,omitempty"`
Sort string `json:"SORT"`
System string `json:"SYSTEM"`
Color string `json:"COLOR,omitempty"`
Semantics string `json:"SEMANTICS,omitempty"`
CategoryID string `json:"CATEGORY_ID"`
}

@ -0,0 +1,21 @@
package models
type ResponseGetListUsers struct {
Result []User `json:"result"`
Total int `json:"total"`
}
type User struct {
ID string `json:"ID"`
Name string `json:"NAME"`
LastName string `json:"LAST_NAME"`
SecondName string `json:"SECOND_NAME"`
Title string `json:"TITLE"`
Email string `json:"EMAIL"`
UFDepartment []int32 `json:"UF_DEPARTMENT"`
WorkPosition string `json:"WORK_POSITION"`
}
type ResponseGetCurrentUser struct {
Result User `json:"result"`
}

@ -0,0 +1,38 @@
package models
import "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
type KafkaMessage struct {
AccountID string
AuthCode string
RefererURL string
MemberID string
Type MessageType
Rule KafkaRule
}
type KafkaRule struct {
QuizID int32
PerformerID string // айдишник ответственного за сделку
PipelineID int32 // айдишник воронки
TypeID string // шаг сделки только с "ENTITY_ID":"DEAL_TYPE","STATUS_ID":"SALE"
StageID string // стадия сделки, шаг "ENTITY_ID":"DEAL_STAGE","STATUS_ID":"NEW"
SourceID string // тип источника, шаг "ENTITY_ID":"SOURCE","STATUS_ID":"CALL"
LeadFlag bool // флаг показывающий на то что нужен лид а не дил
FieldsRule model.BitrixFieldRules
StatusID string
}
type MessageType string
const (
UsersUpdate MessageType = "users"
PipelinesUpdate MessageType = "pipelines"
FieldsUpdate MessageType = "fields"
TagsUpdate MessageType = "tags"
UserCreate MessageType = "userCreate"
AllDataUpdate MessageType = "allDataUpdate"
RuleCheck MessageType = "ruleCheck"
UserReLogin MessageType = "userReLogin"
StepsUpdate MessageType = "steps"
)

@ -0,0 +1,54 @@
package models
import "reflect"
type FieldMapper interface {
GetFields() interface{}
}
func (c *CreatingDealReq) GetFields() interface{} {
return c.Fields
}
func (c *CompanyReq) GetFields() interface{} {
return c.Fields
}
func (c *CreateContactReq) GetFields() interface{} {
return c.Fields
}
func (c *CreatingLeadReq) GetFields() interface{} {
return c.Fields
}
func FormattingToMap(f FieldMapper, fieldAnswer map[string]string) map[string]map[string]interface{} {
resultFields := make(map[string]interface{})
result := make(map[string]map[string]interface{})
fields := reflect.ValueOf(f.GetFields())
fieldType := reflect.TypeOf(f.GetFields())
for i := 0; i < fields.NumField(); i++ {
field := fields.Field(i)
fieldName := fieldType.Field(i).Tag.Get("json")
switch field.Kind() {
case reflect.String:
resultFields[fieldName] = field.String()
case reflect.Int32, reflect.Int:
resultFields[fieldName] = field.Int()
case reflect.Slice:
if field.Type().Elem().Kind() == reflect.Int32 {
resultFields[fieldName] = field.Interface().([]int32)
}
}
}
for key, value := range fieldAnswer {
resultFields[key] = value
}
result["fields"] = resultFields
return result
}

66
internal/server/http.go Normal file

@ -0,0 +1,66 @@
package http
import (
"context"
"fmt"
"github.com/gofiber/fiber/v2"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/middleware"
)
type ServerConfig struct {
Controllers []Controller
}
type Server struct {
Controllers []Controller
app *fiber.App
}
func NewServer(config ServerConfig) *Server {
app := fiber.New()
app.Use("/bitrix", middleware.JWTAuth())
app.Use("/webhook", func(c *fiber.Ctx) error {
return c.Next()
})
s := &Server{
Controllers: config.Controllers,
app: app,
}
s.registerRoutes()
return s
}
func (s *Server) Start(addr string) error {
if err := s.app.Listen(addr); err != nil {
return err
}
return nil
}
func (s *Server) Shutdown(ctx context.Context) error {
return s.app.Shutdown()
}
func (s *Server) registerRoutes() {
for _, c := range s.Controllers {
router := s.app.Group(c.Name())
c.Register(router)
}
}
type Controller interface {
Register(router fiber.Router)
Name() string
}
func (s *Server) ListRoutes() {
fmt.Println("Registered routes:")
for _, stack := range s.app.Stack() {
for _, route := range stack {
fmt.Printf("%s %s\n", route.Method, route.Path)
}
}
}

@ -0,0 +1,38 @@
package service
import (
"context"
"database/sql"
"go.uber.org/zap"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/models"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/tools"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/pj_errors"
)
func (s *Service) GetFieldsWithPagination(ctx context.Context, req *model.PaginationReq, accountID string) (*model.UserListBitrixFieldsResp, error) {
response, err := s.repository.BitrixRepo.GetFieldsWithPagination(ctx, req, accountID)
if err != nil {
if err == sql.ErrNoRows {
return nil, pj_errors.ErrNotFound
}
s.logger.Error("error getting fields with pagination", zap.Error(err))
return nil, err
}
return tools.ValidateUtmFields(response), nil
}
func (s *Service) UpdateListCustom(ctx context.Context, accountID string) error {
message := models.KafkaMessage{
AccountID: accountID,
Type: models.FieldsUpdate,
}
err := s.producer.ToKafkaUpdate(ctx, message)
if err != nil {
s.logger.Error("failed to send message to kafka on service update fields", zap.Error(err))
return err
}
return nil
}

@ -0,0 +1,39 @@
package service
import (
"gitea.pena/PenaSide/common/encrypt"
"go.uber.org/zap"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/brokers"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/initialize"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/pkg/bitrixClient"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal"
)
type Deps struct {
Repository *dal.BitrixDal
Logger *zap.Logger
BitrixClient *bitrixClient.Bitrix
Producer *brokers.Producer
Config initialize.Config
Encrypt *encrypt.Encrypt
}
type Service struct {
repository *dal.BitrixDal
logger *zap.Logger
bitrixClient *bitrixClient.Bitrix
producer *brokers.Producer
config initialize.Config
encrypt *encrypt.Encrypt
}
func NewService(deps Deps) *Service {
return &Service{
repository: deps.Repository,
logger: deps.Logger,
bitrixClient: deps.BitrixClient,
producer: deps.Producer,
config: deps.Config,
encrypt: deps.Encrypt,
}
}

@ -0,0 +1,37 @@
package service
import (
"context"
"database/sql"
"go.uber.org/zap"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/models"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/pj_errors"
)
func (s *Service) UpdateListPipelines(ctx context.Context, accountID string) error {
message := models.KafkaMessage{
AccountID: accountID,
Type: models.PipelinesUpdate,
}
err := s.producer.ToKafkaUpdate(ctx, message)
if err != nil {
s.logger.Error("failed to send message to kafka on service update pipelines", zap.Error(err))
return err
}
return nil
}
func (s *Service) GetPipelinesWithPagination(ctx context.Context, req *model.PaginationReq, accountID string) (*model.UserBitrixListPipelinesResp, error) {
response, err := s.repository.BitrixRepo.GetPipelinesWithPagination(ctx, req, accountID)
if err != nil {
if err == sql.ErrNoRows {
return nil, pj_errors.ErrNotFound
}
s.logger.Error("error getting pipelines with pagination", zap.Error(err))
return nil, err
}
return response, nil
}

96
internal/service/rules.go Normal file

@ -0,0 +1,96 @@
package service
import (
"context"
"database/sql"
"go.uber.org/zap"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/models"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/pj_errors"
)
func (s *Service) ChangeQuizSettings(ctx context.Context, request *model.BitrixRulesReq, accountID string, quizID int) error {
err := s.repository.BitrixRepo.ChangeQuizSettings(ctx, request, accountID, quizID)
if err != nil {
if err == sql.ErrNoRows {
return pj_errors.ErrNotFound
}
s.logger.Error("error change quiz settings", zap.Error(err))
return err
}
messageForUTM := models.KafkaMessage{
AccountID: accountID,
Type: models.RuleCheck,
Rule: models.KafkaRule{
QuizID: int32(quizID),
PerformerID: request.PerformerID,
PipelineID: request.PipelineID,
TypeID: request.TypeID,
StageID: request.StageID,
SourceID: request.SourceID,
FieldsRule: request.FieldsRule,
LeadFlag: request.LeadFlag,
StatusID: request.StatusID,
},
}
err = s.producer.ToKafkaUpdate(ctx, messageForUTM)
if err != nil {
s.logger.Error("error sending message to kafka for check rules", zap.Error(err))
if err != nil {
return err
}
}
return nil
}
func (s *Service) SetQuizSettings(ctx context.Context, request *model.BitrixRulesReq, accountID string, quizID int) error {
err := s.repository.BitrixRepo.SetQuizSettings(ctx, request, accountID, quizID)
if err != nil {
if err == sql.ErrNoRows {
return pj_errors.ErrNotFound
}
s.logger.Error("error setting quiz settings", zap.Error(err))
return err
}
messageForUTM := models.KafkaMessage{
AccountID: accountID,
Type: models.RuleCheck,
Rule: models.KafkaRule{
QuizID: int32(quizID),
PerformerID: request.PerformerID,
PipelineID: request.PipelineID,
TypeID: request.TypeID,
StageID: request.StageID,
SourceID: request.SourceID,
FieldsRule: request.FieldsRule,
LeadFlag: request.LeadFlag,
StatusID: request.StatusID,
},
}
err = s.producer.ToKafkaUpdate(ctx, messageForUTM)
if err != nil {
s.logger.Error("error sending message to kafka for check rules", zap.Error(err))
if err != nil {
return err
}
}
return nil
}
func (s *Service) GettingQuizRules(ctx context.Context, quizID int) (*model.BitrixRule, error) {
rule, err := s.repository.BitrixRepo.GettingQuizRules(ctx, quizID)
if err != nil {
if err == sql.ErrNoRows {
return nil, pj_errors.ErrNotFound
}
s.logger.Error("error getting quiz settings", zap.Error(err))
return nil, err
}
return rule, nil
}

37
internal/service/steps.go Normal file

@ -0,0 +1,37 @@
package service
import (
"context"
"database/sql"
"go.uber.org/zap"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/models"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/pj_errors"
)
func (s *Service) GetStepsWithPagination(ctx context.Context, req *model.PaginationReq, accountID string) (*model.UserListBitrixStepsResp, error) {
response, err := s.repository.BitrixRepo.GetStepsWithPagination(ctx, req, accountID)
if err != nil {
if err == sql.ErrNoRows {
return nil, pj_errors.ErrNotFound
}
s.logger.Error("error getting steps with pagination", zap.Error(err))
return nil, err
}
return response, nil
}
func (s *Service) UpdateListSteps(ctx context.Context, accountID string) error {
message := models.KafkaMessage{
AccountID: accountID,
Type: models.StepsUpdate,
}
err := s.producer.ToKafkaUpdate(ctx, message)
if err != nil {
s.logger.Error("failed to send message to kafka on service update steps", zap.Error(err))
return err
}
return nil
}

28
internal/service/tags.go Normal file

@ -0,0 +1,28 @@
package service
//func (s *Service) GetTagsWithPagination(ctx context.Context, req *model.PaginationReq, accountID string) (*model.UserListTagsResp, error) {
// response, err := s.repository.BitrixRepo.GetTagsWithPagination(ctx, req, accountID)
// if err != nil {
// if err == sql.ErrNoRows {
// return nil, pj_errors.ErrNotFound
// }
// s.logger.Error("error getting tags with pagination", zap.Error(err))
// return nil, err
// }
// return response, nil
//}
//
//func (s *Service) UpdateListTags(ctx context.Context, accountID string) error {
// message := models.KafkaMessage{
// AccountID: accountID,
// Type: models.TagsUpdate,
// }
//
// err := s.producer.ToKafkaUpdate(ctx, message)
// if err != nil {
// s.logger.Error("failed to send message to kafka on service update tags", zap.Error(err))
// return err
// }
//
// return nil
//}

81
internal/service/user.go Normal file

@ -0,0 +1,81 @@
package service
import (
"context"
"database/sql"
"go.uber.org/zap"
"net/url"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/models"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/pj_errors"
)
func (s *Service) UpdateListUsers(ctx context.Context, accountID string) error {
message := models.KafkaMessage{
AccountID: accountID,
Type: models.UsersUpdate,
}
err := s.producer.ToKafkaUpdate(ctx, message)
if err != nil {
s.logger.Error("failed to send message to kafka on service update users", zap.Error(err))
return err
}
return nil
}
func (s *Service) GettingUserWithPagination(ctx context.Context, req *model.PaginationReq, accountID string) (*model.UserListBitrixResp, error) {
response, err := s.repository.BitrixRepo.GettingUserWithPagination(ctx, req, accountID)
if err != nil {
s.logger.Error("error getting users with pagination", zap.Error(err))
return nil, err
}
return response, nil
}
func (s *Service) SoftDeleteAccount(ctx context.Context, accountID string) error {
err := s.repository.BitrixRepo.SoftDeleteAccount(ctx, accountID)
if err != nil {
s.logger.Error("error soft delete current account in softDeleteAccount service", zap.Error(err))
return err
}
return nil
}
func (s *Service) GetCurrentAccount(ctx context.Context, accountID string) (*model.BitrixAccount, error) {
user, err := s.repository.BitrixRepo.GetCurrentAccount(ctx, accountID)
if err != nil {
if err == sql.ErrNoRows {
return nil, pj_errors.ErrNotFound
}
s.logger.Error("error getting current account in getCurrentAccount service", zap.Error(err))
return nil, err
}
return user, nil
}
func (s *Service) ConnectAccount(ctx context.Context, accountID string) (*model.ConnectAccountResp, error) {
state, err := s.encrypt.EncryptStr(accountID)
if err != nil {
s.logger.Error("error encrypting account state", zap.Error(err))
return nil, err
}
oauthURL := url.URL{
Scheme: "https",
Host: "b24-s5jg6c.bitrix24.ru", // todo check надо проверить как с дургими доменами работает, потому что сейчас это домен каждого отдельного битрикса
Path: "/oauth/authorize/",
RawQuery: url.Values{
"client_id": {s.config.BitrixIntegrationID},
"state": {string(state)},
}.Encode(),
}
response := model.ConnectAccountResp{
Link: oauthURL.String(),
}
return &response, nil
}

@ -0,0 +1,67 @@
package service
import (
"context"
"errors"
"go.uber.org/zap"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/models"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/pj_errors"
)
type ParamsWebhookCreate struct {
Code string
Domain string
AccountID string
MemberID string
Scope string
ServerDomain string
}
func (s *Service) WebhookCreate(ctx context.Context, req ParamsWebhookCreate) error {
_, err := s.GetCurrentAccount(ctx, req.AccountID)
if err != nil && !errors.Is(err, pj_errors.ErrNotFound) {
s.logger.Error("error checking current account in bitrix in webhook create", zap.Error(err))
return err
}
if errors.Is(err, pj_errors.ErrNotFound) {
message := models.KafkaMessage{
AccountID: req.AccountID,
AuthCode: req.Code,
RefererURL: req.Domain,
MemberID: req.MemberID,
Type: models.UserCreate,
}
err = s.producer.ToKafkaUpdate(ctx, message)
if err != nil {
s.logger.Error("failed to send message to kafka on service webhook create", zap.Error(err))
return err
}
return nil
}
message := models.KafkaMessage{
AccountID: req.AccountID,
AuthCode: req.Code,
RefererURL: req.Domain,
Type: models.UserReLogin,
}
err = s.producer.ToKafkaUpdate(ctx, message)
if err != nil {
s.logger.Error("failed to send message to kafka on service webhook create, user re-login", zap.Error(err))
return err
}
return nil
}
//func (s *Service) WebhookDelete(ctx context.Context, bitrixID int) error {
// err := s.repository.BitrixRepo.WebhookDelete(ctx, bitrixID)
// if err != nil {
// s.logger.Error("error canceled bitrix integration", zap.Error(err))
// return err
// }
// return nil
//}

104
internal/tools/construct.go Normal file

@ -0,0 +1,104 @@
package tools
import (
"fmt"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/models"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"strconv"
"strings"
"unicode/utf8"
)
func ToPipeline(bitrixPipelines []models.Category, bitrixID string) []model.PipelineBitrix {
var pipelines []model.PipelineBitrix
for _, p := range bitrixPipelines {
pipelines = append(pipelines, model.PipelineBitrix{
BitrixID: p.ID,
Name: p.Name,
EntityTypeId: p.EntityTypeId,
AccountID: bitrixID,
})
}
return pipelines
}
func ToStep(bitrixPipelines []models.Steps, bitrixID string) ([]model.StepBitrix, error) {
var pipelines []model.StepBitrix
for _, p := range bitrixPipelines {
pipelineID, err := strconv.ParseInt(p.ID, 10, 64)
if err != nil {
return nil, err
}
pipelines = append(pipelines, model.StepBitrix{
BitrixID: p.ID,
AccountID: bitrixID,
EntityID: p.EntityID,
StatusID: p.StatusID,
Name: p.Name,
NameInit: p.NameInit,
Color: p.Color,
PipelineID: int32(pipelineID),
})
}
return pipelines, nil
}
func ToField(bitrixFields []models.Fields, bitrixID string) []model.BitrixField {
var fields []model.BitrixField
for _, f := range bitrixFields {
fields = append(fields, model.BitrixField{
AccountID: bitrixID,
BitrixID: f.ID,
EntityID: f.EntityID,
FieldName: f.FieldName,
EditFromLabel: f.EditFormLabel,
FieldType: f.UserTypeID,
})
}
return fields
}
func isEmoji(r rune) bool {
// https://symbl.cc/ru/unicode/blocks/emoticons/
return (r >= 0x1F600 && r <= 0x1F64F) || // эмотикоины
(r >= 0x1F650 && r <= 0x1F67F) || // орнаментные символы
(r >= 0x1F680 && r <= 0x1F6FF) || // Транспортные и картографические символы
(r >= 0x1F700 && r <= 0x1F77F) || // Алхимические символы
(r >= 0x1F780 && r <= 0x1F7FF) || // Расширенные геометрические фигуры
(r >= 0x1F800 && r <= 0x1F8FF) || // Дополнительные стрелки — С
(r >= 0x1F900 && r <= 0x1F9FF) || // Дополнительные символы и пиктограммы
(r >= 0x1FA00 && r <= 0x1FA6F) || // Шахматные символы
(r >= 0x1FA70 && r <= 0x1FAFF) || // Расширенные символы и пиктограммы — A
(r >= 0x1FB00 && r <= 0x1FBFF) || // Символы наследия вычислительной техники
(r >= 0x20000 && r <= 0x2A6DF) || // Унифицированные идеограммы ККЯ. Расширение B
(r >= 0x2A700 && r <= 0x2B73F) || // Унифицированные идеограммы ККЯ. Расширение C
(r >= 0x2B740 && r <= 0x2B81F) || // Унифицированные идеограммы ККЯ. Расширение D
(r >= 0x2B820 && r <= 0x2CEAF) || // Унифицированные идеограммы ККЯ. Расширение E
(r >= 0x2CEB0 && r <= 0x2EBEF) || // Унифицированные идеограммы ККЯ. Расширение F
(r >= 0x2EBF0 && r <= 0x2EE5F) || // CJK Unified Ideographs Extension I
(r >= 0x2F800 && r <= 0x2FA1F) || // Дополнение к совместимым идеограммам ККЯ
(r >= 0x30000 && r <= 0x3134F) || // Унифицированные идеограммы ККЯ. Расширение G
(r >= 0x31350 && r <= 0x323AF) || // Унифицированные идеограммы ККЯ. Расширение H
(r >= 0xE0000 && r <= 0xE007F) || // Теги
(r >= 0xE0100 && r <= 0xE01EF) // Дополнение к селекторам вариантов начертания
}
func EmojiUnicode(text string) string {
var result strings.Builder
for len(text) > 0 {
r, size := utf8.DecodeRuneInString(text)
if size == -1 {
result.WriteString(text[:1])
text = text[1:]
} else {
if isEmoji(r) {
result.WriteString(fmt.Sprintf(`"0x%x"`, r))
} else {
result.WriteRune(r)
}
text = text[size:]
}
}
return result.String()
}

106
internal/tools/forRules.go Normal file

@ -0,0 +1,106 @@
package tools
import (
"fmt"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/models"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"strings"
)
func ToQuestionIDs(rule map[int]string) []int32 {
var ids []int32
for queID, fieldID := range rule {
if fieldID != "" {
continue
}
ids = append(ids, int32(queID))
}
return ids
}
func ForContactRules(quizConfig model.QuizContact, currentFields []model.BitrixField) ([]models.AddFields, map[string]string) {
var contactFieldsArr []model.ContactQuizConfig
contactFieldTypes := map[model.ContactQuizConfig]bool{
model.TypeContactName: quizConfig.FormContact.Fields.Name.Used,
model.TypeContactEmail: quizConfig.FormContact.Fields.Email.Used,
model.TypeContactPhone: quizConfig.FormContact.Fields.Phone.Used,
model.TypeContactText: quizConfig.FormContact.Fields.Text.Used,
model.TypeContactAddress: quizConfig.FormContact.Fields.Address.Used,
}
for fieldType, used := range contactFieldTypes {
if used {
contactFieldsArr = append(contactFieldsArr, fieldType)
}
}
forAdding := make(map[string]string)
var toCreated []models.AddFields
for _, contactField := range contactFieldsArr {
matched := false
for _, field := range currentFields {
if field.EditFromLabel == string(contactField) && field.EntityID == model.FieldTypeContact {
matched = true
forAdding[string(contactField)] = field.BitrixID
break
}
}
if !matched {
toCreated = append(toCreated, models.AddFields{
UserTypeID: model.StringCustomFieldsType,
EditFormLabel: string(contactField),
ListColumnLabel: string(contactField),
})
forAdding[string(contactField)] = ""
}
}
return toCreated, forAdding
}
type ToUpdate struct {
FieldID string
Entity model.FieldsType
}
func ToCreatedUpdateQuestionRules(questionsTypeMap map[model.FieldsType][]model.Question, currentFields []model.BitrixField) (map[model.FieldsType][]models.AddFields, map[int]ToUpdate) {
toUpdate := make(map[int]ToUpdate) // на обновление ключ id вопроса значение id кастомного поля для тех у кого имя совпадает
toCreated := make(map[model.FieldsType][]models.AddFields)
for entity, questions := range questionsTypeMap {
for _, question := range questions {
// если заголоввок пустой у вопроса делаем ему заголовок чтоб в амо легли филды нормально
title := strings.ToLower(strings.ReplaceAll(question.Title, " ", ""))
if title == "" {
question.Title = fmt.Sprintf("Вопрос №%d", question.Page)
}
title = strings.ToLower(strings.ReplaceAll(question.Title, " ", ""))
matched := false
for _, field := range currentFields {
fieldName := strings.ToLower(strings.ReplaceAll(field.EditFromLabel, " ", ""))
if title == fieldName && entity == field.EntityID {
toUpdate[int(question.Id)] = ToUpdate{
FieldID: field.BitrixID,
Entity: entity,
}
matched = true
break
}
}
if !matched {
fieldType := model.StringCustomFieldsType
if question.Type == model.TypeFile {
fieldType = model.FileCustomFieldsType
}
toCreated[entity] = append(toCreated[entity], models.AddFields{
UserTypeID: fieldType,
EditFormLabel: question.Title,
ListColumnLabel: question.Title,
})
}
}
}
return toCreated, toUpdate
}

@ -0,0 +1,42 @@
package tools
import (
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
)
func ValidateUtmFields(response *model.UserListBitrixFieldsResp) *model.UserListBitrixFieldsResp {
checkUTM := map[string]struct{}{
"utm_content": {},
"utm_medium": {},
"utm_campaign": {},
"utm_source": {},
"utm_term": {},
"utm_referrer": {},
"roistat": {},
"referrer": {},
"openstat_service": {},
"openstat_campaign": {},
"openstat_ad": {},
"openstat_source": {},
"from": {},
"gclientid": {},
"_ym_uid": {},
"_ym_counter": {},
"gclid": {},
"yclid": {},
"fbclid": {},
}
data := &model.UserListBitrixFieldsResp{
Count: response.Count,
Items: []model.BitrixField{},
}
for _, r := range response.Items {
if _, ok := checkUTM[r.EditFromLabel]; !ok {
data.Items = append(data.Items, r)
}
}
return data
}

@ -0,0 +1,56 @@
package data_updater
import (
"context"
"go.uber.org/zap"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/brokers"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/models"
"time"
)
type Deps struct {
Logger *zap.Logger
Producer *brokers.Producer
}
type DataUpdater struct {
logger *zap.Logger
producer *brokers.Producer
}
func NewDataUpdaterWC(deps Deps) *DataUpdater {
return &DataUpdater{
logger: deps.Logger,
producer: deps.Producer,
}
}
// раз в час так как акес токен живет час
func (wc *DataUpdater) Start(ctx context.Context) {
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()
for {
select {
case <-ticker.C:
wc.processTasks(ctx)
case <-ctx.Done():
return
}
}
}
func (wc *DataUpdater) processTasks(ctx context.Context) {
err := wc.producer.ToKafkaUpdate(ctx, models.KafkaMessage{
Type: models.AllDataUpdate,
AccountID: "",
})
if err != nil {
wc.logger.Error("error send task in kafka ro update all data", zap.Error(err))
return
}
}
func (wc *DataUpdater) Stop(_ context.Context) error {
return nil
}

@ -0,0 +1,58 @@
package limiter
import (
"context"
"sync"
"time"
)
type RateLimiter struct {
requests chan struct{}
done chan struct{}
maxRequests int
Interval time.Duration
mutex sync.Mutex
}
func NewRateLimiter(ctx context.Context, maxRequests int, interval time.Duration) *RateLimiter {
limiter := &RateLimiter{
requests: make(chan struct{}, maxRequests),
done: make(chan struct{}),
maxRequests: maxRequests,
Interval: interval,
}
go limiter.start(ctx)
return limiter
}
func (limiter *RateLimiter) start(ctx context.Context) {
ticker := time.NewTicker(limiter.Interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
limiter.mutex.Lock()
for i := 0; i < len(limiter.requests); i++ {
<-limiter.requests
}
limiter.mutex.Unlock()
case <-ctx.Done():
return
}
}
}
func (limiter *RateLimiter) Check() bool {
select {
case limiter.requests <- struct{}{}:
return true
default:
return false
}
}
func (limiter *RateLimiter) Stop(_ context.Context) error {
return nil
}

@ -0,0 +1,799 @@
package post_deals_worker
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"go.uber.org/zap"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/models"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/tools"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/pkg/bitrixClient"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/pj_errors"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/repository/bitrix"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/utils"
"strconv"
"strings"
"time"
)
type Deps struct {
BitrixRepo *dal.BitrixDal
BitrixClient *bitrixClient.Bitrix
Logger *zap.Logger
}
type DealsWorker struct {
bitrixRepo *dal.BitrixDal
bitrixClient *bitrixClient.Bitrix
logger *zap.Logger
}
func NewDealsWC(deps Deps) *DealsWorker {
return &DealsWorker{
bitrixRepo: deps.BitrixRepo,
bitrixClient: deps.BitrixClient,
logger: deps.Logger,
}
}
func (wc *DealsWorker) Start(ctx context.Context) {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
wc.startFetching(ctx)
case <-ctx.Done():
return
}
}
}
func (wc *DealsWorker) startFetching(ctx context.Context) {
results, err := wc.bitrixRepo.BitrixRepo.GettingBitrixUsersTrueResults(ctx)
if err != nil {
wc.logger.Error("failed to fetch bitrix users true-results", zap.Error(err))
return
}
for _, result := range results {
userPrivileges, err := wc.bitrixRepo.AccountRepo.GetPrivilegesByAccountID(ctx, result.QuizAccountID)
if err != nil {
wc.logger.Error("error getting user privileges", zap.Error(err))
return
}
if !utils.VerifyUserPrivileges(userPrivileges) {
wc.logger.Info("User don't have active quizCnt or quizUnlim privileges, aborting")
continue
}
allAnswers, err := wc.bitrixRepo.AnswerRepo.GetAllAnswersByQuizID(ctx, result.Session)
if err != nil {
wc.logger.Error("error getting all user answers by result session", zap.Error(err))
return
}
// todo
//userTags, err := wc.amoRepo.AmoRepo.GetUserTagsByID(ctx, result.AmoAccountID)
//if err != nil {
// wc.logger.Error("error getting user tags by ano account id", zap.Error(err))
// return
//}'
performerIDInt, err := strconv.Atoi(result.PerformerID)
if err != nil {
wc.logger.Error("error parsing performerID", zap.Error(err))
return
}
var request interface{}
if result.LeadFlag {
request = models.CreatingLeadReq{
Fields: models.CreateLeadFields{
Title: fmt.Sprintf("lead quiz number %d", result.QuizID),
Opened: "Y",
AssignedByID: performerIDInt,
SourceID: result.SourceID,
StatusID: result.StatusID,
UtmSource: result.UTMs["utm_source"],
UtmCampaign: result.UTMs["utm_campaign"],
UtmContent: result.UTMs["utm_content"],
UtmMedium: result.UTMs["utm_medium"],
UtmTerm: result.UTMs["utm_term"],
},
}
} else {
request = models.CreatingDealReq{
Fields: models.CreateDealFields{
Title: fmt.Sprintf("deal quiz number %d", result.QuizID),
TypeID: result.TypeID,
StageID: result.StageID,
Opened: "Y",
AssignedByID: performerIDInt,
CategoryID: result.PipelineID,
SourceID: result.SourceID,
UtmSource: result.UTMs["utm_source"],
UtmCampaign: result.UTMs["utm_campaign"],
UtmContent: result.UTMs["utm_content"],
UtmMedium: result.UTMs["utm_medium"],
UtmTerm: result.UTMs["utm_term"],
},
}
}
reqMap, err := wc.constructField(ctx, request, allAnswers, result, performerIDInt)
if err != nil {
wc.logger.Error("error construct fields", zap.Error(err))
return
}
wc.logger.Info("NOW DEAL/LEAD CONSTRUCTED IS:", zap.Any("DEAL", reqMap))
var errCreating error
var id int32
if result.LeadFlag {
id, errCreating = wc.bitrixClient.CreatingLead(reqMap, result.AccessToken, result.SubDomain)
if errCreating != nil {
wc.logger.Error("error creating lead", zap.Error(errCreating))
}
} else {
id, errCreating = wc.bitrixClient.CreatingDeal(reqMap, result.AccessToken, result.SubDomain)
if errCreating != nil {
wc.logger.Error("error creating deal", zap.Error(err))
}
}
if errCreating != nil {
err = wc.bitrixRepo.BitrixRepo.SaveDealBitrixStatus(ctx, bitrix.SaveDealBitrixDeps{
DealID: 0,
AnswerID: result.AnswerID,
AccessToken: result.AccessToken,
Status: err.Error(),
})
if err != nil {
wc.logger.Error("error saving deal status", zap.Error(err))
return
}
return
}
fmt.Println("DEALID", id)
err = wc.bitrixRepo.BitrixRepo.SaveDealBitrixStatus(ctx, bitrix.SaveDealBitrixDeps{
DealID: id,
AnswerID: result.AnswerID,
AccessToken: result.AccessToken,
Status: "OK",
})
if err != nil {
wc.logger.Error("error saving deal status", zap.Error(err))
return
}
}
}
// todo ОБДУМАТЬ ЛИДЫ ПОЧЕМУ ЛИД ПРЕВРАЩАЕТСЯ В СДЕЛКУ
func (wc *DealsWorker) constructField(ctx context.Context, request interface{}, allAnswers []model.ResultAnswer, result model.BitrixUsersTrueResults, performerIDInt int) (map[string]map[string]interface{}, error) {
// id поля - ответ по типу поля
entityFieldsMap := make(map[model.FieldsType]map[string]string)
entityFieldsMap[model.FieldTypeDeal] = make(map[string]string)
entityFieldsMap[model.FieldTypeLead] = make(map[string]string)
entityFieldsMap[model.FieldTypeCompany] = make(map[string]string)
entityFieldsMap[model.FieldTypeContact] = make(map[string]string)
// тип поля = правила на этот тип
entityRules := make(map[model.FieldsType]map[int]string)
entityRules[model.FieldTypeDeal] = result.FieldsRule.Deal.QuestionID
entityRules[model.FieldTypeLead] = result.FieldsRule.Lead.QuestionID
entityRules[model.FieldTypeCompany] = result.FieldsRule.Company.QuestionID
entityRules[model.FieldTypeContact] = result.FieldsRule.Contact.QuestionID
fmt.Println("result.FieldsRule.Deal.QuestionID", result.FieldsRule.Deal.QuestionID)
fmt.Println("result.FieldsRule.Lead.QuestionID", result.FieldsRule.Lead.QuestionID)
for entityType, rule := range entityRules {
for _, data := range allAnswers {
if fieldID, ok := rule[int(data.QuestionID)]; ok {
fieldData, err := wc.bitrixRepo.BitrixRepo.GetFieldByID(ctx, fieldID)
if err != nil {
if err == sql.ErrNoRows {
wc.logger.Info("This field id does not exist in db", zap.Any("fieldID", fieldID))
continue
}
return nil, err
}
if fieldData.FieldType == model.FileCustomFieldsType {
continue
}
//if entityType != model.FieldTypeDeal {
if fieldData.FieldType == model.StringCustomFieldsType {
content := strings.ReplaceAll(data.Content, " ", "")
if content == "" {
data.Content = "Пустая строка"
}
entityFieldsMap[entityType][fieldData.FieldName] = tools.EmojiUnicode(data.Content)
continue
}
//}
// todo need test // "https://storage.yandexcloud.net/squizanswer/5873c188-9ece-4540-96d8-4c3cb451a13f/109965/coq1ei7ot84c73fnk5cg.pdf"
//fieldData.FieldType = model.FileCustomFieldsType
if fieldData.FieldType == model.FileCustomFieldsType && data.Content != "" {
fileName, base64Data, err := wc.bitrixClient.DownLoadFile(data.Content)
if err != nil {
return nil, err
}
fileEntry := models.FileField{
FileData: []string{fileName, base64Data},
}
fileEntryJson, err := json.Marshal(fileEntry)
if err != nil {
return nil, err
}
//entityFieldsMap[entityType]["UF_CRM_1729686490"] = string(fileEntryJson)
entityFieldsMap[entityType][fieldData.FieldName] = string(fileEntryJson)
continue
}
}
}
}
contactFields := entityFieldsMap[model.FieldTypeContact]
companyFields := entityFieldsMap[model.FieldTypeCompany]
dealFields := make(map[string]string)
var dealReq models.CreatingDealReq
leadFields := make(map[string]string)
var leadReq models.CreatingLeadReq
if result.LeadFlag {
leadReq = request.(models.CreatingLeadReq)
leadFields = entityFieldsMap[model.FieldTypeLead]
} else {
dealFields = entityFieldsMap[model.FieldTypeDeal]
dealReq = request.(models.CreatingDealReq)
}
var resultInfo model.ResultContent
err := json.Unmarshal([]byte(result.Content), &resultInfo)
if err != nil {
return nil, err
}
contactRuleMap := result.FieldsRule.Contact.ContactRuleMap
contactFields, err = wc.addContactFields(ctx, contactFields, resultInfo.Name, model.TypeContactName, contactRuleMap)
if err != nil {
wc.logger.Error("error getting field data in addContactFields --- Name", zap.Error(err))
return nil, err
}
if len(resultInfo.Phone) > 4 || resultInfo.Phone != "" {
contactFields, err = wc.addContactFields(ctx, contactFields, resultInfo.Phone, model.TypeContactPhone, contactRuleMap)
if err != nil {
wc.logger.Error("error getting field data in addContactFields --- Phone", zap.Error(err))
return nil, err
}
}
contactFields, err = wc.addContactFields(ctx, contactFields, resultInfo.Text, model.TypeContactText, contactRuleMap)
if err != nil {
wc.logger.Error("error getting field data in addContactFields --- Text", zap.Error(err))
return nil, err
}
contactFields, err = wc.addContactFields(ctx, contactFields, resultInfo.Email, model.TypeContactEmail, contactRuleMap)
if err != nil {
wc.logger.Error("error getting field data in addContactFields --- Email", zap.Error(err))
return nil, err
}
contactFields, err = wc.addContactFields(ctx, contactFields, resultInfo.Address, model.TypeContactAddress, contactRuleMap)
if err != nil {
wc.logger.Error("error getting field data in addContactFields --- Address", zap.Error(err))
return nil, err
}
//check duplicate contacts
var fields []string
var contactID int32
name := resultInfo.Name
if name == "" {
name = fmt.Sprintf("empty name, quiz %d, triggered by answer - %d", result.QuizID, result.AnswerID)
}
if len(resultInfo.Phone) > 4 || resultInfo.Phone != "" {
fields = append(fields, resultInfo.Phone)
}
if resultInfo.Email != "" {
fields = append(fields, resultInfo.Email)
}
existContactData, err := wc.bitrixRepo.BitrixRepo.GetExistingContactBitrix(ctx, result.AmoAccountID, fields)
if err != nil && !errors.Is(err, pj_errors.ErrNotFound) {
return nil, err
}
if errors.Is(err, pj_errors.ErrNotFound) || len(existContactData) == 0 {
fmt.Println("NO CONTACT", contactFields)
contactReq := models.CreateContactReq{
Fields: models.ContactFields{
Name: name,
TypeID: result.TypeID,
SourceID: result.SourceID,
Opened: "Y",
UtmSource: result.UTMs["utm_source"],
UtmCampaign: result.UTMs["utm_campaign"],
UtmContent: result.UTMs["utm_content"],
UtmMedium: result.UTMs["utm_medium"],
UtmTerm: result.UTMs["utm_term"],
},
}
contactReqMap := models.FormattingToMap(&contactReq, contactFields)
contactID, err = wc.bitrixClient.CreateContact(contactReqMap, result.AccessToken, result.SubDomain)
if err != nil {
wc.logger.Error("error creating contact", zap.Error(err))
return nil, err
}
if len(resultInfo.Phone) > 4 || resultInfo.Phone != "" {
_, err = wc.bitrixRepo.BitrixRepo.InsertContactBitrix(ctx, model.ContactBitrix{
AccountID: result.AmoAccountID,
BitrixID: contactID,
Field: resultInfo.Phone,
})
if err != nil {
return nil, err
}
}
if resultInfo.Email != "" {
_, err = wc.bitrixRepo.BitrixRepo.InsertContactBitrix(ctx, model.ContactBitrix{
AccountID: result.AmoAccountID,
BitrixID: contactID,
Field: resultInfo.Email,
})
if err != nil {
return nil, err
}
}
} else if existContactData != nil && len(existContactData) > 0 {
contactID, err = wc.chooseAndCreateContact(ctx, result, resultInfo, existContactData, contactFields, contactRuleMap)
if err != nil {
return nil, err
}
}
companyReq := models.CompanyReq{
Fields: models.CompanyFields{
Title: fmt.Sprintf("Компания %d", result.AnswerID),
AssignedByID: performerIDInt,
Opened: "Y",
UtmSource: result.UTMs["utm_source"],
UtmCampaign: result.UTMs["utm_campaign"],
UtmContent: result.UTMs["utm_content"],
UtmMedium: result.UTMs["utm_medium"],
UtmTerm: result.UTMs["utm_term"],
ContactID: contactID,
},
}
companyReqMap := models.FormattingToMap(&companyReq, companyFields)
companyID, err := wc.bitrixClient.CreateCompany(companyReqMap, result.AccessToken, result.SubDomain)
if err != nil {
wc.logger.Error("error creating company", zap.Error(err))
return nil, err
}
var reqMap map[string]map[string]interface{}
if result.LeadFlag {
fmt.Println("leadFields", leadFields)
leadReq.Fields.CompanyID = companyID
leadReq.Fields.ContactIDs = append(leadReq.Fields.ContactIDs, contactID)
reqMap = models.FormattingToMap(&leadReq, leadFields)
} else {
fmt.Println("dealFields", dealFields)
dealReq.Fields.CompanyID = companyID
dealReq.Fields.ContactIDs = append(dealReq.Fields.ContactIDs, contactID)
//dealFields["UF_CRM_1729778229491"] = "UF_CRM_1729778229491"
reqMap = models.FormattingToMap(&dealReq, dealFields)
}
return reqMap, nil
}
func (wc *DealsWorker) addContactFields(ctx context.Context, contactFields map[string]string, fieldValue string, fieldType model.ContactQuizConfig, fieldMap map[string]string) (map[string]string, error) {
if fieldValue != "" && fieldMap[string(fieldType)] != "" {
fieldData, err := wc.bitrixRepo.BitrixRepo.GetFieldByID(ctx, fieldMap[string(fieldType)])
if err != nil {
return nil, err
}
contactFields[fieldData.FieldName] = fieldValue
}
return contactFields, nil
}
func (wc *DealsWorker) chooseAndCreateContact(ctx context.Context, result model.BitrixUsersTrueResults, resultInfo model.ResultContent, existingContacts map[int32][]model.ContactBitrix, contactFields map[string]string, contactRuleMap map[string]string) (int32, error) {
var err error
// 1 ищем контакт в котором совпадает и телефон и емайл
if (len(resultInfo.Phone) > 4 || resultInfo.Phone != "") && resultInfo.Email != "" {
phoneMatchedContacts := make(map[int32]bool)
for _, contactVariants := range existingContacts {
for _, contact := range contactVariants {
if contact.Field == resultInfo.Phone {
phoneMatchedContacts[contact.BitrixID] = true
}
}
}
for _, contactVariants := range existingContacts {
for _, contact := range contactVariants {
if contact.Field == resultInfo.Email {
if _, ok := phoneMatchedContacts[contact.BitrixID]; ok {
wc.logger.Info("нашлось телефон и емайл в бд, с одинаковым битрикс ид", zap.Any("битрикс ид", contact.BitrixID), zap.String("Name", resultInfo.Name), zap.String("Phone", resultInfo.Phone), zap.String("Email", resultInfo.Email))
return contact.BitrixID, nil
}
}
}
}
var phoneContactID, emailContactID int32
var phoneID int64 /*emailID*/
for _, contactVariants := range existingContacts {
for _, contact := range contactVariants {
if contact.Field == resultInfo.Phone {
phoneContactID = contact.BitrixID
phoneID = contact.ID
}
if contact.Field == resultInfo.Email {
emailContactID = contact.BitrixID
//emailID = contact.ID
}
}
}
if phoneContactID != 0 && emailContactID != 0 && phoneContactID != emailContactID {
wc.logger.Info("нашлось телефон и емайл в бд, но это пока разные контакты", zap.String("Name", resultInfo.Name), zap.String("Phone", resultInfo.Phone), zap.String("Email", resultInfo.Email))
// делаем обновление телефона там где уже есть email
valuePhone := make(map[string]string)
valuePhone, err = wc.addContactFields(ctx, valuePhone, resultInfo.Phone, model.TypeContactPhone, contactRuleMap)
if err != nil {
wc.logger.Error("error adding contact fields", zap.Error(err))
return 0, err
}
name := resultInfo.Name
if name == "" {
name = fmt.Sprintf("empty name, quiz %d, triggered by answer - %d", result.QuizID, result.AnswerID)
}
contactUpdateReq := models.CreateContactReq{
Fields: models.ContactFields{
Name: name,
},
}
contactUpdateReqMap := models.FormattingToMap(&contactUpdateReq, valuePhone)
err = wc.bitrixClient.UpdateContact(contactUpdateReqMap, result.AccessToken, result.SubDomain, emailContactID)
if err != nil {
wc.logger.Error("error updating contact", zap.Error(err))
return 0, err
}
err = wc.bitrixRepo.BitrixRepo.UpdateBitrixContact(ctx, phoneID, resultInfo.Phone, emailContactID)
if err != nil {
return 0, err
}
// todo пока без линковки
//_, err = wc.amoClient.LinkedContactToContact([]models.LinkedContactReq{
// {
// ToEntityID: emailContactID,
// ToEntityType: "contacts",
// //Metadata: struct {
// // //CatalogID int `json:"catalog_id"`
// // //Quantity int `json:"quantity"`
// // IsMain bool `json:"is_main"`
// // //UpdatedBy int `json:"updated_by"`
// // //PriceID int `json:"price_id"`
// //}(struct {
// // //CatalogID int
// // //Quantity int
// // IsMain bool
// // //UpdatedBy int
// // //PriceID int
// //}{IsMain: true}),
// },
//}, result.SubDomain, result.AccessToken, phoneContactID)
//if err != nil {
// return 0, err
//}
return emailContactID, nil
}
}
// 2 ищем контакт только по телефону
if len(resultInfo.Phone) > 4 || resultInfo.Phone != "" {
for _, contactVariants := range existingContacts {
for _, contact := range contactVariants {
if contact.Field == resultInfo.Phone {
// нашли контакт по телефону
emailExists := false
for _, variant := range existingContacts[contact.BitrixID] {
if variant.Field != contact.Field {
if variant.Field != "" {
emailExists = true
break
}
}
}
if !emailExists && resultInfo.Email != "" {
// email пустой обновляем контакт добавляя email, если не пустой
wc.logger.Info("нашлось телефон, емайл не пустой, а в бд пустой. обновляем контакт", zap.String("Name", resultInfo.Name), zap.String("Phone", resultInfo.Phone), zap.String("Email", resultInfo.Email))
valueEmail := make(map[string]string)
valueEmail, err = wc.addContactFields(ctx, valueEmail, resultInfo.Email, model.TypeContactEmail, contactRuleMap)
if err != nil {
wc.logger.Error("error adding contact fields", zap.Error(err))
return 0, err
}
name := resultInfo.Name
if name == "" {
name = fmt.Sprintf("empty name, quiz %d, triggered by answer - %d", result.QuizID, result.AnswerID)
}
contactUpdateReq := models.CreateContactReq{
Fields: models.ContactFields{
Name: name,
},
}
contactUpdateReqMap := models.FormattingToMap(&contactUpdateReq, valueEmail)
err = wc.bitrixClient.UpdateContact(contactUpdateReqMap, result.AccessToken, result.SubDomain, contact.BitrixID)
if err != nil {
wc.logger.Error("error updating contact", zap.Error(err))
return 0, err
}
_, err = wc.bitrixRepo.BitrixRepo.InsertContactBitrix(ctx, model.ContactBitrix{
AccountID: result.AmoAccountID,
BitrixID: contact.BitrixID,
Field: resultInfo.Email,
})
if err != nil {
return 0, err
}
return contact.BitrixID, nil
}
if emailExists && resultInfo.Email != "" {
// email не пустой значит это новый контакт создаем если наш email тоже не пустой
wc.logger.Info("нашлось телефон, емайл не пустой и в бд не пустой. создаем новый контакт", zap.String("Name", resultInfo.Name), zap.String("Phone", resultInfo.Phone), zap.String("Email", resultInfo.Email))
name := resultInfo.Name
if name == "" {
name = fmt.Sprintf("empty name, quiz %d, triggered by answer - %d", result.QuizID, result.AnswerID)
}
contactCreateReq := models.CreateContactReq{
Fields: models.ContactFields{
Name: name,
TypeID: result.TypeID,
SourceID: result.SourceID,
Opened: "Y",
UtmSource: result.UTMs["utm_source"],
UtmCampaign: result.UTMs["utm_campaign"],
UtmContent: result.UTMs["utm_content"],
UtmMedium: result.UTMs["utm_medium"],
UtmTerm: result.UTMs["utm_term"],
},
}
contactCreateReqMap := models.FormattingToMap(&contactCreateReq, contactFields)
contactID, err := wc.bitrixClient.CreateContact(contactCreateReqMap, result.AccessToken, result.SubDomain)
if err != nil {
wc.logger.Error("error creating contact", zap.Error(err))
return 0, err
}
_, err = wc.bitrixRepo.BitrixRepo.InsertContactBitrix(ctx, model.ContactBitrix{
AccountID: result.AmoAccountID,
BitrixID: contactID,
Field: resultInfo.Phone,
})
if err != nil {
return 0, err
}
_, err = wc.bitrixRepo.BitrixRepo.InsertContactBitrix(ctx, model.ContactBitrix{
AccountID: result.AmoAccountID,
BitrixID: contactID,
Field: resultInfo.Email,
})
if err != nil {
return 0, err
}
return contactID, nil
}
// если пустой то это нужный контакт возвращаем его id, так как если мейл пустой у нас но номер совпадает а в бд не пустой значит оно нам надо
wc.logger.Info("нашлось телефон, емайл пустой возвращаем существующий контакт", zap.String("Name", resultInfo.Name), zap.String("Phone", resultInfo.Phone), zap.String("Email", resultInfo.Email))
return contact.BitrixID, nil
}
}
}
}
// 3 ищем контакт только по email
if resultInfo.Email != "" {
for _, contactVariants := range existingContacts {
for _, contact := range contactVariants {
if contact.Field == resultInfo.Email {
// нашли контакт по email
phoneExists := false
for _, variant := range existingContacts[contact.BitrixID] {
if variant.Field != contact.Field {
if variant.Field != "" {
phoneExists = true
break
}
}
}
if !phoneExists && (len(resultInfo.Phone) > 4 || resultInfo.Phone != "") {
// телефон пустой обновляем контакт добавляя телефон, если не пустой
wc.logger.Info("нашлось емайл, телефон не пустой, а в бд пустой. обновляем контакт", zap.String("Name", resultInfo.Name), zap.String("Phone", resultInfo.Phone), zap.String("Email", resultInfo.Email))
valuePhone := make(map[string]string)
valuePhone, err = wc.addContactFields(ctx, valuePhone, resultInfo.Phone, model.TypeContactPhone, contactRuleMap)
if err != nil {
wc.logger.Error("error adding contact fields", zap.Error(err))
return 0, err
}
name := resultInfo.Name
if name == "" {
name = fmt.Sprintf("empty name, quiz %d, triggered by answer - %d", result.QuizID, result.AnswerID)
}
contactUpdateReq := models.CreateContactReq{
Fields: models.ContactFields{
Name: name,
},
}
contactUpdateReqMap := models.FormattingToMap(&contactUpdateReq, valuePhone)
err := wc.bitrixClient.UpdateContact(contactUpdateReqMap, result.AccessToken, result.SubDomain, contact.BitrixID)
if err != nil {
wc.logger.Error("error updating contact", zap.Error(err))
return 0, err
}
_, err = wc.bitrixRepo.BitrixRepo.InsertContactBitrix(ctx, model.ContactBitrix{
AccountID: result.AmoAccountID,
BitrixID: contact.BitrixID,
Field: resultInfo.Phone,
})
if err != nil {
return 0, err
}
return contact.BitrixID, nil
}
if phoneExists && (len(resultInfo.Phone) > 4 || resultInfo.Phone != "") {
// телефон не пустой значит это новый контакт создаем если наш телефон не пустой
wc.logger.Info("нашлось емайл, телефон не пустой и в бд не пустой. создаем новый контакт", zap.String("Name", resultInfo.Name), zap.String("Phone", resultInfo.Phone), zap.String("Email", resultInfo.Email))
name := resultInfo.Name
if name == "" {
name = fmt.Sprintf("empty name, quiz %d, triggered by answer - %d", result.QuizID, result.AnswerID)
}
contactCreateReq := models.CreateContactReq{
Fields: models.ContactFields{
Name: name,
TypeID: result.TypeID,
SourceID: result.SourceID,
Opened: "Y",
UtmSource: result.UTMs["utm_source"],
UtmCampaign: result.UTMs["utm_campaign"],
UtmContent: result.UTMs["utm_content"],
UtmMedium: result.UTMs["utm_medium"],
UtmTerm: result.UTMs["utm_term"],
},
}
contactCreateMapReq := models.FormattingToMap(&contactCreateReq, contactFields)
contactID, err := wc.bitrixClient.CreateContact(contactCreateMapReq, result.AccessToken, result.SubDomain)
if err != nil {
wc.logger.Error("error creating contact", zap.Error(err))
return 0, err
}
_, err = wc.bitrixRepo.BitrixRepo.InsertContactBitrix(ctx, model.ContactBitrix{
AccountID: result.AmoAccountID,
BitrixID: contactID,
Field: resultInfo.Phone,
})
if err != nil {
return 0, err
}
_, err = wc.bitrixRepo.BitrixRepo.InsertContactBitrix(ctx, model.ContactBitrix{
AccountID: result.AmoAccountID,
BitrixID: contactID,
Field: resultInfo.Email,
})
if err != nil {
return 0, err
}
return contactID, nil
}
// если пустой то это нужный контакт возвращаем его id, так как если телефон пустой у нас но мейл совпадает а в бд не пустой значит оно нам надо
wc.logger.Info("нашлось емайл, телефон пустой возвращаем существующий контакт", zap.String("Name", resultInfo.Name), zap.String("Phone", resultInfo.Phone), zap.String("Email", resultInfo.Email))
return contact.BitrixID, nil
}
}
}
}
wc.logger.Info("ничего не нашлось, создаем новый контакт", zap.String("Name", resultInfo.Name), zap.String("Phone", resultInfo.Phone), zap.String("Email", resultInfo.Email))
// если дошлю до сюда то это новый контакт с новым email and phone
name := resultInfo.Name
if name == "" {
name = fmt.Sprintf("empty name, quiz %d, triggered by answer - %d", result.QuizID, result.AnswerID)
}
contactCreateReq := models.CreateContactReq{
Fields: models.ContactFields{
Name: name,
TypeID: result.TypeID,
SourceID: result.SourceID,
Opened: "Y",
UtmSource: result.UTMs["utm_source"],
UtmCampaign: result.UTMs["utm_campaign"],
UtmContent: result.UTMs["utm_content"],
UtmMedium: result.UTMs["utm_medium"],
UtmTerm: result.UTMs["utm_term"],
},
}
contactCreateMapReq := models.FormattingToMap(&contactCreateReq, contactFields)
contactID, err := wc.bitrixClient.CreateContact(contactCreateMapReq, result.AccessToken, result.SubDomain)
if err != nil {
wc.logger.Error("error creating contact", zap.Error(err))
return 0, err
}
if len(resultInfo.Phone) > 4 || resultInfo.Phone != "" {
_, err = wc.bitrixRepo.BitrixRepo.InsertContactBitrix(ctx, model.ContactBitrix{
AccountID: result.AmoAccountID,
BitrixID: contactID,
Field: resultInfo.Phone,
})
if err != nil {
return 0, err
}
}
if resultInfo.Email != "" {
_, err = wc.bitrixRepo.BitrixRepo.InsertContactBitrix(ctx, model.ContactBitrix{
AccountID: result.AmoAccountID,
BitrixID: contactID,
Field: resultInfo.Email,
})
if err != nil {
return 0, err
}
}
return contactID, nil
}
func (wc *DealsWorker) Stop(_ context.Context) error {
return nil
}

@ -0,0 +1,260 @@
package queueUpdater
import (
"context"
"encoding/json"
"github.com/twmb/franz-go/pkg/kgo"
"go.uber.org/zap"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/models"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/workers_methods"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"time"
)
type QueueUpdater struct {
logger *zap.Logger
kafkaClient *kgo.Client
methods *workers_methods.Methods
}
type Deps struct {
Logger *zap.Logger
KafkaClient *kgo.Client
Methods *workers_methods.Methods
}
func NewQueueUpdater(deps Deps) *QueueUpdater {
return &QueueUpdater{
logger: deps.Logger,
kafkaClient: deps.KafkaClient,
methods: deps.Methods,
}
}
func (wc *QueueUpdater) Start(ctx context.Context) {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
wc.consumeMessages(ctx)
case <-ctx.Done():
return
}
}
}
func (wc *QueueUpdater) consumeMessages(ctx context.Context) {
fetches := wc.kafkaClient.PollFetches(ctx)
iter := fetches.RecordIter()
for !iter.Done() {
record := iter.Next()
var message models.KafkaMessage
err := json.Unmarshal(record.Value, &message)
if err != nil {
wc.logger.Error("error unmarshal kafka message:", zap.Error(err))
continue
}
err = wc.processMessages(ctx, message)
if err != nil {
wc.logger.Error("error processing kafka message:", zap.Error(err))
}
}
}
func (wc *QueueUpdater) processMessages(ctx context.Context, message models.KafkaMessage) error {
switch message.Type {
case models.UsersUpdate:
token, err := wc.methods.GetTokenByID(ctx, message.AccountID)
if err != nil {
wc.logger.Error("error getting user token from db", zap.Error(err))
return err
}
if token != nil {
err = wc.methods.CheckUsers(ctx, []model.Token{*token})
if err != nil {
wc.logger.Error("error update user information in queue worker", zap.Error(err))
return err
}
}
case models.PipelinesUpdate:
token, err := wc.methods.GetTokenByID(ctx, message.AccountID)
if err != nil {
wc.logger.Error("error getting user token from db", zap.Error(err))
return err
}
if token != nil {
err = wc.methods.CheckPipelines(ctx, []model.Token{*token})
if err != nil {
wc.logger.Error("error update user pipelines and steps information in queue worker", zap.Error(err))
return err
}
}
case models.StepsUpdate:
token, err := wc.methods.GetTokenByID(ctx, message.AccountID)
if err != nil {
wc.logger.Error("error getting user token from db", zap.Error(err))
return err
}
if token != nil {
err = wc.methods.CheckSteps(ctx, []model.Token{*token})
if err != nil {
wc.logger.Error("error update user pipelines and steps information in queue worker", zap.Error(err))
return err
}
}
case models.FieldsUpdate:
token, err := wc.methods.GetTokenByID(ctx, message.AccountID)
if err != nil {
wc.logger.Error("error getting user token from db", zap.Error(err))
return err
}
if token != nil {
err = wc.methods.CheckFields(ctx, []model.Token{*token})
if err != nil {
wc.logger.Error("error update user fields information in queue worker", zap.Error(err))
return err
}
}
//case models.TagsUpdate:
// token, err := wc.methods.GetTokenByID(ctx, message.AccountID)
// if err != nil {
// wc.logger.Error("error getting user token from db", zap.Error(err))
// return err
// }
//
// if token != nil {
// err = wc.methods.CheckTags(ctx, []model.Token{*token})
// if err != nil {
// wc.logger.Error("error update user tags information in queue worker", zap.Error(err))
// return err
// }
// }
case models.UserCreate:
token, err := wc.methods.CreateUserFromWebHook(ctx, message)
if err != nil {
wc.logger.Error("error creating user from webhook request", zap.Error(err))
return err
}
err = wc.methods.CheckUsers(ctx, []model.Token{token})
if err != nil {
wc.logger.Error("error update user information in queue worker", zap.Error(err))
return err
}
err = wc.methods.CheckPipelines(ctx, []model.Token{token})
if err != nil {
wc.logger.Error("error update user pipelines information in queue worker", zap.Error(err))
return err
}
err = wc.methods.CheckSteps(ctx, []model.Token{token})
if err != nil {
wc.logger.Error("error update user steps information in queue worker", zap.Error(err))
return err
}
err = wc.methods.CheckFields(ctx, []model.Token{token})
if err != nil {
wc.logger.Error("error update user fields information in queue worker", zap.Error(err))
return err
}
//err = wc.methods.CheckTags(ctx, []model.Token{*token})
//if err != nil {
// wc.logger.Error("error update user tags information in queue worker", zap.Error(err))
// return err
//}
case models.AllDataUpdate:
// сначала получаем список токенов
newTokens, err := wc.methods.UpdateTokens(ctx)
if err != nil {
wc.logger.Error("error updating tokens and getting new tokens", zap.Error(err))
return err
}
if len(newTokens) > 0 {
// обновляем информацию о пользователях
err = wc.methods.CheckUsers(ctx, newTokens)
if err != nil {
wc.logger.Error("error update users information", zap.Error(err))
return err
}
// обновляем информацию о pipelines
err = wc.methods.CheckPipelines(ctx, newTokens)
if err != nil {
wc.logger.Error("error updating users pipelines and users pipelines-steps", zap.Error(err))
return err
}
// обновляем информацию о steps
err = wc.methods.CheckSteps(ctx, newTokens)
if err != nil {
wc.logger.Error("error updating users pipelines and users pipelines-steps", zap.Error(err))
return err
}
// обновляем информацию о tags
//err = wc.methods.CheckTags(ctx, newTokens)
//if err != nil {
// wc.logger.Error("error updating users tags", zap.Error(err))
// return err
//}
// обновляем информацию о fields
err = wc.methods.CheckFields(ctx, newTokens)
if err != nil {
wc.logger.Error("error updating users fields", zap.Error(err))
return err
}
}
case models.RuleCheck:
token, err := wc.methods.GetTokenByID(ctx, message.AccountID)
if err != nil {
wc.logger.Error("error getting user token from db", zap.Error(err))
return err
}
if token != nil {
err = wc.methods.CheckFields(ctx, []model.Token{*token})
if err != nil {
wc.logger.Error("error update user fields information in queue worker", zap.Error(err))
return err
}
err = wc.methods.CheckFieldRule(ctx, token.AccessToken, message)
if err != nil {
wc.logger.Error("error check field rules for fields rules", zap.Error(err))
return err
}
}
case models.UserReLogin:
err := wc.methods.UserReLogin(ctx, message)
if err != nil {
wc.logger.Error("error update user information in re-login method", zap.Error(err))
return err
}
default:
wc.logger.Error("incorrect message type", zap.Any("Type:", message))
return nil
}
return nil
}
func (wc *QueueUpdater) Stop(_ context.Context) error {
return nil
}

@ -0,0 +1,779 @@
package workers_methods
import (
"context"
"encoding/json"
"errors"
"fmt"
"go.uber.org/zap"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/models"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/tools"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/pkg/bitrixClient"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"strings"
"sync"
"time"
)
type Methods struct {
repo *dal.BitrixDal
bitrixClient *bitrixClient.Bitrix
logger *zap.Logger
}
type Deps struct {
Repo *dal.BitrixDal
BitrixClient *bitrixClient.Bitrix
Logger *zap.Logger
}
func NewWorkersMethods(deps Deps) *Methods {
return &Methods{
repo: deps.Repo,
bitrixClient: deps.BitrixClient,
logger: deps.Logger,
}
}
func (m *Methods) UpdateTokens(ctx context.Context) ([]model.Token, error) {
allTokens, err := m.repo.BitrixRepo.GetAllTokens(ctx)
if err != nil {
m.logger.Error("error getting all tokens from db in UpdateTokens", zap.Error(err))
return nil, err
}
for _, oldToken := range allTokens {
req := models.UpdateWebHookReq{
GrantType: "refresh_token",
RefreshToken: oldToken.RefreshToken,
}
resp, err := m.bitrixClient.CreateWebHook(&req, false)
if err != nil {
m.logger.Error("error create webhook in UpdateTokens", zap.Error(err))
continue
}
newToken := model.Token{
AccountID: oldToken.AccountID,
RefreshToken: resp.RefreshToken,
AccessToken: resp.AccessToken,
Expiration: time.Now().Unix() + resp.ExpiresIn,
CreatedAt: time.Now().Unix(),
}
err = m.repo.BitrixRepo.WebhookUpdate(ctx, newToken)
if err != nil {
m.logger.Error("error update token in db", zap.Error(err))
return nil, err
}
}
newTokens, err := m.repo.BitrixRepo.GetAllTokens(ctx)
if err != nil {
m.logger.Error("error getting all new updated tokens from db in UpdateTokens", zap.Error(err))
return nil, err
}
return newTokens, nil
}
func (m *Methods) CheckUsers(ctx context.Context, allTokens []model.Token) error {
listUser := make(map[string][]models.User)
for _, token := range allTokens {
currentCompany, err := m.repo.BitrixRepo.GetCurrentAccount(ctx, token.AccountID)
if err != nil {
m.logger.Error("error getting current company", zap.Error(err))
return err
}
userData, err := m.bitrixClient.GetUserList(token.AccessToken, currentCompany.Subdomain)
if err != nil {
m.logger.Error("error fetching list users", zap.Error(err))
break
}
listUser[token.AccountID] = append(listUser[token.AccountID], userData.Result...)
}
for accountID, users := range listUser {
currentCompany, err := m.repo.BitrixRepo.GetCurrentAccount(ctx, accountID)
if err != nil {
m.logger.Error("error getting current company", zap.Error(err))
return err
}
currentUserUsers, err := m.repo.BitrixRepo.GetUserUsersByID(ctx, currentCompany.BitrixID)
if err != nil {
m.logger.Error("error getting user users by bitrix user id", zap.Error(err))
return err
}
for _, user := range users {
found := false
for _, currentUser := range currentUserUsers {
if user.ID == currentUser.BitrixIDUserID {
found = true
err := m.repo.BitrixRepo.UpdateBitrixAccountUser(ctx, model.BitrixAccountUser{
AccountID: currentUser.AccountID,
BitrixIDUserID: currentUser.BitrixIDUserID,
Name: user.Name,
LastName: user.LastName,
SecondName: user.SecondName,
Title: user.Title,
Email: user.Email,
UFDepartment: user.UFDepartment,
WorkPosition: user.WorkPosition,
})
if err != nil {
m.logger.Error("failed update user bitrix account in db", zap.Error(err))
return err
}
}
}
if !found {
err := m.repo.BitrixRepo.AddBitrixAccountUser(ctx, model.BitrixAccountUser{
AccountID: currentCompany.BitrixID,
BitrixIDUserID: user.ID,
Name: user.Name,
LastName: user.LastName,
SecondName: user.SecondName,
Title: user.Title,
Email: user.Email,
UFDepartment: user.UFDepartment,
WorkPosition: user.WorkPosition,
})
if err != nil {
m.logger.Error("failed insert user bitrix account in db", zap.Error(err))
return err
}
}
}
var deletedUserIDs []int64
for _, currentUserUser := range currentUserUsers {
found := false
for _, user := range users {
if currentUserUser.BitrixIDUserID == user.ID {
found = true
break
}
}
if !found {
deletedUserIDs = append(deletedUserIDs, currentUserUser.ID)
}
}
if len(deletedUserIDs) > 0 {
err := m.repo.BitrixRepo.DeleteUsers(ctx, deletedUserIDs)
if err != nil {
m.logger.Error("error deleting users in db", zap.Error(err))
return err
}
}
}
return nil
}
func (m *Methods) CheckPipelines(ctx context.Context, tokens []model.Token) error {
for _, token := range tokens {
currentCompany, err := m.repo.BitrixRepo.GetCurrentAccount(ctx, token.AccountID)
if err != nil {
m.logger.Error("error getting bitrix company by account quiz id", zap.Error(err))
return err
}
currentCompanyPipelines, err := m.repo.BitrixRepo.GetUserPipelinesByID(ctx, currentCompany.BitrixID)
if err != nil {
m.logger.Error("error getting company pipelines by bitrix id", zap.Error(err))
return err
}
var listPipelines []models.Category
for _, categoryType := range model.CategoryArr {
pipelinesResp, err := m.bitrixClient.GetListPipelines(categoryType, token.AccessToken, currentCompany.Subdomain)
if err != nil {
m.logger.Error("error fetching list pipelines from bitrix", zap.Error(err))
continue
}
listPipelines = append(listPipelines, pipelinesResp.Result.Categories...)
}
if len(listPipelines) > 0 {
receivedPipelines := tools.ToPipeline(listPipelines, currentCompany.BitrixID)
err = m.repo.BitrixRepo.CheckPipelines(ctx, receivedPipelines)
if err != nil {
m.logger.Error("error checking pipelines", zap.Error(err))
}
var deletedPipelineIDs []int64
for _, currentUserPipeline := range currentCompanyPipelines {
found := false
for _, receivedPipeline := range receivedPipelines {
if currentUserPipeline.BitrixID == receivedPipeline.BitrixID && currentUserPipeline.AccountID == currentCompany.BitrixID {
found = true
break
}
}
if !found {
deletedPipelineIDs = append(deletedPipelineIDs, currentUserPipeline.ID)
}
}
if len(deletedPipelineIDs) > 0 {
err := m.repo.BitrixRepo.DeletePipelines(ctx, deletedPipelineIDs)
if err != nil {
m.logger.Error("error deleting pipelines in db", zap.Error(err))
return err
}
}
}
}
return nil
}
func (m *Methods) CheckSteps(ctx context.Context, tokens []model.Token) error {
for _, token := range tokens {
currentCompany, err := m.repo.BitrixRepo.GetCurrentAccount(ctx, token.AccountID)
if err != nil {
m.logger.Error("error getting bitrix company by account quiz id", zap.Error(err))
return err
}
currentCompanySteps, err := m.repo.BitrixRepo.GetUserStepsByID(ctx, currentCompany.BitrixID)
if err != nil {
m.logger.Error("error getting company steps by bitrix id", zap.Error(err))
return err
}
var listSteps []models.Steps
stepsResp, err := m.bitrixClient.GetListSteps(token.AccessToken, currentCompany.Subdomain)
if err != nil {
m.logger.Error("error fetching list steps from bitrix", zap.Error(err))
continue
}
listSteps = append(listSteps, stepsResp.Result...)
if len(listSteps) > 0 {
receivedSteps, err := tools.ToStep(listSteps, currentCompany.BitrixID)
if err != nil {
m.logger.Error("error converting steps to bitrix", zap.Error(err))
return err
}
err = m.repo.BitrixRepo.CheckSteps(ctx, receivedSteps)
if err != nil {
m.logger.Error("error checking steps", zap.Error(err))
}
var deletedStepIDs []int64
for _, currentUserStep := range currentCompanySteps {
found := false
for _, receivedStep := range receivedSteps {
if currentUserStep.BitrixID == receivedStep.BitrixID && currentUserStep.AccountID == currentCompany.BitrixID {
found = true
break
}
}
if !found {
deletedStepIDs = append(deletedStepIDs, currentUserStep.ID)
}
}
if len(deletedStepIDs) > 0 {
err := m.repo.BitrixRepo.DeleteSteps(ctx, deletedStepIDs)
if err != nil {
m.logger.Error("error deleting steps in db", zap.Error(err))
return err
}
}
}
}
return nil
}
//func (m *Methods) CheckTags(ctx context.Context, tokens []model.Token) error {
// for _, token := range tokens {
// user, err := m.repo.AmoRepo.GetCurrentAccount(ctx, token.AccountID)
// if err != nil {
// m.logger.Error("error getting amoUserInfo by account quiz id", zap.Error(err))
// return err
// }
//
// currentUserTags, err := m.repo.AmoRepo.GetUserTagsByID(ctx, user.AmoID)
// if err != nil {
// m.logger.Error("error getting user tags by amo id", zap.Error(err))
// return err
// }
//
// var wg sync.WaitGroup
// wg.Add(4)
//
// var tagsMap sync.Map
// entityTypes := []model.EntityType{model.LeadsType, model.ContactsType, model.CompaniesType, model.CustomersType}
// for _, entityType := range entityTypes {
// go func(entityType model.EntityType) {
// defer wg.Done()
// page := 1
// limit := 250
//
// for {
// req := models.GetListTagsReq{
// Page: page,
// Limit: limit,
// EntityType: entityType,
// }
// tags, err := m.amoClient.GetListTags(req, token.AccessToken, user.Subdomain)
// if err != nil {
// m.logger.Error("error getting list of tags", zap.Error(err))
// return
// }
//
// if tags == nil || len(tags.Embedded.Tags) == 0 {
// break
// }
//
// tagsMap.Store(entityType, tags.Embedded.Tags)
//
// page++
// }
// }(entityType)
// }
//
// wg.Wait()
//
// var deletedTagIDs []int64
// for _, currentUserTag := range currentUserTags {
// found := false
// for _, entityType := range entityTypes {
// if tags, ok := tagsMap.Load(entityType); ok {
// if len(tags.([]models.Tag)) > 0 {
// receivedTags := tools.ToTag(tags.([]models.Tag), entityType)
// for _, tag := range receivedTags {
// if currentUserTag.Amoid == tag.Amoid && currentUserTag.Accountid == user.AmoID && currentUserTag.Entity == entityType {
// found = true
// break
// }
// }
// }
// }
// if found {
// break
// }
// }
//
// if !found {
// deletedTagIDs = append(deletedTagIDs, currentUserTag.ID)
// }
// }
//
// if len(deletedTagIDs) > 0 {
// err = m.repo.AmoRepo.DeleteTags(ctx, deletedTagIDs)
// if err != nil {
// m.logger.Error("error deleting tags in db", zap.Error(err))
// return err
// }
// }
//
// for _, entityType := range entityTypes {
// if tags, ok := tagsMap.Load(entityType); ok {
// if len(tags.([]models.Tag)) > 0 {
// err := m.repo.AmoRepo.CheckTags(ctx, tools.ToTag(tags.([]models.Tag), entityType), token.AccountID)
// if err != nil {
// switch entityType {
// case model.LeadsType:
// m.logger.Error("error updating leads tags in db", zap.Error(err))
// return err
// case model.ContactsType:
// m.logger.Error("error updating contacts tags in db", zap.Error(err))
// return err
// case model.CompaniesType:
// m.logger.Error("error updating companies tags in db", zap.Error(err))
// return err
// case model.CustomersType:
// m.logger.Error("error updating customer tags in db", zap.Error(err))
// return err
// }
// }
// }
// }
// }
// }
// return nil
//}
func (m *Methods) CheckFields(ctx context.Context, tokens []model.Token) error {
for _, token := range tokens {
currentCompany, err := m.repo.BitrixRepo.GetCurrentAccount(ctx, token.AccountID)
if err != nil {
m.logger.Error("error getting company by account quiz id", zap.Error(err))
return err
}
currentUserFields, err := m.repo.BitrixRepo.GetUserFieldsByID(ctx, currentCompany.BitrixID)
if err != nil {
m.logger.Error("error getting user fields by bitrix id", zap.Error(err))
return err
}
var wg sync.WaitGroup
wg.Add(4)
var fieldsMap sync.Map
entityTypes := []model.FieldsType{model.FieldTypeCompany, model.FieldTypeLead, model.FieldTypeContact, model.FieldTypeDeal}
for _, entityType := range entityTypes {
go func(entityType model.FieldsType) {
defer wg.Done()
for {
fields, err := m.bitrixClient.GetListFields(entityType, token.AccessToken, currentCompany.Subdomain)
if err != nil {
m.logger.Error("error getting list of fields", zap.Error(err))
return
}
if fields == nil || len(fields.Result) == 0 {
break
}
fieldsMap.Store(entityType, fields.Result)
break
}
}(entityType)
}
wg.Wait()
var deletedFieldIDs []int64
for _, currentUserField := range currentUserFields {
found := false
for _, entityType := range entityTypes {
if fields, ok := fieldsMap.Load(entityType); ok {
if len(fields.([]models.Fields)) > 0 {
receivedFields := tools.ToField(fields.([]models.Fields), currentCompany.BitrixID)
for _, field := range receivedFields {
if currentUserField.BitrixID == field.BitrixID && currentUserField.AccountID == currentCompany.BitrixID && currentUserField.EntityID == entityType {
found = true
break
}
}
}
}
if found {
break
}
}
if !found {
deletedFieldIDs = append(deletedFieldIDs, currentUserField.ID)
}
}
if len(deletedFieldIDs) > 0 {
err = m.repo.BitrixRepo.DeleteFields(ctx, deletedFieldIDs)
if err != nil {
m.logger.Error("error deleting fields in db", zap.Error(err))
return err
}
}
for _, entityType := range entityTypes {
if fields, ok := fieldsMap.Load(entityType); ok {
if len(fields.([]models.Fields)) > 0 {
err := m.repo.BitrixRepo.CheckFields(ctx, tools.ToField(fields.([]models.Fields), currentCompany.BitrixID), token.AccountID)
if err != nil {
switch entityType {
case model.FieldTypeLead:
m.logger.Error("error updating leads fields in db", zap.Error(err))
return err
case model.FieldTypeContact:
m.logger.Error("error updating contacts fields in db", zap.Error(err))
return err
case model.FieldTypeCompany:
m.logger.Error("error updating companies fields in db", zap.Error(err))
return err
case model.FieldTypeDeal:
m.logger.Error("error updating deal fields in db", zap.Error(err))
return err
}
}
}
}
}
}
return nil
}
func (m *Methods) GetTokenByID(ctx context.Context, accountID string) (*model.Token, error) {
token, err := m.repo.BitrixRepo.GetTokenByID(ctx, accountID)
if err != nil {
m.logger.Error("error getting token by account id from db", zap.Error(err))
return nil, err
}
return token, nil
}
func (m *Methods) CreateUserFromWebHook(ctx context.Context, msg models.KafkaMessage) (model.Token, error) {
// получаем аксес и рефреш токены по коду авторизации, id битрикса ==member id
forGetTokens := models.CreateWebHookReq{
GrantType: "authorization_code",
Code: msg.AuthCode,
}
tokens, err := m.bitrixClient.CreateWebHook(&forGetTokens, true)
if err != nil {
m.logger.Error("error getting webhook in CreateUserFromWebHook:", zap.Error(err))
return model.Token{}, err
}
fmt.Println("tokens", tokens)
if tokens.AccessToken == "" || tokens.RefreshToken == "" {
return model.Token{}, errors.New("invalid token")
}
toCreate := model.BitrixAccount{
AccountID: msg.AccountID,
BitrixID: msg.MemberID,
Subdomain: msg.RefererURL,
}
err = m.repo.BitrixRepo.CreateAccount(ctx, toCreate)
if err != nil {
m.logger.Error("error create account in db in CreateUserFromWebHook", zap.Error(err))
return model.Token{}, err
}
err = m.repo.BitrixRepo.WebhookCreate(ctx, model.Token{
RefreshToken: tokens.RefreshToken,
AccessToken: tokens.AccessToken,
AccountID: msg.AccountID,
AuthCode: msg.AuthCode,
Expiration: time.Now().Unix() + tokens.ExpiresIn,
CreatedAt: time.Now().Unix(),
})
if err != nil {
m.logger.Error("error adding tokens to db in CreateUserFromWebHook", zap.Error(err))
return model.Token{}, err
}
return model.Token{
AccountID: msg.AccountID,
RefreshToken: tokens.RefreshToken,
AccessToken: tokens.AccessToken,
}, nil
}
func (m *Methods) CheckFieldRule(ctx context.Context, token string, msg models.KafkaMessage) error {
var (
leadIDs, companyIDs, dealIDs, contactIDs []int32
leadQuestions, companyQuestions, dealQuestions, contactQuestions []model.Question
questionsTypeMap = make(map[model.FieldsType][]model.Question)
newFields []model.BitrixField
lead, company, deal, contact model.BitrixFieldRule
currentFieldsRule = msg.Rule.FieldsRule
err error
)
user, err := m.repo.BitrixRepo.GetCurrentAccount(ctx, msg.AccountID)
if err != nil {
m.logger.Error("error getting user data by account id in check utms wc method", zap.Error(err))
return err
}
currentFields, err := m.repo.BitrixRepo.GetUserFieldsByID(ctx, user.BitrixID)
if err != nil {
m.logger.Error("error getting user fields by amo account id", zap.Error(err))
return err
}
quiz, err := m.repo.QuizRepo.GetQuizById(ctx, msg.AccountID, uint64(msg.Rule.QuizID))
if err != nil {
m.logger.Error("error getting quiz by quizID and accountID", zap.Error(err))
return err
}
var quizConfig model.QuizContact
err = json.Unmarshal([]byte(quiz.Config), &quizConfig)
if err != nil {
m.logger.Error("error serialization quizConfig to model QuizContact", zap.Error(err))
return err
}
leadIDs = tools.ToQuestionIDs(msg.Rule.FieldsRule.Lead.QuestionID)
dealIDs = tools.ToQuestionIDs(msg.Rule.FieldsRule.Deal.QuestionID)
companyIDs = tools.ToQuestionIDs(msg.Rule.FieldsRule.Company.QuestionID)
contactIDs = tools.ToQuestionIDs(msg.Rule.FieldsRule.Contact.QuestionID)
getQuestions := func(questionIDs []int32, questions *[]model.Question) {
if len(questionIDs) > 0 {
*questions, err = m.repo.QuestionRepo.GetQuestionListByIDs(ctx, questionIDs)
if err != nil {
m.logger.Error("error getting questions", zap.Error(err))
return
}
}
}
getQuestions(leadIDs, &leadQuestions)
getQuestions(dealIDs, &dealQuestions)
getQuestions(companyIDs, &companyQuestions)
getQuestions(contactIDs, &contactQuestions)
questionsTypeMap[model.FieldTypeLead] = append(questionsTypeMap[model.FieldTypeLead], leadQuestions...)
questionsTypeMap[model.FieldTypeDeal] = append(questionsTypeMap[model.FieldTypeDeal], dealQuestions...)
questionsTypeMap[model.FieldTypeCompany] = append(questionsTypeMap[model.FieldTypeCompany], companyQuestions...)
questionsTypeMap[model.FieldTypeContact] = append(questionsTypeMap[model.FieldTypeContact], contactQuestions...)
toCreated, toUpdate := tools.ToCreatedUpdateQuestionRules(questionsTypeMap, currentFields)
contactFieldsToCreate, forAdding := tools.ForContactRules(quizConfig, currentFields)
for entity, fields := range toCreated {
if len(fields) == 0 {
continue
}
for _, field := range fields {
field.GenFieldName()
createdID, err := m.bitrixClient.AddFields(field, entity, token, user.Subdomain)
if err != nil {
m.logger.Error("error adding fields to amo", zap.Any("type", entity), zap.Error(err))
continue
}
// todo need checking in prod
newFields = append(newFields, model.BitrixField{
BitrixID: fmt.Sprintf("%d", createdID),
EntityID: entity,
FieldName: "UF_CRM_" + field.FieldName,
EditFromLabel: field.EditFormLabel,
FieldType: field.UserTypeID,
})
}
}
if len(contactFieldsToCreate) > 0 {
for _, contactField := range contactFieldsToCreate {
contactField.GenFieldName()
createdID, err := m.bitrixClient.AddFields(contactField, model.FieldTypeContact, token, user.Subdomain)
if err != nil {
m.logger.Error("error adding fields to amo", zap.Any("type", model.FieldTypeContact), zap.Error(err))
continue
}
// todo need checking in prod
newFields = append(newFields, model.BitrixField{
BitrixID: fmt.Sprintf("%d", createdID),
EntityID: model.FieldTypeContact,
FieldName: "UF_CRM_" + contactField.FieldName,
EditFromLabel: contactField.EditFormLabel,
FieldType: contactField.UserTypeID,
})
if _, ok := forAdding[contactField.EditFormLabel]; ok {
forAdding[contactField.EditFormLabel] = fmt.Sprintf("%d", createdID)
}
}
}
if len(newFields) > 0 {
err = m.repo.BitrixRepo.CheckFields(ctx, newFields, msg.AccountID)
if err != nil {
m.logger.Error("error updating fields rule in db Check Fields", zap.Error(err))
return err
}
}
constructFieldRules := func(fieldRuleArrCurrent map[int]string, questions []model.Question, fieldRule *model.BitrixFieldRule, currentEntity model.FieldsType) {
ruleMap := make(map[int]string)
for questionID, fieldID := range fieldRuleArrCurrent {
if fieldID != "" {
// если fieldID уже заполнен добавляем его как есть
ruleMap[questionID] = fieldID
continue
}
for _, question := range questions {
if dataQues, ok := toUpdate[questionID]; ok {
if dataQues.Entity == currentEntity {
ruleMap[questionID] = dataQues.FieldID
break
}
}
if questionID == int(question.Id) {
// тут также делаем чтобы сверить филд с вопросом
title := strings.ToLower(strings.ReplaceAll(question.Title, " ", ""))
if title == "" {
question.Title = fmt.Sprintf("Вопрос №%d", question.Page)
}
title = strings.ToLower(strings.ReplaceAll(question.Title, " ", ""))
for _, field := range newFields {
fieldName := strings.ToLower(strings.ReplaceAll(field.EditFromLabel, " ", ""))
if title == fieldName && field.EntityID == currentEntity {
ruleMap[questionID] = field.BitrixID
}
}
}
}
}
fieldRule.QuestionID = ruleMap
}
constructFieldRules(currentFieldsRule.Lead.QuestionID, leadQuestions, &lead, model.FieldTypeLead)
constructFieldRules(currentFieldsRule.Deal.QuestionID, dealQuestions, &deal, model.FieldTypeDeal)
constructFieldRules(currentFieldsRule.Company.QuestionID, companyQuestions, &company, model.FieldTypeCompany)
constructFieldRules(currentFieldsRule.Contact.QuestionID, contactQuestions, &contact, model.FieldTypeContact)
err = m.repo.BitrixRepo.UpdateFieldRules(ctx, model.BitrixFieldRules{
Lead: lead,
Deal: deal,
Company: company,
Contact: model.BitrixContactRules{ContactRuleMap: forAdding, QuestionID: contact.QuestionID},
}, msg.AccountID, msg.Rule.QuizID)
if err != nil {
m.logger.Error("error updating fields rule in db", zap.Error(err))
return err
}
return nil
}
func (m *Methods) UserReLogin(ctx context.Context, msg models.KafkaMessage) error {
forGetTokens := models.CreateWebHookReq{
GrantType: "authorization_code",
Code: msg.AuthCode,
}
tokens, err := m.bitrixClient.CreateWebHook(&forGetTokens, true)
if err != nil {
m.logger.Error("error getting tokens in method user re-login:", zap.Error(err))
return err
}
toUpdate := model.BitrixAccount{
AccountID: msg.AccountID,
BitrixID: msg.MemberID,
Subdomain: msg.RefererURL,
}
err = m.repo.BitrixRepo.UpdateCurrentAccount(ctx, toUpdate)
if err != nil {
m.logger.Error("error update account in db in method user re-login", zap.Error(err))
return err
}
err = m.repo.BitrixRepo.WebhookUpdate(ctx, model.Token{
RefreshToken: tokens.RefreshToken,
AccessToken: tokens.AccessToken,
AccountID: msg.AccountID,
Expiration: time.Now().Unix() + tokens.ExpiresIn,
CreatedAt: time.Now().Unix(),
})
if err != nil {
m.logger.Error("error update tokens in db in method user re-login", zap.Error(err))
return err
}
return nil
}

939
pkg/bitrixClient/bitrix.go Normal file

@ -0,0 +1,939 @@
package bitrixClient
import (
"encoding/base64"
"encoding/json"
"fmt"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/models"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/workers/limiter"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"strings"
"sync"
"time"
)
type Bitrix struct {
fiberClient *fiber.Client
logger *zap.Logger
redirectionURL string
integrationID string
integrationSecret string
rateLimiter *limiter.RateLimiter
fileMutex sync.Mutex
}
type BitrixDeps struct {
FiberClient *fiber.Client
Logger *zap.Logger
RedirectionURL string
IntegrationID string
IntegrationSecret string
RateLimiter *limiter.RateLimiter
}
func NewBitrixClient(deps BitrixDeps) *Bitrix {
if deps.FiberClient == nil {
deps.FiberClient = fiber.AcquireClient()
}
return &Bitrix{
fiberClient: deps.FiberClient,
logger: deps.Logger,
redirectionURL: deps.RedirectionURL,
integrationSecret: deps.IntegrationSecret,
integrationID: deps.IntegrationID,
rateLimiter: deps.RateLimiter,
}
}
// todo для выполнения некоторых операций нужен определенный скоуп токена надо тоже проверить мою теорию по правам приложения
// todo растестить этот запрос пока не проходит
// https://dev.1c-bitrix.ru/rest_help/users/user_search.php
func (b *Bitrix) GetUserList(accessToken string, domain string) (*models.ResponseGetListUsers, error) {
for {
if b.rateLimiter.Check() {
uri := fmt.Sprintf("https://%s/rest/user.search", domain)
agent := b.fiberClient.Post(uri)
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err := range errs {
b.logger.Error("error sending request in GetUserList", zap.Error(err))
}
return nil, fmt.Errorf("request GetUserList failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("error GetUserList statusCode - %d, respBody - %s", statusCode, string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return nil, fmt.Errorf(errorMessage)
}
var userListResponse models.ResponseGetListUsers
err := json.Unmarshal(resBody, &userListResponse)
if err != nil {
b.logger.Error("error unmarshal ResponseGetListUsers:", zap.Error(err))
return nil, err
}
return &userListResponse, nil
}
time.Sleep(b.rateLimiter.Interval)
}
}
// https://dev.1c-bitrix.ru/learning/course/index.php?COURSE_ID=99&LESSON_ID=2486
// https://apidocs.bitrix24.ru/api-reference/oauth/index.html
func (b *Bitrix) CreateWebHook(req models.WebHookRequest, tp bool) (*models.CreateWebHookResp, error) {
for {
if b.rateLimiter.Check() {
req.SetClientID(b.integrationID)
req.SetClientSecret(b.integrationSecret)
var query string
if tp {
query = fmt.Sprintf(
"https://oauth.bitrix.info/oauth/token/?grant_type=%s&client_id=%s&client_secret=%s&code=%s",
req.GetGrantType(), b.integrationID, b.integrationSecret, req.GetToken(),
)
} else {
query = fmt.Sprintf(
"https://oauth.bitrix.info/oauth/token/?grant_type=%s&client_id=%s&client_secret=%s&refresh_token=%s",
req.GetGrantType(), b.integrationID, b.integrationSecret, req.GetToken(),
)
}
agent := b.fiberClient.Get(query)
agent.Set("Content-Type", "application/json")
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err := range errs {
b.logger.Error("error sending request in CreateWebHook for create or update tokens", zap.Error(err))
}
return nil, fmt.Errorf("request failed: %v", errs[0])
}
fmt.Println("CreateWebHook", string(resBody), statusCode)
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("received an incorrect response from CreateWebHook: %s", string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return nil, fmt.Errorf(errorMessage)
}
var tokens models.CreateWebHookResp
err := json.Unmarshal(resBody, &tokens)
if err != nil {
b.logger.Error("error unmarshal CreateWebHookResp:", zap.Error(err))
return nil, err
}
return &tokens, nil
}
time.Sleep(b.rateLimiter.Interval)
}
}
func (b *Bitrix) GetListSteps(accessToken string, domain string) (*models.StepsResponse, error) {
for {
if b.rateLimiter.Check() {
uri := fmt.Sprintf("https://%s/rest/crm.status.list", domain)
agent := b.fiberClient.Post(uri)
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err := range errs {
b.logger.Error("error sending request in GetListSteps", zap.Error(err))
}
return nil, fmt.Errorf("request GetListSteps failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("error GetListSteps statusCode - %d, respBody - %s", statusCode, string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return nil, fmt.Errorf(errorMessage)
}
var result models.StepsResponse
err := json.Unmarshal(resBody, &result)
if err != nil {
b.logger.Error("error unmarshal StepsResponse", zap.Error(err))
return nil, err
}
return &result, nil
}
time.Sleep(b.rateLimiter.Interval)
}
}
func (b *Bitrix) GetListPipelines(entityTypeID model.IntegerEntityType, accessToken string, domain string) (*models.CategoryResponse, error) {
for {
if b.rateLimiter.Check() {
uri := fmt.Sprintf("https://%s/rest/crm.category.list", domain)
agent := b.fiberClient.Post(uri)
agent.Set("Authorization", "Bearer "+accessToken)
agent.Set("Content-Type", "application/json")
requestBody := map[string]interface{}{
"entityTypeId": entityTypeID,
}
agent.JSON(requestBody)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err := range errs {
b.logger.Error("error sending request in GetListPipelines", zap.Error(err))
}
return nil, fmt.Errorf("request GetListPipelines failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("error GetListPipelines statusCode - %d, respBody - %s", statusCode, string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return nil, fmt.Errorf(errorMessage)
}
var result models.CategoryResponse
err := json.Unmarshal(resBody, &result)
if err != nil {
b.logger.Error("error unmarshal CategoryResponse", zap.Error(err))
return nil, err
}
return &result, nil
}
time.Sleep(b.rateLimiter.Interval)
}
}
func (b *Bitrix) GetListFields(fieldType model.FieldsType, accessToken string, domain string) (*models.FieldsResponse, error) {
for {
if b.rateLimiter.Check() {
var listFields models.FieldsResponse
switch fieldType {
case model.FieldTypeCompany:
fullURL := fmt.Sprintf("https://%s/rest/crm.company.userfield.list", domain)
agent := b.fiberClient.Post(fullURL)
agent.Set("Authorization", "Bearer "+accessToken)
requestBody := map[string]interface{}{
"order": map[string]string{"SORT": "ASC"},
"filter": map[string]string{"LANG": "ru"},
}
agent.JSON(requestBody)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err := range errs {
b.logger.Error("error sending request in GetListFields", zap.Error(err))
}
return nil, fmt.Errorf("request GetListFields failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("received an incorrect response from GetListFields: %s", string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return nil, fmt.Errorf(errorMessage)
}
err := json.Unmarshal(resBody, &listFields)
if err != nil {
b.logger.Error("error unmarshal models.Company:", zap.Error(err))
return nil, err
}
return &listFields, nil
case model.FieldTypeLead:
fullURL := fmt.Sprintf("https://%s/rest/crm.lead.userfield.list", domain)
agent := b.fiberClient.Post(fullURL)
agent.Set("Authorization", "Bearer "+accessToken)
requestBody := map[string]interface{}{
"order": map[string]string{"SORT": "ASC"},
"filter": map[string]string{"LANG": "ru"},
}
agent.JSON(requestBody)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err := range errs {
b.logger.Error("error sending request in GetListFields", zap.Error(err))
}
return nil, fmt.Errorf("request GetListFields failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("received an incorrect response from GetListFields: %s", string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return nil, fmt.Errorf(errorMessage)
}
err := json.Unmarshal(resBody, &listFields)
if err != nil {
b.logger.Error("error unmarshal models.Lead:", zap.Error(err))
return nil, err
}
return &listFields, nil
case model.FieldTypeContact:
fullURL := fmt.Sprintf("https://%s/rest/crm.contact.userfield.list", domain)
agent := b.fiberClient.Post(fullURL)
agent.Set("Authorization", "Bearer "+accessToken)
requestBody := map[string]interface{}{
"order": map[string]string{"SORT": "ASC"},
"filter": map[string]string{"LANG": "ru"},
}
agent.JSON(requestBody)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err := range errs {
b.logger.Error("error sending request in GetListFields", zap.Error(err))
}
return nil, fmt.Errorf("request GetListFields failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("received an incorrect response from GetListFields: %s", string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return nil, fmt.Errorf(errorMessage)
}
err := json.Unmarshal(resBody, &listFields)
if err != nil {
b.logger.Error("error unmarshal models.Contact:", zap.Error(err))
return nil, err
}
return &listFields, nil
case model.FieldTypeDeal:
fullURL := fmt.Sprintf("https://%s/rest/crm.deal.userfield.list", domain)
agent := b.fiberClient.Post(fullURL)
agent.Set("Authorization", "Bearer "+accessToken)
requestBody := map[string]interface{}{
"order": map[string]string{"SORT": "ASC"},
"filter": map[string]string{"LANG": "ru"},
}
agent.JSON(requestBody)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err := range errs {
b.logger.Error("error sending request in GetListFields", zap.Error(err))
}
return nil, fmt.Errorf("request GetListFields failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("received an incorrect response from GetListFields: %s", string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return nil, fmt.Errorf(errorMessage)
}
err := json.Unmarshal(resBody, &listFields)
if err != nil {
b.logger.Error("error unmarshal models.Company:", zap.Error(err))
return nil, err
}
return &listFields, nil
}
}
time.Sleep(b.rateLimiter.Interval)
}
}
func (b *Bitrix) GetListTags() {
}
func (b *Bitrix) GetCurrentUser(accessToken string, domain string) (*models.ResponseGetCurrentUser, error) {
for {
if b.rateLimiter.Check() {
uri := fmt.Sprintf("https://%s/rest/user.current", domain)
agent := b.fiberClient.Post(uri)
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err := range errs {
b.logger.Error("error sending request in GetCurrentUser", zap.Error(err))
}
return nil, fmt.Errorf("request GetCurrentUser failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("error GetCurrentUser statusCode - %d, respBody - %s", statusCode, string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return nil, fmt.Errorf(errorMessage)
}
var result models.ResponseGetCurrentUser
err := json.Unmarshal(resBody, &result)
if err != nil {
b.logger.Error("error unmarshal CurrentUser", zap.Error(err))
return nil, err
}
return &result, nil
}
time.Sleep(b.rateLimiter.Interval)
}
}
// before neeed call req.GenFieldName()
func (b *Bitrix) AddFields(req models.AddFields, entity model.FieldsType, accessToken string, domain string) (int32, error) {
for {
if b.rateLimiter.Check() {
fmt.Println("REQ", req)
var result models.MultiResp
switch entity {
case model.FieldTypeContact:
bodyBytes, err := json.Marshal(req)
if err != nil {
b.logger.Error("error marshal req in Add Fields:", zap.Error(err))
return 0, err
}
uri := fmt.Sprintf("https://%s/rest/crm.contact.userfield.add", domain)
agent := b.fiberClient.Post(uri)
agent.Set("Content-Type", "application/json").Body(bodyBytes)
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err := range errs {
b.logger.Error("error sending request in AddFields", zap.Error(err))
}
return 0, fmt.Errorf("request AddFields failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("error AddFields contact statusCode - %d, respBody - %s", statusCode, string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return 0, fmt.Errorf(errorMessage)
}
err = json.Unmarshal(resBody, &result)
if err != nil {
b.logger.Error("error unmarshal AddFields", zap.Error(err))
return 0, err
}
return result.ID, nil
case model.FieldTypeCompany:
bodyBytes, err := json.Marshal(req)
if err != nil {
b.logger.Error("error marshal req in Add Fields:", zap.Error(err))
return 0, err
}
uri := fmt.Sprintf("https://%s/rest/crm.company.userfield.add", domain)
agent := b.fiberClient.Post(uri)
agent.Set("Content-Type", "application/json").Body(bodyBytes)
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err := range errs {
b.logger.Error("error sending request in AddFields", zap.Error(err))
}
return 0, fmt.Errorf("request AddFields failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("error AddFields company statusCode - %d, respBody - %s", statusCode, string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return 0, fmt.Errorf(errorMessage)
}
err = json.Unmarshal(resBody, &result)
if err != nil {
b.logger.Error("error unmarshal AddFields", zap.Error(err))
return 0, err
}
return result.ID, nil
case model.FieldTypeDeal:
bodyBytes, err := json.Marshal(req)
if err != nil {
b.logger.Error("error marshal req in Add Fields:", zap.Error(err))
return 0, err
}
uri := fmt.Sprintf("https://%s/rest/crm.deal.userfield.add", domain)
agent := b.fiberClient.Post(uri)
agent.Set("Content-Type", "application/json").Body(bodyBytes)
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err := range errs {
b.logger.Error("error sending request in AddFields", zap.Error(err))
}
return 0, fmt.Errorf("request AddFields failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("error AddFields deal statusCode - %d, respBody - %s", statusCode, string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return 0, fmt.Errorf(errorMessage)
}
err = json.Unmarshal(resBody, &result)
if err != nil {
b.logger.Error("error unmarshal AddFields", zap.Error(err))
return 0, err
}
return result.ID, nil
case model.FieldTypeLead:
bodyBytes, err := json.Marshal(req)
if err != nil {
b.logger.Error("error marshal req in Add Fields:", zap.Error(err))
return 0, err
}
uri := fmt.Sprintf("https://%s/rest/crm.lead.userfield.add", domain)
agent := b.fiberClient.Post(uri)
agent.Set("Content-Type", "application/json").Body(bodyBytes)
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err := range errs {
b.logger.Error("error sending request in AddFields", zap.Error(err))
}
return 0, fmt.Errorf("request AddFields failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("error AddFields lead statusCode - %d, respBody - %s", statusCode, string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return 0, fmt.Errorf(errorMessage)
}
err = json.Unmarshal(resBody, &result)
if err != nil {
b.logger.Error("error unmarshal AddFields", zap.Error(err))
return 0, err
}
return result.ID, nil
}
}
time.Sleep(b.rateLimiter.Interval)
}
}
func (b *Bitrix) CreatingDeal(req map[string]map[string]interface{}, accessToken string, domain string) (int32, error) {
for {
if b.rateLimiter.Check() {
uri := fmt.Sprintf("https://%s/rest/crm.deal.add", domain)
bodyBytes, err := json.Marshal(req)
if err != nil {
b.logger.Error("error marshal req in Creating Deal:", zap.Error(err))
return 0, err
}
agent := b.fiberClient.Post(uri)
agent.Set("Content-Type", "application/json").Body(bodyBytes)
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err = range errs {
b.logger.Error("error sending request in Creating Deal", zap.Error(err))
}
return 0, fmt.Errorf("request failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("received an incorrect response from Creating Deal: %s", string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return 0, fmt.Errorf(errorMessage)
}
var resp models.MultiResp
err = json.Unmarshal(resBody, &resp)
if err != nil {
b.logger.Error("error unmarshal response body in Creating Deal:", zap.Error(err))
return 0, err
}
return resp.ID, nil
}
time.Sleep(b.rateLimiter.Interval)
}
}
func (b *Bitrix) CreateCompany(req map[string]map[string]interface{}, accessToken string, domain string) (int32, error) {
for {
if b.rateLimiter.Check() {
uri := fmt.Sprintf("https://%s/rest/crm.company.add", domain)
bodyBytes, err := json.Marshal(req)
if err != nil {
b.logger.Error("error marshal req in Creating Company:", zap.Error(err))
return 0, err
}
agent := b.fiberClient.Post(uri)
agent.Set("Content-Type", "application/json").Body(bodyBytes)
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err = range errs {
b.logger.Error("error sending request in Creating Company", zap.Error(err))
}
return 0, fmt.Errorf("request failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("received an incorrect response from Creating Company: %s", string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return 0, fmt.Errorf(errorMessage)
}
var result models.MultiResp
err = json.Unmarshal(resBody, &result)
if err != nil {
b.logger.Error("error unmarshal response body in Creating Company:", zap.Error(err))
return 0, err
}
return result.ID, nil
}
time.Sleep(b.rateLimiter.Interval)
}
}
func (b *Bitrix) CreateContact(req map[string]map[string]interface{}, accessToken string, domain string) (int32, error) {
for {
if b.rateLimiter.Check() {
uri := fmt.Sprintf("https://%s/rest/crm.contact.add", domain)
bodyBytes, err := json.Marshal(req)
if err != nil {
b.logger.Error("error marshal req in Creating Contact:", zap.Error(err))
return 0, err
}
agent := b.fiberClient.Post(uri)
agent.Set("Content-Type", "application/json").Body(bodyBytes)
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err = range errs {
b.logger.Error("error sending request in Creating Contact", zap.Error(err))
}
return 0, fmt.Errorf("request failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("received an incorrect response from Creating Contact: %s", string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return 0, fmt.Errorf(errorMessage)
}
var result models.MultiResp
err = json.Unmarshal(resBody, &result)
if err != nil {
b.logger.Error("error unmarshal response body in Creating Contact:", zap.Error(err))
return 0, err
}
return result.ID, nil
}
time.Sleep(b.rateLimiter.Interval)
}
}
func (b *Bitrix) UpdateContact(req map[string]map[string]interface{}, accessToken string, domain string, contactID int32) error {
for {
if b.rateLimiter.Check() {
uri := fmt.Sprintf("https://%s/rest/crm.contact.update?ID=%d", domain, contactID)
bodyBytes, err := json.Marshal(req)
if err != nil {
b.logger.Error("error marshal req in Updating Contact:", zap.Error(err))
return err
}
agent := b.fiberClient.Post(uri)
agent.Set("Content-Type", "application/json").Body(bodyBytes)
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err = range errs {
b.logger.Error("error sending request in Updating Contact", zap.Error(err))
}
return fmt.Errorf("request failed: %v", errs[0])
}
b.logger.Info("received response from Updating Contact:", zap.String("resBody", string(resBody)))
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("received an incorrect response from Creating Contact: %s", string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return fmt.Errorf(errorMessage)
}
return nil
}
time.Sleep(b.rateLimiter.Interval)
}
}
func (b *Bitrix) CreatingLead(req map[string]map[string]interface{}, accessToken string, domain string) (int32, error) {
for {
if b.rateLimiter.Check() {
uri := fmt.Sprintf("https://%s/rest/crm.lead.add", domain)
bodyBytes, err := json.Marshal(req)
if err != nil {
b.logger.Error("error marshal req in Creating Lead:", zap.Error(err))
return 0, err
}
agent := b.fiberClient.Post(uri)
agent.Set("Content-Type", "application/json").Body(bodyBytes)
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err = range errs {
b.logger.Error("error sending request in Creating Lead", zap.Error(err))
return 0, fmt.Errorf("request failed: %v", errs[0])
}
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("received an incorrect response from Creating Lead: %s", string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return 0, fmt.Errorf(errorMessage)
}
var result models.MultiResp
err = json.Unmarshal(resBody, &result)
if err != nil {
b.logger.Error("error unmarshal response body in Creating Lead:", zap.Error(err))
return 0, err
}
return result.ID, nil
}
time.Sleep(b.rateLimiter.Interval)
}
}
func (b *Bitrix) GetCustomFieldByID(fieldID int32, fieldType model.FieldsType, accessToken string, domain string) (*models.Fields, error) {
for {
if b.rateLimiter.Check() {
var resp models.Fields
var err error
switch fieldType {
case model.FieldTypeContact:
uri := fmt.Sprintf("https://%s/rest/crm.contact.userfield.get?ID=%d", domain, fieldID)
agent := b.fiberClient.Post(uri)
agent.Set("Content-Type", "application/json")
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err := range errs {
b.logger.Error("error sending request in GetCustomFieldByID", zap.Error(err))
}
return nil, fmt.Errorf("request GetCustomFieldByID failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("error GetCustomFieldByID contact statusCode - %d, respBody - %s", statusCode, string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return nil, fmt.Errorf(errorMessage)
}
err = json.Unmarshal(resBody, &resp)
if err != nil {
b.logger.Error("error unmarshal GetCustomFieldByID", zap.Error(err))
return nil, err
}
return &resp, nil
case model.FieldTypeCompany:
uri := fmt.Sprintf("https://%s/rest/crm.company.userfield.get?ID=%d", domain, fieldID)
agent := b.fiberClient.Post(uri)
agent.Set("Content-Type", "application/json")
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err := range errs {
b.logger.Error("error sending request in GetCustomFieldByID", zap.Error(err))
}
return nil, fmt.Errorf("request GetCustomFieldByID failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("error GetCustomFieldByID company statusCode - %d, respBody - %s", statusCode, string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return nil, fmt.Errorf(errorMessage)
}
err = json.Unmarshal(resBody, &resp)
if err != nil {
b.logger.Error("error unmarshal GetCustomFieldByID", zap.Error(err))
return nil, err
}
return &resp, nil
case model.FieldTypeDeal:
uri := fmt.Sprintf("https://%s/rest/crm.deal.userfield.get?ID=%d", domain, fieldID)
agent := b.fiberClient.Post(uri)
agent.Set("Content-Type", "application/json")
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err := range errs {
b.logger.Error("error sending request in GetCustomFieldByID", zap.Error(err))
}
return nil, fmt.Errorf("request GetCustomFieldByID failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("error GetCustomFieldByID deal statusCode - %d, respBody - %s", statusCode, string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return nil, fmt.Errorf(errorMessage)
}
err = json.Unmarshal(resBody, &resp)
if err != nil {
b.logger.Error("error unmarshal GetCustomFieldByID", zap.Error(err))
return nil, err
}
return &resp, nil
case model.FieldTypeLead:
uri := fmt.Sprintf("https://%s/rest/crm.lead.userfield.get?ID=%d", domain, fieldID)
agent := b.fiberClient.Post(uri)
agent.Set("Content-Type", "application/json")
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err := range errs {
b.logger.Error("error sending request in GetCustomFieldByID", zap.Error(err))
}
return nil, fmt.Errorf("request GetCustomFieldByID failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("error GetCustomFieldByID lead statusCode - %d, respBody - %s", statusCode, string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return nil, fmt.Errorf(errorMessage)
}
err = json.Unmarshal(resBody, &resp)
if err != nil {
b.logger.Error("error unmarshal AddFields", zap.Error(err))
return nil, err
}
return &resp, nil
}
}
time.Sleep(b.rateLimiter.Interval)
}
}
func (b *Bitrix) DownLoadFile(urlFile string) (string, string, error) {
var err error
agent := b.fiberClient.Get(urlFile)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err = range errs {
b.logger.Error("error sending request for getting file by url", zap.Error(err))
}
return "", "", fmt.Errorf("request failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("received an incorrect response from getting file by url: %s", string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return "", "", fmt.Errorf(errorMessage)
}
fileName := strings.Split(urlFile, "/")
resBodyBase64 := base64.StdEncoding.EncodeToString(resBody)
return fileName[len(fileName)-1], resBodyBase64, nil
}
// scope crm do not have privileges
func (b *Bitrix) AddedTagCustomField(accessToken string, domain string) error {
for {
if b.rateLimiter.Check() {
uri := fmt.Sprintf("https://%s/rest/userfieldtype.add", domain)
req := models.UserFieldTypeAddReq{
UserTypeID: string(model.PenaTagCustomFieldsType),
Handler: "https://b24-s5jg6c.bitrix24.ru", // todo заменить на что то реальное
Title: "Кастомный тип пользовательского поля Pena",
Description: "Кастомный тип пользовательского поля Pena",
}
bodyBytes, err := json.Marshal(req)
if err != nil {
b.logger.Error("error marshalling body", zap.Error(err))
return err
}
agent := b.fiberClient.Post(uri)
agent.Set("Content-Type", "application/json").Body(bodyBytes)
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err := range errs {
b.logger.Error("error sending request for AddedTagCustomField", zap.Error(err))
}
return fmt.Errorf("request failed: %v", errs[0])
}
fmt.Println(string(resBody))
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("received an incorrect response from AddedTagCustomField: %s", string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return fmt.Errorf("request failed: %v", errs[0])
}
return nil
}
time.Sleep(b.rateLimiter.Interval)
}
}
func (b *Bitrix) CheckScope(token, domain string) (any, error) {
for {
if b.rateLimiter.Check() {
uri := fmt.Sprintf("https://%s/rest/scope?auth=%s", domain, token)
agent := b.fiberClient.Get(uri)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err := range errs {
b.logger.Error("error check scope", zap.Error(err))
}
return 0, fmt.Errorf("request failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("received an incorrect response from check scope: %s", string(resBody))
b.logger.Error(errorMessage, zap.Int("status", statusCode))
return 0, fmt.Errorf(errorMessage)
}
return string(resBody), nil
}
time.Sleep(b.rateLimiter.Interval)
}
}

@ -0,0 +1,280 @@
package bitrixClient
import (
"context"
"fmt"
"go.uber.org/zap"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/models"
"penahub.gitlab.yandexcloud.net/backend/quiz/bitrix/internal/workers/limiter"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"testing"
"time"
)
func TestGetListFields(t *testing.T) {
ctx := context.Background()
lim := limiter.NewRateLimiter(ctx, 50, 2*time.Second)
logger := zap.NewNop()
b := NewBitrixClient(BitrixDeps{
Logger: logger,
RedirectionURL: "test",
IntegrationID: "test",
IntegrationSecret: "test",
RateLimiter: lim,
})
//result, err := b.GetUserList("262df9660000071b00717f9200000001000007c9148fd5a4211fc98142ea9bc41fc8d3", "b24-ld76ub.bitrix24.ru")
//if err != nil {
// t.Fatal(err)
//}
//fmt.Println(result)
//arr := []model.FieldsType{model.FieldTypeLead, model.FieldTypeCompany, model.FieldTypeContact, model.FieldTypeDeal}
//
//for i, tipe := range arr {
// req := models.AddFields{
// EditFormLabel: fmt.Sprintf("EditFormLabel %d", i),
// ListColumnLabel: fmt.Sprintf("ListColumnLabel %d", i),
// UserTypeID: "string",
// Settings: map[string]interface{}{
// "DEFAULT_VALUE": "GOGOGOGOGOGOGO!",
// }}
// req.GenFieldName()
// result, err := b.AddFields(req, tipe, "9c7cf1660000071b00717f9200000001000007b3c27dd12d61d2e90dd1e630638b8346", "b24-ld76ub.bitrix24.ru")
// if err != nil {
// fmt.Println(err)
// }
// fmt.Println(result)
//}
//
fieldAnswer := make(map[string]string)
fieldAnswer["UF_CRM_1729778229491"] = "хуккккк"
//
//createContactReq := models.CreateContactReq{
// Fields: models.ContactFields{
// Name: "Контакт фром апи",
// SecondName: "SecondName",
// LastName: "LastName",
// Opened: "Y",
// LeadID: 1,
// UtmSource: "UtmSource",
// UtmMedium: "UtmMedium",
// UtmCampaign: "UtmCampaign",
// UtmContent: "UtmContent",
// UtmTerm: "UtmTerm",
// },
//}
//
//reqMap := models.FormattingToMap(&createContactReq, fieldAnswer)
//
//contactResult, err := b.CreateContact(reqMap, "ad5df5660000071b00717f920000000100000783b6e19c671e64c3655e5c7aff197e14", "b24-ld76ub.bitrix24.ru")
//if err != nil {
// fmt.Println(err)
//}
//
//createCompanyReq := models.CompanyReq{
// Fields: models.CompanyFields{
// Title: "TEST FORMATTER",
// Opened: "Y",
// LeadID: 1,
// UtmSource: "UtmSource",
// UtmMedium: "UtmMedium",
// UtmCampaign: "UtmCampaign",
// UtmContent: "UtmContent",
// UtmTerm: "UtmTerm",
// ContactID: contactResult,
// },
//}
//
//reqMap = models.FormattingToMap(&createCompanyReq, fieldAnswer)
//companyResult, err := b.CreateCompany(reqMap, "ad5df5660000071b00717f920000000100000783b6e19c671e64c3655e5c7aff197e14", "b24-ld76ub.bitrix24.ru")
//if err != nil {
// fmt.Println(err)
//}
//
createDealReq := models.CreatingDealReq{
Fields: models.CreateDealFields{
Title: "ТЕСТ ОТ ГОУ АПИ 10/10",
TypeID: "SALE",
StageID: "NEW",
CompanyID: 383,
ContactIDs: []int32{235},
Opened: "Y",
AssignedByID: 1,
CategoryID: 1,
SourceID: "CALL",
UtmSource: "UtmSource",
UtmMedium: "UtmMedium",
UtmCampaign: "UtmCampaign",
UtmContent: "UtmContent",
UtmTerm: "UtmTerm",
},
}
//
reqMap := models.FormattingToMap(&createDealReq, fieldAnswer)
fmt.Println(reqMap)
result, err := b.CreatingDeal(reqMap, "e16b1a670000071b007254120000000100000759485bd49a2206c79f84e8a150522f43", "b24-s5jg6c.bitrix24.ru")
if err != nil {
fmt.Println(err)
}
fmt.Println(result)
//
//for _, tipe := range model.CategoryArr {
// result, err := b.GetListPipelines(tipe, "9d5bf9660000071b00717f9200000001000007b8da5b64a2142c5a0abcfb3e65f89b0c", "b24-ld76ub.bitrix24.ru")
// if err != nil {
// fmt.Println(err)
// }
//
// r, _ := json.Marshal(result)
// fmt.Println(string(r))
//}
//
//for _, tipe := range arr {
// result, err := b.GetListFields(tipe, "07cff6660000071b00717f92000000010000079e5af88b052dbcfe9e9d98cac38710ad", "b24-ld76ub.bitrix24.ru")
// if err != nil {
// fmt.Println(err)
// }
//
// r, _ := json.Marshal(result)
// fmt.Println(string(r))
// fmt.Println(tipe)
//}
//arr2 := []model.TypeStepsEntityID{model.StatusStepsEntityID, model.DealTypeStepsEntityID, model.DealStageStepsEntityID, model.SourceStepsEntityID, model.ContactTypeStepsEntityID, model.CompanyTypeStepsEntityID, model.EmployeesStepsEntityID, model.IndustryStepsEntityID, model.SmartInvoiceStageStepsEntityID, model.QuoteStatusStepsEntityID, model.HonorificStepsEntityID, model.CallListStepsEntityID, model.SmartDocumentStageStepsEntityID}
//for _, stepType := range arr2 {
//result, err := b.GetListSteps("9d5bf9660000071b00717f9200000001000007b8da5b64a2142c5a0abcfb3e65f89b0c", "b24-ld76ub.bitrix24.ru")
//if err != nil {
// fmt.Println(err)
//}
//for _, i := range result.Result {
// fmt.Println(i.ID)
//}
//}
//
//"CATEGORY_ID":"1"
}
func Test_Auth(t *testing.T) {
ctx := context.Background()
lim := limiter.NewRateLimiter(ctx, 50, 2*time.Second)
logger := zap.NewNop()
b := NewBitrixClient(BitrixDeps{
Logger: logger,
RedirectionURL: "https://squiz.pena.digital/integrations",
IntegrationID: "app.670bd825e44c52.61826940",
IntegrationSecret: "Ki0MElZXS6dE6tRsGxixri2jmxbxF2Xa4qQpBPziGdAvvLAHJx",
RateLimiter: lim,
})
tokens, err := b.CreateWebHook(&models.CreateWebHookReq{
GrantType: "authorization_code",
Code: "50cb1067007232200072541200000001000007aab0e419e6de4ebff7dd4b238c144bae",
}, true)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(tokens)
}
func Test_GetListUsers(t *testing.T) {
ctx := context.Background()
lim := limiter.NewRateLimiter(ctx, 50, 2*time.Second)
logger := zap.NewNop()
b := NewBitrixClient(BitrixDeps{
Logger: logger,
RedirectionURL: "https://squiz.pena.digital/integrations",
IntegrationID: "app.670bd825e44c52.61826940",
IntegrationSecret: "Ki0MElZXS6dE6tRsGxixri2jmxbxF2Xa4qQpBPziGdAvvLAHJx",
RateLimiter: lim,
})
r, err := b.GetUserList("664e12670000071b00725412000000010000071da5594e25e0731541e2bd2ea0d78a7b", "b24-s5jg6c.bitrix24.ru")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(r)
}
func Test_Add_Fields(t *testing.T) {
ctx := context.Background()
lim := limiter.NewRateLimiter(ctx, 50, 2*time.Second)
logger := zap.NewNop()
b := NewBitrixClient(BitrixDeps{
Logger: logger,
RedirectionURL: "https://squiz.pena.digital/integrations",
IntegrationID: "app.670bd825e44c52.61826940",
IntegrationSecret: "Ki0MElZXS6dE6tRsGxixri2jmxbxF2Xa4qQpBPziGdAvvLAHJx",
RateLimiter: lim,
})
req := models.AddFields{
FieldName: "Test_Add_Fields_Deal",
EditFormLabel: "Test_Add_Fields_Deal",
ListColumnLabel: "Test_Add_Fields_Deal",
UserTypeID: model.StringCustomFieldsType,
}
req.GenFieldName()
resp, err := b.AddFields(req, model.FieldTypeDeal, "9afc14670072322000725412000000010000074c130dd22fbea5321c68aebd7e4e6b98", "b24-s5jg6c.bitrix24.ru")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(resp)
}
func Test_Update_Contact(t *testing.T) {
ctx := context.Background()
lim := limiter.NewRateLimiter(ctx, 50, 2*time.Second)
logger := zap.NewNop()
b := NewBitrixClient(BitrixDeps{
Logger: logger,
RedirectionURL: "https://squiz.pena.digital/integrations",
IntegrationID: "app.670bd825e44c52.61826940",
IntegrationSecret: "Ki0MElZXS6dE6tRsGxixri2jmxbxF2Xa4qQpBPziGdAvvLAHJx",
RateLimiter: lim,
})
contactFields := make(map[string]string)
contactFields["UF_CRM_1729426581"] = "phoneTESTUPDATE"
contactFields["UF_CRM_1729425300"] = "emailTESTUPDATE"
contactFields["UF_CRM_1729425184"] = "nameTESTUPDATE"
reqContact := models.CreateContactReq{
Fields: models.ContactFields{
Name: "TEST UPDATE CONTACT",
},
}
reqMapContact := models.FormattingToMap(&reqContact, contactFields)
err := b.UpdateContact(reqMapContact, "6b2c16670000071b0072541200000001000007b21dd57044f7de29fde6a9566e6932f1", "b24-s5jg6c.bitrix24.ru", 53)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("GOOD")
}
func Test_AddedTagCustomField(t *testing.T) {
ctx := context.Background()
lim := limiter.NewRateLimiter(ctx, 50, 2*time.Second)
logger := zap.NewNop()
b := NewBitrixClient(BitrixDeps{
Logger: logger,
RedirectionURL: "https://squiz.pena.digital/integrations",
IntegrationID: "app.670bd825e44c52.61826940",
IntegrationSecret: "Ki0MElZXS6dE6tRsGxixri2jmxbxF2Xa4qQpBPziGdAvvLAHJx",
RateLimiter: lim,
})
err := b.AddedTagCustomField("e16b1a670000071b007254120000000100000759485bd49a2206c79f84e8a150522f43", "b24-s5jg6c.bitrix24.ru")
if err != nil {
fmt.Println(err)
}
fmt.Println("GOOD")
}

37
pkg/closer/closer.go Normal file

@ -0,0 +1,37 @@
package closer
import (
"context"
)
type Closer interface {
Close(ctx context.Context) error
}
type CloserFunc func(ctx context.Context) error
func (cf CloserFunc) Close(ctx context.Context) error {
return cf(ctx)
}
type CloserGroup struct {
closers []Closer
}
func NewCloserGroup() *CloserGroup {
return &CloserGroup{}
}
func (cg *CloserGroup) Add(c Closer) {
cg.closers = append(cg.closers, c)
}
func (cg *CloserGroup) Call(ctx context.Context) error {
var closeErr error
for i := len(cg.closers) - 1; i >= 0; i-- {
if err := cg.closers[i].Close(ctx); err != nil && closeErr == nil {
closeErr = err
}
}
return closeErr
}