1
This commit is contained in:
34
Dockerfile
Normal file
34
Dockerfile
Normal file
@@ -0,0 +1,34 @@
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
ARG BUILD_LDFLAGS
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download -x
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="${BUILD_LDFLAGS}" -trimpath -o main ./cmd/main.go
|
||||
|
||||
FROM alpine:3.18 AS certs
|
||||
RUN wget -q "https://storage.yandexcloud.net/cloud-certs/CA.pem" -O /YandexInternalRootCA.crt \
|
||||
&& chmod 0655 /YandexInternalRootCA.crt
|
||||
|
||||
FROM alpine:3.18
|
||||
WORKDIR /
|
||||
ARG CONFIG_FILE_PATH
|
||||
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata
|
||||
|
||||
COPY --from=certs /YandexInternalRootCA.crt /root/.redis/YandexInternalRootCA.crt
|
||||
COPY --from=certs /YandexInternalRootCA.crt /root/.postgres/YandexInternalRootCA.crt
|
||||
COPY ${CONFIG_FILE_PATH} /config.yaml
|
||||
|
||||
COPY --from=builder /app/main /main
|
||||
|
||||
RUN adduser -D -u 10001 appuser && chown appuser /main
|
||||
USER appuser
|
||||
|
||||
ENTRYPOINT ["/main"]
|
||||
CMD ["--config_path", "/config.yaml"]
|
||||
82
Makefile
Normal file
82
Makefile
Normal file
@@ -0,0 +1,82 @@
|
||||
BIN_DIR := $(CURDIR)/bin
|
||||
APP_NAME ?= molva-app-backend
|
||||
VERSION ?= $(shell git describe --tags 2>/dev/null || echo "v0.0.0")
|
||||
BUILD_DATE := $(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
GIT_COMMIT := $(shell git rev-parse HEAD 2>/dev/null)
|
||||
LDFLAGS := "-s -w -X main.version=$(VERSION) -X main.commit=$(GIT_COMMIT) -X main.date=$(BUILD_DATE)"
|
||||
GOLANGCI_LINT_VERSION := v1.62.2
|
||||
ENV := "local"
|
||||
UNAME_S := $(shell uname -s)
|
||||
CONFIG_PATH := .build/config/
|
||||
|
||||
ifeq ($(OS),Windows_NT)
|
||||
CONFIG_PATH := $(subst /,\,$(CONFIG_PATH))
|
||||
endif
|
||||
|
||||
.PHONY: all
|
||||
all: lint build
|
||||
|
||||
## -- Dependency Management --
|
||||
.PHONY: install-tools
|
||||
install-tools: $(BIN_DIR)/golangci-lint
|
||||
|
||||
.PHONY: $(BIN_DIR)/golangci-lint
|
||||
$(BIN_DIR)/golangci-lint:
|
||||
@mkdir -p $(@D)
|
||||
@echo "Installing golangci-lint $(GOLANGCI_LINT_VERSION)..."
|
||||
@GOBIN=$(BIN_DIR) go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
|
||||
|
||||
## -- Linting --
|
||||
.PHONY: lint
|
||||
lint: $(BIN_DIR)/golangci-lint
|
||||
@echo "Running linter..."
|
||||
@$(BIN_DIR)/golangci-lint run --config .golangci-lint.yaml --fix=false --color=always
|
||||
|
||||
## -- Run --
|
||||
## To change env use: make run CONFIG_PATH={config file dir path} ENV={env}
|
||||
.PHONY: run
|
||||
run:
|
||||
@echo "Starting $(APP_NAME)..."
|
||||
@echo CGO_ENABLED=0 go run -ldflags=$(LDFLAGS) ./cmd/main.go --config_path=$(CONFIG_PATH) --env=$(ENV)
|
||||
@CGO_ENABLED=0 go run -ldflags=$(LDFLAGS) ./cmd/main.go --config_path=$(CONFIG_PATH) --env=$(ENV)
|
||||
|
||||
## -- Building --
|
||||
.PHONY: build
|
||||
build: build-linux build-darwin build-windows
|
||||
|
||||
# Build for Linux
|
||||
.PHONY: build-linux
|
||||
build-linux:
|
||||
@echo "Building for Linux..."
|
||||
@GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags=$(LDFLAGS) -o $(BIN_DIR)/$(APP_NAME)_linux_amd64 ./cmd/main.go
|
||||
@GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags=$(LDFLAGS) -o $(BIN_DIR)/$(APP_NAME)_linux_arm64 ./cmd/main.go
|
||||
|
||||
# Build for macOS (Darwin)
|
||||
.PHONY: build-darwin
|
||||
build-darwin:
|
||||
@echo "Building for macOS..."
|
||||
@GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -ldflags=$(LDFLAGS) -o $(BIN_DIR)/$(APP_NAME)_darwin_amd64 ./cmd/main.go
|
||||
@GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -ldflags=$(LDFLAGS) -o $(BIN_DIR)/$(APP_NAME)_darwin_arm64 ./cmd/main.go
|
||||
|
||||
# Build for Windows
|
||||
.PHONY: build-windows
|
||||
build-windows:
|
||||
@echo "Building for Windows..."
|
||||
@GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags=$(LDFLAGS) -o $(BIN_DIR)/$(APP_NAME)_windows_amd64.exe ./cmd/main.go
|
||||
@GOOS=windows GOARCH=arm64 CGO_ENABLED=0 go build -ldflags=$(LDFLAGS) -o $(BIN_DIR)/$(APP_NAME)_windows_arm64.exe ./cmd/main.go
|
||||
|
||||
## -- Docker --
|
||||
.PHONY: docker-build
|
||||
docker-build:
|
||||
@echo "Building Docker image..."
|
||||
@docker build \
|
||||
--build-arg BUILD_LDFLAGS=$(LDFLAGS) \
|
||||
--build-arg CONFIG_FILE_PATH="$(CONFIG_PATH)$(ENV).yaml" \
|
||||
-t $(APP_NAME):$(VERSION) \
|
||||
.
|
||||
|
||||
## -- Cleanup --
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@echo "Cleaning..."
|
||||
@rm -rf $(BIN_DIR)
|
||||
169
README.md
Normal file
169
README.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# Molva Backend
|
||||
|
||||
Backend ัะตัะฒะธั ะดะปั ะฟะปะฐััะพัะผั Molva, ะฝะฐะฟะธัะฐะฝะฝัะน ะฝะฐ Go.
|
||||
|
||||
## ะขัะตะฑะพะฒะฐะฝะธั
|
||||
|
||||
- Go 1.24+
|
||||
- Docker (ะพะฟัะธะพะฝะฐะปัะฝะพ)
|
||||
- Git
|
||||
|
||||
## ะฃััะฐะฝะพะฒะบะฐ ะธ ะฝะฐัััะพะนะบะฐ
|
||||
|
||||
### 1. ะะปะพะฝะธัะพะฒะฐะฝะธะต ัะตะฟะพะทะธัะพัะธั
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd molva-backend
|
||||
```
|
||||
|
||||
### 2. ะฃััะฐะฝะพะฒะบะฐ ะทะฐะฒะธัะธะผะพััะตะน
|
||||
|
||||
```bash
|
||||
go mod download
|
||||
```
|
||||
|
||||
### 3. ะฃััะฐะฝะพะฒะบะฐ ะธะฝััััะผะตะฝัะพะฒ ัะฐะทัะฐะฑะพัะบะธ
|
||||
|
||||
```bash
|
||||
make install-tools
|
||||
```
|
||||
|
||||
## ะะฐะฟััะบ ะฟัะพะตะบัะฐ
|
||||
|
||||
### ะะพะบะฐะปัะฝะฐั ัะฐะทัะฐะฑะพัะบะฐ
|
||||
|
||||
ะะปั ะทะฐะฟััะบะฐ ะฒ ัะตะถะธะผะต ัะฐะทัะฐะฑะพัะบะธ:
|
||||
|
||||
```bash
|
||||
make run
|
||||
```
|
||||
|
||||
ะะพ ัะผะพะปัะฐะฝะธั ะฟัะพะตะบั ะทะฐะฟััะบะฐะตััั ั ะบะพะฝัะธะณััะฐัะธะตะน:
|
||||
- **ะะพะฝัะธะณััะฐัะธั**: `.build/config/`
|
||||
- **ะะบััะถะตะฝะธะต**: `local`
|
||||
|
||||
### ะะทะผะตะฝะตะฝะธะต ะฟะฐัะฐะผะตััะพะฒ ะทะฐะฟััะบะฐ
|
||||
|
||||
ะั ะผะพะถะตัะต ะธะทะผะตะฝะธัั ะฟะฐัะฐะผะตััั ะทะฐะฟััะบะฐ:
|
||||
|
||||
```bash
|
||||
# ะะทะผะตะฝะตะฝะธะต ะฟััะธ ะบ ะบะพะฝัะธะณััะฐัะธะธ
|
||||
make run CONFIG_PATH=...
|
||||
|
||||
# ะะทะผะตะฝะตะฝะธะต ะพะบััะถะตะฝะธั
|
||||
make run ENV=...
|
||||
|
||||
# ะะทะผะตะฝะตะฝะธะต ะพะฑะพะธั
ะฟะฐัะฐะผะตััะพะฒ
|
||||
make run CONFIG_PATH=... ENV=...
|
||||
```
|
||||
|
||||
### ะกะฑะพัะบะฐ ะฟัะพะตะบัะฐ
|
||||
|
||||
#### ะกะฑะพัะบะฐ ะดะปั ะฒัะตั
ะฟะปะฐััะพัะผ
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
ะญัะฐ ะบะพะผะฐะฝะดะฐ ัะพะทะดะฐัั ะธัะฟะพะปะฝัะตะผัะต ัะฐะนะปั ะดะปั:
|
||||
- Linux (amd64, arm64)
|
||||
- macOS (amd64, arm64)
|
||||
- Windows (amd64, arm64)
|
||||
|
||||
ะคะฐะนะปั ะฑัะดัั ัะพั
ัะฐะฝะตะฝั ะฒ ะดะธัะตะบัะพัะธะธ `bin/` ั ะธะผะตะฝะฐะผะธ:
|
||||
- `molva-app-backend_linux_amd64`
|
||||
- `molva-app-backend_linux_arm64`
|
||||
- `molva-app-backend_darwin_amd64`
|
||||
- `molva-app-backend_darwin_arm64`
|
||||
- `molva-app-backend_windows_amd64.exe`
|
||||
- `molva-app-backend_windows_arm64.exe`
|
||||
|
||||
#### ะกะฑะพัะบะฐ ะดะปั ะบะพะฝะบัะตัะฝะพะน ะฟะปะฐััะพัะผั
|
||||
|
||||
```bash
|
||||
# ะขะพะปัะบะพ ะดะปั Linux
|
||||
make build-linux
|
||||
|
||||
# ะขะพะปัะบะพ ะดะปั macOS
|
||||
make build-darwin
|
||||
|
||||
# ะขะพะปัะบะพ ะดะปั Windows
|
||||
make build-windows
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
ะะปั ัะฑะพัะบะธ Docker ะพะฑัะฐะทะฐ:
|
||||
|
||||
```bash
|
||||
make docker-build
|
||||
```
|
||||
|
||||
ะะฑัะฐะท ะฑัะดะตั ัะพะทะดะฐะฝ ั ัะตะณะพะผ `molva-app-backend:$(VERSION)`, ะณะดะต `VERSION` - ััะพ git tag ะธะปะธ "v0.0.0" ะฟะพ ัะผะพะปัะฐะฝะธั.
|
||||
|
||||
## ะ ะฐะทัะฐะฑะพัะบะฐ
|
||||
|
||||
### ะัะพะฒะตัะบะฐ ะบะพะดะฐ
|
||||
|
||||
ะะตัะตะด ะบะพะผะผะธัะพะผ ัะตะบะพะผะตะฝะดัะตััั ะทะฐะฟัััะธัั ะปะธะฝัะตั:
|
||||
|
||||
```bash
|
||||
make lint
|
||||
```
|
||||
|
||||
### ะัะธััะบะฐ
|
||||
|
||||
ะะปั ัะดะฐะปะตะฝะธั ัะบะพะผะฟะธะปะธัะพะฒะฐะฝะฝัั
ัะฐะนะปะพะฒ:
|
||||
|
||||
```bash
|
||||
make clean
|
||||
```
|
||||
|
||||
## ะกัััะบัััะฐ ะฟัะพะตะบัะฐ
|
||||
|
||||
```
|
||||
molva-backend/
|
||||
โโโ bin/ # ะกะบะพะผะฟะธะปะธัะพะฒะฐะฝะฝัะต ัะฐะนะปั
|
||||
โโโ cmd/
|
||||
โ โโโ main.go # ะขะพัะบะฐ ะฒั
ะพะดะฐ ะฟัะธะปะพะถะตะฝะธั
|
||||
โโโ internal/ # ะะฝัััะตะฝะฝะธะต ะฟะฐะบะตัั
|
||||
โ โโโ auth/ # ะััะตะฝัะธัะธะบะฐัะธั
|
||||
โ โโโ broker/ # Message broker
|
||||
โ โโโ cache/ # ะััะธัะพะฒะฐะฝะธะต
|
||||
โ โโโ config/ # ะะพะฝัะธะณััะฐัะธั
|
||||
โ โโโ database/ # ะ ะฐะฑะพัะฐ ั ะะ
|
||||
โ โโโ feed/ # ะะตะฝัะฐ
|
||||
โ โโโ file_manager/ # ะฃะฟัะฐะฒะปะตะฝะธะต ัะฐะนะปะฐะผะธ
|
||||
โ โโโ form_generator/ # ะะตะฝะตัะฐัะพั ัะพัะผ
|
||||
โ โโโ http/ # HTTP handlers
|
||||
โ โโโ integration/ # ะะฝัะตะณัะฐัะธะธ
|
||||
โ โโโ logger/ # ะะพะณะธัะพะฒะฐะฝะธะต
|
||||
โ โโโ notifications/ # ะฃะฒะตะดะพะผะปะตะฝะธั
|
||||
โ โโโ object_storage/ # ะะฑัะตะบัะฝะพะต ั
ัะฐะฝะธะปะธัะต
|
||||
โ โโโ request_model/ # ะะพะดะตะปะธ ะทะฐะฟัะพัะพะฒ
|
||||
โ โโโ services/ # ะกะตัะฒะธัั
|
||||
โ โโโ types/ # ะขะธะฟั ะดะฐะฝะฝัั
|
||||
โ โโโ url_shortener/ # ะกะพะบัะฐัะตะฝะธะต URL
|
||||
โโโ Dockerfile
|
||||
โโโ go.mod
|
||||
โโโ Makefile
|
||||
โโโ README.md
|
||||
```
|
||||
|
||||
## ะะพะฝัะธะณััะฐัะธั
|
||||
|
||||
ะัะพะตะบั ะธัะฟะพะปัะทัะตั ะบะพะฝัะธะณััะฐัะธะพะฝะฝัะต ัะฐะนะปั ะฒ ัะพัะผะฐัะต YAML. ะะพ ัะผะพะปัะฐะฝะธั ะพะฝะธ ะฝะฐั
ะพะดัััั ะฒ ะดะธัะตะบัะพัะธะธ `.build/config/`.
|
||||
|
||||
ะกัััะบัััะฐ ะบะพะฝัะธะณััะฐัะธะธ:
|
||||
- `local.yaml` - ะดะปั ะปะพะบะฐะปัะฝะพะน ัะฐะทัะฐะฑะพัะบะธ
|
||||
- `staging.yaml` - ะดะปั dev ะพะบััะถะตะฝะธั
|
||||
- `production.yaml` - ะดะปั production ะพะบััะถะตะฝะธั
|
||||
|
||||
## ะะตัะตะผะตะฝะฝัะต ัะฑะพัะบะธ
|
||||
|
||||
ะัะธะปะพะถะตะฝะธะต ะฒะบะปััะฐะตั ัะปะตะดัััะธะต ะฟะตัะตะผะตะฝะฝัะต ัะฑะพัะบะธ:
|
||||
- `version` - ะฒะตััะธั ะฟัะธะปะพะถะตะฝะธั (git tag)
|
||||
- `commit` - ั
ะตั ะบะพะผะผะธัะฐ
|
||||
- `date` - ะดะฐัะฐ ัะฑะพัะบะธ
|
||||
|
||||
5
build_push.sh
Executable file
5
build_push.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
echo "714dadb9-e8ad-468c-a77f-e98d86cd7393" | docker login -u "molvaapp" --password-stdin
|
||||
IMAGE_NAME="molvaapp/api_gateway"
|
||||
TAG="latest"
|
||||
docker build -t $IMAGE_NAME:$TAG .
|
||||
docker push $IMAGE_NAME:$TAG
|
||||
126
cmd/main.go
Normal file
126
cmd/main.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/logger"
|
||||
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/constants"
|
||||
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/broker"
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/config"
|
||||
httprouter "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/http"
|
||||
)
|
||||
|
||||
var (
|
||||
version string
|
||||
commit string
|
||||
date string
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.MustLoad(version, commit, date)
|
||||
|
||||
log, err := logger.New(cfg.Env)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log.Info("Starting App", slog.String("version", version), slog.String("commit", commit), slog.String("build_date", date))
|
||||
|
||||
brok := setupBroker(log, &cfg.Broker)
|
||||
defer func(brok *broker.RabbitMQBroker) {
|
||||
if err := brok.Disconnect(); err != nil {
|
||||
log.Error("Error disconnecting from broker")
|
||||
}
|
||||
}(brok)
|
||||
|
||||
server, err := setupServer(log, cfg)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("can not start http server: %v", err))
|
||||
}
|
||||
|
||||
serverErrors := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
log.Info("starting server", slog.Int("port", cfg.Port))
|
||||
serverErrors <- server.ListenAndServe()
|
||||
}()
|
||||
|
||||
shutdownSignal := make(chan os.Signal, 1)
|
||||
signal.Notify(shutdownSignal, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
select {
|
||||
case err := <-serverErrors:
|
||||
log.Error("Error starting server: %v\n", slog.Any("err", err))
|
||||
case sig := <-shutdownSignal:
|
||||
log.Info("received signal. shutting down server", slog.String("signal", sig.String()))
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
log.Info("shutting down server...")
|
||||
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
log.Error(fmt.Sprintf("Error shutting down server: %v\n", err))
|
||||
}
|
||||
|
||||
log.Info("server gracefully stopped")
|
||||
}
|
||||
|
||||
func setupServer(logger *slog.Logger, cfg *config.Config) (*http.Server, error) {
|
||||
router, err := httprouter.New(logger, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httprouter.SetupRouter(router, cfg.Build)
|
||||
|
||||
return &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
|
||||
Handler: router.Mux,
|
||||
ReadHeaderTimeout: 15 * time.Second,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func doWithRetry(function func() error, serviceName string, logger *slog.Logger) {
|
||||
if err := function(); err != nil {
|
||||
logger.Error(fmt.Sprintf("Error connecting to %s: %v", serviceName, err),
|
||||
slog.String("error", err.Error()))
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(constants.DefaultRetryTimeout)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
if err := function(); err != nil {
|
||||
logger.Error(fmt.Sprintf("Error connecting to %s: %v", serviceName, err),
|
||||
slog.String("error", err.Error()))
|
||||
} else {
|
||||
logger.Info(fmt.Sprintf("Successfully connected to %s service", serviceName))
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
logger.Info(fmt.Sprintf("Successfully connected to %s service", serviceName))
|
||||
}
|
||||
}
|
||||
|
||||
func setupBroker(logger *slog.Logger, cfg *config.Broker) *broker.RabbitMQBroker {
|
||||
brok := broker.NewRabbitMQBroker(cfg)
|
||||
|
||||
doWithRetry(func() error {
|
||||
return brok.Connect()
|
||||
}, "Broker", logger)
|
||||
|
||||
return brok
|
||||
}
|
||||
53
docker-compose.staging.yaml
Normal file
53
docker-compose.staging.yaml
Normal file
@@ -0,0 +1,53 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
molva-api-gateway:
|
||||
image: molvaapp/api_gateway:${VERSION:-latest}
|
||||
container_name: molva-api-gateway
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8001:8000"
|
||||
volumes:
|
||||
- /opt/molva:/opt/molva
|
||||
- /opt/molva/object_storage/credentials:/root/.aws/credentials:ro
|
||||
- /opt/molva/object_storage/config:/root/.aws/config:ro
|
||||
networks:
|
||||
- molva
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/healthcheck"]
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 3
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 20s
|
||||
window: 120s
|
||||
update_config:
|
||||
parallelism: 1
|
||||
failure_action: rollback
|
||||
monitor: 60s
|
||||
max_failure_ratio: 0.3
|
||||
order: start-first
|
||||
rollback_config:
|
||||
parallelism: 0
|
||||
failure_action: continue
|
||||
order: start-first
|
||||
placement:
|
||||
constraints:
|
||||
- node.labels.env == stg
|
||||
- node.labels.app == true
|
||||
preferences:
|
||||
- spread: node.labels.TZ
|
||||
|
||||
molva-notifications:
|
||||
image: molvaapp/notification:latest
|
||||
container_name: molva-notifications
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /opt/molva:/opt/molva
|
||||
networks:
|
||||
- molva
|
||||
|
||||
networks:
|
||||
molva:
|
||||
external: true
|
||||
62
go.mod
Normal file
62
go.mod
Normal file
@@ -0,0 +1,62 @@
|
||||
module git-molva.ru/Molva/molva-backend/services/api_gateway
|
||||
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
github.com/AlexOreL-272/ProtoMolva v1.32.5
|
||||
github.com/Masterminds/squirrel v1.5.4
|
||||
github.com/Nerzal/gocloak/v13 v13.9.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.14
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.79.4
|
||||
github.com/aws/smithy-go v1.22.3
|
||||
github.com/go-redis/redis/v7 v7.4.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0
|
||||
github.com/jackc/pgx/v5 v5.7.5
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/rabbitmq/amqp091-go v1.10.0
|
||||
google.golang.org/protobuf v1.36.6
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
|
||||
github.com/go-resty/resty/v2 v2.16.5 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/segmentio/ksuid v1.0.4 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/net v0.37.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
|
||||
google.golang.org/grpc v1.71.0 // indirect
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
|
||||
)
|
||||
186
go.sum
Normal file
186
go.sum
Normal file
@@ -0,0 +1,186 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/AlexOreL-272/ProtoMolva v1.32.5 h1:mQybFqLmwlW7DpSppiRsID11m6t8GGOOkiuzWtsjyT0=
|
||||
github.com/AlexOreL-272/ProtoMolva v1.32.5/go.mod h1:0lY7t+z3fhKs2ZxsdVvAePxAC4UdIRQT7LOejnJmb00=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
||||
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||
github.com/Nerzal/gocloak/v13 v13.9.0 h1:YWsJsdM5b0yhM2Ba3MLydiOlujkBry4TtdzfIzSVZhw=
|
||||
github.com/Nerzal/gocloak/v13 v13.9.0/go.mod h1:YYuDcXZ7K2zKECyVP7pPqjKxx2AzYSpKDj8d6GuyM10=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.2 h1:BCG7DCXEXpNCcpwCxg1oi9pkJWH2+eZzTn9MY56MbVw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.2/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.79.4 h1:4yxno6bNHkekkfqG/a1nz/gC2gBwhJSojV1+oTE7K+4=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.79.4/go.mod h1:qbn305Je/IofWBJ4bJz/Q7pDEtnnoInw/dGt71v6rHE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
|
||||
github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
|
||||
github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI=
|
||||
github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
|
||||
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
||||
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
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/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
||||
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||
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-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
|
||||
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
|
||||
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
|
||||
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
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.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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=
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=
|
||||
27
internal/auth/client.go
Normal file
27
internal/auth/client.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type Manager interface {
|
||||
RegisterUser(ctx context.Context, request RegisterUserRequest) (*RegisterUserResponse, error)
|
||||
LoginUser(ctx context.Context, request LoginUserRequest) (*LoginUserResponse, error)
|
||||
LogoutUser(ctx context.Context, request LogoutUserRequest) error
|
||||
GetNewAccessToken(ctx context.Context, request GetNewAccessTokenRequest) (*GetNewAccessTokenResponse, error)
|
||||
GetUserEmailVerificationToken(ctx context.Context, uid string) (*GetUserTokenResponse, error)
|
||||
CheckTokenIsValid(ctx context.Context, token string) (bool, error)
|
||||
GetPermissionsByUsersId(ctx context.Context, id string) (*GetPermissionsByUsersIdResponse, error)
|
||||
CheckPermissions(ctx context.Context, required CheckPermissionsRequest) (bool, error)
|
||||
GetUserInfo(ctx context.Context, id string) (*UserInfo, error)
|
||||
VerifyEmail(ctx context.Context, userID string, token string) error
|
||||
GetEmailVerificationStatus(ctx context.Context, userID string) (*GetEmailVerificationResponse, error)
|
||||
ResetPassword(ctx context.Context, request ResetPasswordRequest) error
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
AuthServerAddr string
|
||||
Realm string
|
||||
ClientId string
|
||||
ClientSecret string
|
||||
}
|
||||
104
internal/auth/domain.go
Normal file
104
internal/auth/domain.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package auth
|
||||
|
||||
type (
|
||||
User struct {
|
||||
Email string
|
||||
Password string
|
||||
FirstName string
|
||||
SecondName string
|
||||
Patronymic *string
|
||||
Number string
|
||||
Permissions map[string]string
|
||||
}
|
||||
|
||||
RegisterUserRequest struct {
|
||||
User User
|
||||
UserType int32
|
||||
}
|
||||
|
||||
RegisterUserResponse struct {
|
||||
UserId string
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
LoginUserRequest struct {
|
||||
Email string
|
||||
Password string
|
||||
}
|
||||
|
||||
LoginUserResponse struct {
|
||||
UserId string `json:"user_id"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
GetUserTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
LogoutUserRequest struct {
|
||||
RefreshToken string
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
GetNewAccessTokenRequest struct {
|
||||
RefreshToken string
|
||||
}
|
||||
|
||||
GetNewAccessTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
UserInfoFromToken struct {
|
||||
UserId string
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
GetPermissionsByUsersIdResponse struct {
|
||||
Balance string
|
||||
Company string
|
||||
Employees string
|
||||
Profile string
|
||||
Submissions string
|
||||
Vacancies string
|
||||
}
|
||||
)
|
||||
type (
|
||||
CheckPermissionsRequest struct {
|
||||
UserId string
|
||||
RequiredPermission string
|
||||
RequiredPermissionLevel string
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
ResetPasswordRequest struct {
|
||||
Email string
|
||||
NewPassword string
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
UserInfo struct {
|
||||
Email string
|
||||
FirstName string
|
||||
SecondName string
|
||||
Patronymic *string
|
||||
Permissions map[string]string
|
||||
UserType int32
|
||||
}
|
||||
)
|
||||
|
||||
type GetEmailVerificationResponse struct {
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
}
|
||||
390
internal/auth/keycloak/auth.go
Normal file
390
internal/auth/keycloak/auth.go
Normal file
@@ -0,0 +1,390 @@
|
||||
package keycloak
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/auth"
|
||||
"github.com/Nerzal/gocloak/v13"
|
||||
)
|
||||
|
||||
func (k *Client) RegisterUser(ctx context.Context, request auth.RegisterUserRequest) (*auth.RegisterUserResponse, error) {
|
||||
attrs := make(map[string][]string)
|
||||
|
||||
if request.User.Patronymic != nil {
|
||||
attrs[PatronymicAttr] = []string{*request.User.Patronymic}
|
||||
}
|
||||
|
||||
for key, value := range request.User.Permissions {
|
||||
attrs[key] = []string{value}
|
||||
}
|
||||
|
||||
for _, perm := range permissionsSlice {
|
||||
if _, ok := attrs[perm]; !ok {
|
||||
return nil, errors.Join(ErrBadRequest, ErrMissingPermission)
|
||||
}
|
||||
}
|
||||
|
||||
client, err := k.client.LoginClient(ctx, k.clientId, k.clientSecret, k.realm)
|
||||
if err != nil {
|
||||
return nil, k.handleError(err)
|
||||
}
|
||||
|
||||
userGroup, err := k.getUserGroupByType(request.UserType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
emailVerified := false
|
||||
if k.env != "production" {
|
||||
emailVerified = true
|
||||
}
|
||||
|
||||
userId, err := k.client.CreateUser(
|
||||
ctx,
|
||||
client.AccessToken,
|
||||
k.realm,
|
||||
gocloak.User{
|
||||
Email: gocloak.StringP(request.User.Email),
|
||||
EmailVerified: &emailVerified,
|
||||
FirstName: gocloak.StringP(request.User.FirstName),
|
||||
LastName: gocloak.StringP(request.User.SecondName),
|
||||
Groups: &[]string{userGroup},
|
||||
Enabled: gocloak.BoolP(true),
|
||||
Attributes: &attrs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, k.handleError(err)
|
||||
}
|
||||
|
||||
if err = k.client.SetPassword(
|
||||
ctx, client.AccessToken,
|
||||
userId, k.realm,
|
||||
request.User.Password, false,
|
||||
); err != nil {
|
||||
return nil, k.handleError(err)
|
||||
}
|
||||
|
||||
return &auth.RegisterUserResponse{
|
||||
UserId: userId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (k *Client) getUserGroupByType(userType int32) (string, error) {
|
||||
switch userType {
|
||||
case UserAgentType:
|
||||
return UserGroupAgents, nil
|
||||
case UserDistributorType:
|
||||
return UserGroupDistributor, nil
|
||||
default:
|
||||
return "", ErrUnknownUserType
|
||||
}
|
||||
}
|
||||
|
||||
func (k *Client) LoginUser(ctx context.Context, request auth.LoginUserRequest) (*auth.LoginUserResponse, error) {
|
||||
token, err := k.client.Login(ctx, k.clientId, k.clientSecret, k.realm, request.Email, request.Password)
|
||||
if err != nil {
|
||||
return nil, k.handleError(err)
|
||||
}
|
||||
|
||||
userInfo, err := k.jwtManager.GetUserInfoFromToken(token.AccessToken)
|
||||
if err != nil {
|
||||
return nil, k.handleError(err)
|
||||
}
|
||||
|
||||
return &auth.LoginUserResponse{
|
||||
UserId: userInfo.UserId,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (k *Client) GetNewAccessToken(ctx context.Context, request auth.GetNewAccessTokenRequest) (*auth.GetNewAccessTokenResponse, error) {
|
||||
token, err := k.client.RefreshToken(ctx, request.RefreshToken, k.clientId, k.clientSecret, k.realm)
|
||||
if err != nil {
|
||||
return nil, k.handleError(err)
|
||||
}
|
||||
|
||||
return &auth.GetNewAccessTokenResponse{
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (k *Client) CheckTokenIsValid(ctx context.Context, token string) (bool, error) {
|
||||
resp, err := k.client.RetrospectToken(ctx, token, k.clientId, k.clientSecret, k.realm)
|
||||
if err != nil {
|
||||
return false, k.handleError(err)
|
||||
}
|
||||
|
||||
if resp.Active == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return *resp.Active, nil
|
||||
}
|
||||
|
||||
func (k *Client) LogoutUser(ctx context.Context, request auth.LogoutUserRequest) error {
|
||||
if err := k.client.Logout(ctx, k.clientId, k.clientSecret, k.realm, request.RefreshToken); err != nil {
|
||||
return k.handleError(err)
|
||||
}
|
||||
|
||||
if err := k.client.RevokeToken(ctx, k.realm, k.clientId, k.clientSecret, request.RefreshToken); err != nil {
|
||||
return k.handleError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *Client) GetUserInfo(ctx context.Context, id string) (*auth.UserInfo, error) {
|
||||
client, err := k.client.LoginClient(ctx, k.clientId, k.clientSecret, k.realm)
|
||||
if err != nil {
|
||||
return nil, k.handleError(err)
|
||||
}
|
||||
|
||||
user, err := k.client.GetUserByID(ctx, client.AccessToken, k.realm, id)
|
||||
if err != nil {
|
||||
return nil, k.handleError(err)
|
||||
}
|
||||
|
||||
if user.Attributes == nil {
|
||||
return nil, ErrNoAttributes
|
||||
}
|
||||
|
||||
groups, err := k.client.GetUserGroups(ctx, client.AccessToken, k.realm, id, gocloak.GetGroupsParams{})
|
||||
if err != nil {
|
||||
return nil, k.handleError(err)
|
||||
}
|
||||
|
||||
if len(groups) == 0 {
|
||||
return nil, ErrUnknownUserType
|
||||
}
|
||||
|
||||
if groups[0].Name == nil {
|
||||
return nil, ErrUnknownUserType
|
||||
}
|
||||
|
||||
userType, err := k.getUserTypeByUserGroup(*groups[0].Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &auth.UserInfo{
|
||||
Email: *user.Email,
|
||||
FirstName: *user.FirstName,
|
||||
SecondName: *user.LastName,
|
||||
Patronymic: k.getUserPatronymicFromAttributes(*user.Attributes),
|
||||
Permissions: k.getUserPermissionsFromAttributes(*user.Attributes),
|
||||
UserType: userType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (k *Client) getUserPatronymicFromAttributes(attr attributes) *string {
|
||||
item, ok := attr[PatronymicAttr]
|
||||
if !ok || len(item) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &item[0]
|
||||
}
|
||||
|
||||
func (k *Client) getUserPermissionsFromAttributes(attr attributes) permissions {
|
||||
resp := make(map[string]string)
|
||||
|
||||
for _, permission := range permissionsSlice {
|
||||
if item, ok := attr[permission]; ok {
|
||||
if len(item) != 0 {
|
||||
resp[permission] = item[0]
|
||||
} else {
|
||||
resp[permission] = "" // todo: ะฟะพะดัะผะฐัั ะฝะฐะด ัะตะผ ะบะฐะบ ะฟัะฐะฒะธะปัะฝะพ ะดะตะปะฐัั ะฒ ัะฐะบะธั
ัะธััะฐัะธัั
|
||||
}
|
||||
} else {
|
||||
resp[permission] = "" // todo: ะฟะพะดัะผะฐัั ะฝะฐะด ัะตะผ ะบะฐะบ ะฟัะฐะฒะธะปัะฝะพ ะดะตะปะฐัั ะฒ ัะฐะบะธั
ัะธััะฐัะธัั
|
||||
}
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (k *Client) getUserTypeByUserGroup(attr string) (int32, error) {
|
||||
switch attr {
|
||||
case UserGroupAgents:
|
||||
return UserAgentType, nil
|
||||
case UserGroupDistributor:
|
||||
return UserDistributorType, nil
|
||||
default:
|
||||
return -1, ErrUnknownUserType
|
||||
}
|
||||
}
|
||||
|
||||
func (k *Client) GetPermissionsByUsersId(ctx context.Context, id string) (*auth.GetPermissionsByUsersIdResponse, error) {
|
||||
userInfo, err := k.GetUserInfo(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &auth.GetPermissionsByUsersIdResponse{
|
||||
Balance: userInfo.Permissions[PermissionBalance],
|
||||
Company: userInfo.Permissions[PermissionCompany],
|
||||
Employees: userInfo.Permissions[PermissionEmployees],
|
||||
Profile: userInfo.Permissions[PermissionProfile],
|
||||
Submissions: userInfo.Permissions[PermissionSubmissions],
|
||||
Vacancies: userInfo.Permissions[PermissionVacancies],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (k *Client) CheckPermissions(ctx context.Context, request auth.CheckPermissionsRequest) (bool, error) {
|
||||
userInfo, err := k.GetUserInfo(ctx, request.UserId)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if userInfo.Permissions == nil {
|
||||
return false, ErrInternal
|
||||
}
|
||||
|
||||
if val, ok := userInfo.Permissions[request.RequiredPermission]; ok {
|
||||
return k.getPermissionLevel(val) >= k.getPermissionLevel(request.RequiredPermissionLevel), nil
|
||||
}
|
||||
|
||||
return false, ErrInternal
|
||||
}
|
||||
|
||||
func (k *Client) getPermissionLevel(permission string) int8 {
|
||||
switch permission {
|
||||
case PermissionLevelCanEdit:
|
||||
return 2
|
||||
case PermissionLevelCanView:
|
||||
return 1
|
||||
case PermissionLevelNoPermission:
|
||||
return 0
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
func (k *Client) VerifyEmail(ctx context.Context, userID string, token string) error {
|
||||
if err := k.jwtManager.VerifyEmailToken(token, userID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get admin token
|
||||
adminToken, err := k.client.LoginClient(ctx, k.clientId, k.clientSecret, k.realm)
|
||||
if err != nil {
|
||||
return k.handleError(err)
|
||||
}
|
||||
|
||||
// Get user
|
||||
user, err := k.client.GetUserByID(ctx, adminToken.AccessToken, k.realm, userID)
|
||||
if err != nil {
|
||||
return k.handleError(err)
|
||||
}
|
||||
|
||||
// Set email as verified
|
||||
user.EmailVerified = gocloak.BoolP(true)
|
||||
|
||||
// Update user
|
||||
if err := k.client.UpdateUser(ctx, adminToken.AccessToken, k.realm, *user); err != nil {
|
||||
return k.handleError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *Client) GetEmailVerificationStatus(ctx context.Context, userID string) (*auth.GetEmailVerificationResponse, error) {
|
||||
// Get admin token
|
||||
adminToken, err := k.client.LoginClient(ctx, k.clientId, k.clientSecret, k.realm)
|
||||
if err != nil {
|
||||
return nil, k.handleError(err)
|
||||
}
|
||||
|
||||
// Get user
|
||||
user, err := k.client.GetUserByID(ctx, adminToken.AccessToken, k.realm, userID)
|
||||
if err != nil {
|
||||
return nil, k.handleError(err)
|
||||
}
|
||||
|
||||
// Check if email is verified
|
||||
verified := false
|
||||
if user.EmailVerified != nil {
|
||||
verified = *user.EmailVerified
|
||||
}
|
||||
|
||||
return &auth.GetEmailVerificationResponse{
|
||||
EmailVerified: verified,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (k *Client) GetUserEmailVerificationToken(ctx context.Context, uid string) (*auth.GetUserTokenResponse, error) {
|
||||
// ะะพะปััะตะฝะธะต ัะพะบะตะฝะฐ ะฐะดะผะธะฝะธัััะฐัะพัะฐ
|
||||
adminToken, err := k.client.LoginClient(ctx, k.clientId, k.clientSecret, k.realm)
|
||||
if err != nil {
|
||||
return nil, k.handleError(err)
|
||||
}
|
||||
|
||||
// ะะพะปััะฐะตะผ ะฟะพะปัะทะพะฒะฐัะตะปั, ััะพะฑั ัะฑะตะดะธัััั ะฒ ะตะณะพ ัััะตััะฒะพะฒะฐะฝะธะธ
|
||||
_, err = k.client.GetUserByID(ctx, adminToken.AccessToken, k.realm, uid)
|
||||
if err != nil {
|
||||
return nil, k.handleError(err)
|
||||
}
|
||||
|
||||
// ะกะพะทะดะฐัะผ ะบะฐััะพะผะฝัะน JWT-ัะพะบะตะฝ ะดะปั ะฒะตัะธัะธะบะฐัะธะธ email ััะพะบะพะผ ะฝะฐ 24 ัะฐัะฐ
|
||||
claims := jwt.MapClaims{
|
||||
"sub": uid, // Subject (user ID)
|
||||
"iat": time.Now().Unix(), // Issued at
|
||||
"exp": time.Now().Add(24 * time.Hour).Unix(), // Expires in 24 hours
|
||||
"purpose": "email_verification", // Custom claim to identify purpose
|
||||
"iss": "molva-api-gateway", // Issuer
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
// ะัะฟะพะปัะทัะตะผ ะบะปะธะตะฝััะบะธะน ัะตะบัะตั ะบะฐะบ ะบะปัั ะฟะพะดะฟะธัะธ
|
||||
signedToken, err := token.SignedString([]byte(k.clientSecret))
|
||||
if err != nil {
|
||||
return nil, ErrInternal
|
||||
}
|
||||
|
||||
return &auth.GetUserTokenResponse{
|
||||
AccessToken: signedToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (k *Client) ResetPassword(ctx context.Context, request auth.ResetPasswordRequest) error {
|
||||
token, err := k.client.LoginClient(ctx, k.clientId, k.clientSecret, k.realm)
|
||||
if err != nil {
|
||||
return k.handleError(err)
|
||||
}
|
||||
|
||||
user, err := k.client.GetUsers(
|
||||
ctx,
|
||||
token.AccessToken,
|
||||
k.realm,
|
||||
gocloak.GetUsersParams{
|
||||
Username: gocloak.StringP(request.Email),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return k.handleError(err)
|
||||
}
|
||||
|
||||
if len(user) == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
if err := k.client.SetPassword(
|
||||
ctx,
|
||||
token.AccessToken,
|
||||
*user[0].ID,
|
||||
k.realm,
|
||||
request.NewPassword,
|
||||
false,
|
||||
); err != nil {
|
||||
return k.handleError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
15
internal/auth/keycloak/client.go
Normal file
15
internal/auth/keycloak/client.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package keycloak
|
||||
|
||||
import (
|
||||
"github.com/Nerzal/gocloak/v13"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
client *gocloak.GoCloak
|
||||
jwtManager TokenManager
|
||||
|
||||
env string
|
||||
realm string
|
||||
clientId string
|
||||
clientSecret string
|
||||
}
|
||||
38
internal/auth/keycloak/constants.go
Normal file
38
internal/auth/keycloak/constants.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package keycloak
|
||||
|
||||
const (
|
||||
PatronymicAttr = "patronymic"
|
||||
|
||||
PermissionLevelCanEdit = "can_edit"
|
||||
PermissionLevelCanView = "can_view"
|
||||
PermissionLevelNoPermission = "no_permission"
|
||||
|
||||
UserGroupAgents = "agents"
|
||||
UserGroupDistributor = "distributors"
|
||||
|
||||
UserAgentType = 0
|
||||
UserDistributorType = 1
|
||||
|
||||
PermissionBalance = "balance"
|
||||
PermissionCompany = "company"
|
||||
PermissionEmployees = "employees"
|
||||
PermissionProfile = "profile"
|
||||
PermissionSubmissions = "submissions"
|
||||
PermissionVacancies = "vacancies"
|
||||
)
|
||||
|
||||
var (
|
||||
permissionsSlice = []string{
|
||||
PermissionBalance,
|
||||
PermissionCompany,
|
||||
PermissionEmployees,
|
||||
PermissionProfile,
|
||||
PermissionSubmissions,
|
||||
PermissionVacancies,
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
attributes map[string][]string
|
||||
permissions map[string]string
|
||||
)
|
||||
16
internal/auth/keycloak/errors.go
Normal file
16
internal/auth/keycloak/errors.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package keycloak
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrInvalidToken = errors.New("invalid token")
|
||||
ErrInternal = errors.New("internal error")
|
||||
ErrRealmClientNotFound = errors.New("realm client not found")
|
||||
ErrRealmClientUnauthorized = errors.New("realm client unauthorized")
|
||||
ErrUnknownUserType = errors.New("unknown user type")
|
||||
ErrNoAttributes = errors.New("no attributes")
|
||||
ErrAlreadyExists = errors.New("user already exists")
|
||||
ErrBadRequest = errors.New("bad request")
|
||||
ErrNotFound = errors.New("user not found")
|
||||
ErrMissingPermission = errors.New("missing one or more permissions")
|
||||
)
|
||||
51
internal/auth/keycloak/interface.go
Normal file
51
internal/auth/keycloak/interface.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package keycloak
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/Nerzal/gocloak/v13"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Env string
|
||||
AuthServerAddr string
|
||||
Realm string
|
||||
ClientId string
|
||||
ClientSecret string
|
||||
}
|
||||
|
||||
func New(c *Config) (*Client, error) {
|
||||
cli := &Client{
|
||||
client: gocloak.NewClient(c.AuthServerAddr),
|
||||
jwtManager: NewJWTManager(),
|
||||
|
||||
env: c.Env,
|
||||
realm: c.Realm,
|
||||
clientId: c.ClientId,
|
||||
clientSecret: c.ClientSecret,
|
||||
}
|
||||
|
||||
return cli, nil
|
||||
}
|
||||
|
||||
func (k *Client) handleError(err error) error {
|
||||
var apiErr *gocloak.APIError
|
||||
|
||||
if !errors.As(err, &apiErr) {
|
||||
return ErrInternal
|
||||
}
|
||||
|
||||
switch apiErr.Code {
|
||||
case http.StatusNotFound:
|
||||
return ErrRealmClientNotFound
|
||||
case http.StatusUnauthorized:
|
||||
return ErrRealmClientUnauthorized
|
||||
case http.StatusConflict:
|
||||
return ErrAlreadyExists
|
||||
case http.StatusBadRequest:
|
||||
return ErrBadRequest
|
||||
default:
|
||||
return ErrInternal
|
||||
}
|
||||
}
|
||||
76
internal/auth/keycloak/jwt.go
Normal file
76
internal/auth/keycloak/jwt.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package keycloak
|
||||
|
||||
import (
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/auth"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TokenManager interface {
|
||||
GetUserInfoFromToken(token string) (*auth.UserInfoFromToken, error)
|
||||
VerifyEmailToken(token string, expectedUserID string) error
|
||||
}
|
||||
|
||||
type tokenManager struct {
|
||||
parser *jwt.Parser
|
||||
}
|
||||
|
||||
func NewJWTManager() TokenManager {
|
||||
return &tokenManager{
|
||||
parser: jwt.NewParser(),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tokenManager) GetUserInfoFromToken(token string) (*auth.UserInfoFromToken, error) {
|
||||
userToken, _, err := t.parser.ParseUnverified(token, jwt.MapClaims{})
|
||||
if err != nil {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
if claims, ok := userToken.Claims.(jwt.MapClaims); ok {
|
||||
resp := auth.UserInfoFromToken{}
|
||||
|
||||
resp.UserId, ok = claims["sub"].(string)
|
||||
if !ok {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
func (t *tokenManager) VerifyEmailToken(token string, expectedUserID string) error {
|
||||
// Parse token without verification (we'll verify signature manually)
|
||||
parsedToken, _, err := t.parser.ParseUnverified(token, jwt.MapClaims{})
|
||||
if err != nil {
|
||||
return ErrInvalidToken
|
||||
}
|
||||
|
||||
// Extract claims
|
||||
claims, ok := parsedToken.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return ErrInvalidToken
|
||||
}
|
||||
|
||||
// Check if token is for email verification
|
||||
purpose, ok := claims["purpose"].(string)
|
||||
if !ok || purpose != "email_verification" {
|
||||
return ErrInvalidToken
|
||||
}
|
||||
|
||||
// Check if subject matches expected user ID
|
||||
sub, ok := claims["sub"].(string)
|
||||
if !ok || sub != expectedUserID {
|
||||
return ErrInvalidToken
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
exp, ok := claims["exp"].(float64)
|
||||
if !ok || float64(time.Now().Unix()) > exp {
|
||||
return ErrInvalidToken
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
257
internal/broker/rmq.go
Normal file
257
internal/broker/rmq.go
Normal file
@@ -0,0 +1,257 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/constants"
|
||||
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/config"
|
||||
requestmodel "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/request_model"
|
||||
"github.com/google/uuid"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
)
|
||||
|
||||
var conn *amqp.Connection
|
||||
|
||||
var (
|
||||
NotificationQueue string
|
||||
)
|
||||
|
||||
type Broker interface {
|
||||
Connect() error
|
||||
Disconnect() error
|
||||
}
|
||||
|
||||
type RabbitMQBroker struct {
|
||||
cfg *config.Broker
|
||||
}
|
||||
|
||||
func NewRabbitMQBroker(cfg *config.Broker) *RabbitMQBroker {
|
||||
NotificationQueue = cfg.NotificationsQueueName
|
||||
|
||||
return &RabbitMQBroker{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RabbitMQBroker) Connect() error {
|
||||
var err error
|
||||
|
||||
rabbitConn := fmt.Sprintf(
|
||||
"amqp://%s:%s@%s:%d",
|
||||
r.cfg.UserName,
|
||||
r.cfg.Password,
|
||||
r.cfg.Host,
|
||||
r.cfg.Port,
|
||||
)
|
||||
|
||||
conn, err = amqp.Dial(rabbitConn)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *RabbitMQBroker) Disconnect() error {
|
||||
return conn.Close()
|
||||
}
|
||||
|
||||
func produceMessage(qName, replyToQName, corrId string, message []byte, headers amqp.Table) (err error) {
|
||||
ch, err := conn.Channel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func(ch *amqp.Channel) {
|
||||
if errClose := ch.Close(); errClose != nil {
|
||||
err = errors.Join(err, errClose)
|
||||
}
|
||||
}(ch)
|
||||
|
||||
q, err := ch.QueueDeclare(
|
||||
qName, // name
|
||||
true, // durable
|
||||
false, // delete when unused
|
||||
false, // exclusive
|
||||
false, // no-wait
|
||||
nil, // arguments
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), constants.DefaultContextTimeout)
|
||||
defer cancel()
|
||||
|
||||
err = ch.PublishWithContext(ctx,
|
||||
"", // exchange
|
||||
q.Name, // routing key
|
||||
false, // mandatory
|
||||
false, // immediate
|
||||
amqp.Publishing{
|
||||
DeliveryMode: amqp.Persistent,
|
||||
Type: headers["command"].(string),
|
||||
ReplyTo: replyToQName,
|
||||
CorrelationId: corrId,
|
||||
Body: message,
|
||||
Headers: headers,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func consumeMessage(qName string, corrId string) (resp []byte, err error) {
|
||||
ch, err := conn.Channel()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func(ch *amqp.Channel) {
|
||||
if errClose := ch.Close(); errClose != nil {
|
||||
err = errors.Join(err, errClose)
|
||||
}
|
||||
}(ch)
|
||||
|
||||
if _, err = ch.QueueDeclare(
|
||||
qName, // name
|
||||
true, // durable
|
||||
true, // delete when unused
|
||||
false, // exclusive
|
||||
false, // no-wait
|
||||
nil, // arguments
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
messages, err := ch.Consume(
|
||||
qName, // queue
|
||||
"", // consumer
|
||||
false, // auto-ack
|
||||
false, // exclusive
|
||||
false, // no-local
|
||||
false, // no-wait
|
||||
nil, // args
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := <-messages
|
||||
//todo: ะะพะฟัะฐะฒะธัั ะฝะฐ ะฟัะพะฒะตัะบั correlationId
|
||||
// if d.CorrelationId == corrId {
|
||||
// return nil, fmt.Errorf("invalid correlation ID %s", d.CorrelationId)
|
||||
// }
|
||||
|
||||
if err = response.Ack(false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response.Body, nil
|
||||
}
|
||||
|
||||
func ProcessRequest(qName, replyToQName, corrId string, headers amqp.Table, message []byte, log *slog.Logger) ([]byte, error) {
|
||||
log.Info("produce request to: ",
|
||||
slog.String("qName", qName),
|
||||
slog.String("replyToQName", replyToQName),
|
||||
slog.String("command", headers["command"].(string)),
|
||||
slog.String("corrId", corrId),
|
||||
)
|
||||
|
||||
if err := produceMessage(qName, replyToQName, corrId, message, headers); err != nil {
|
||||
log.Error("got some error while producing message: ",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("corrId", corrId))
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Info("consume response from: ",
|
||||
slog.String("qName", qName),
|
||||
slog.String("replyToQName", replyToQName),
|
||||
slog.String("command", headers["command"].(string)),
|
||||
slog.String("corrId", corrId),
|
||||
)
|
||||
|
||||
body, err := consumeMessage(replyToQName, corrId)
|
||||
if err != nil {
|
||||
log.Error("got some error while consuming message: ",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("corrId", corrId))
|
||||
|
||||
return body, err
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func BuildAndProcessRequest(qName string, headers requestmodel.Header, message []byte, log *slog.Logger) ([]byte, error) {
|
||||
queueUuid := uuid.New().String()
|
||||
|
||||
resp, err := ProcessRequest(
|
||||
qName,
|
||||
queueUuid,
|
||||
"0",
|
||||
headers.ToTable(),
|
||||
message,
|
||||
log,
|
||||
)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func SendNotification(qName, messageType string, message []byte, log *slog.Logger) error {
|
||||
ch, err := conn.Channel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func(ch *amqp.Channel) {
|
||||
if errClose := ch.Close(); errClose != nil {
|
||||
err = errors.Join(err, errClose)
|
||||
}
|
||||
}(ch)
|
||||
|
||||
q, err := ch.QueueDeclare(
|
||||
qName, // name
|
||||
true, // durable
|
||||
false, // delete when unused
|
||||
false, // exclusive
|
||||
false, // no-wait
|
||||
nil, // arguments
|
||||
)
|
||||
if err != nil {
|
||||
log.Error("error while sending notification message",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("qName", qName))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), constants.DefaultContextTimeout)
|
||||
defer cancel()
|
||||
|
||||
err = ch.PublishWithContext(ctx,
|
||||
"", // exchange
|
||||
q.Name, // routing key
|
||||
false, // mandatory
|
||||
false, // immediate
|
||||
amqp.Publishing{
|
||||
Type: messageType,
|
||||
Body: message,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
log.Error("error while sending notification message",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("qName", qName))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
16
internal/cache/client.go
vendored
Normal file
16
internal/cache/client.go
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
package cache
|
||||
|
||||
import "fmt"
|
||||
|
||||
func New(c Config, t Type) (Client, error) {
|
||||
switch t {
|
||||
case ValkeyCacheType:
|
||||
return newValKeyCache(c)
|
||||
default:
|
||||
return nil, ErrUnknownCacheType
|
||||
}
|
||||
}
|
||||
|
||||
func getKey(k string, t ValueType) string {
|
||||
return fmt.Sprintf("%s:%s", t, k)
|
||||
}
|
||||
16
internal/cache/errors.go
vendored
Normal file
16
internal/cache/errors.go
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
package cache
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrUnknownCacheType = errors.New("unknown cache type")
|
||||
ErrInvalidConfig = errors.New("invalid config")
|
||||
ErrConnect = errors.New("connecting to server")
|
||||
ErrTlsConfig = errors.New("tls config")
|
||||
ErrInvalidInstance = errors.New("check instance failed")
|
||||
ErrKeyNotFound = errors.New("key not found")
|
||||
ErrKeyExists = errors.New("key already exists")
|
||||
ErrGet = errors.New("failed to get value")
|
||||
ErrSet = errors.New("failed to set value")
|
||||
ErrDel = errors.New("failed to delete value")
|
||||
)
|
||||
53
internal/cache/types.go
vendored
Normal file
53
internal/cache/types.go
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client interface {
|
||||
// Set ะดะพะฑะฐะฒะปัะตั ะฒ ะบะตั ะฟะพะด ะบะปััะตะผ key ะทะฝะฐัะตะฝะธะต value
|
||||
//
|
||||
// ะัะปะธ ัะฐะบะพะณะพ ะบะปััะฐ ั ัะฐะบะธะผ ัะธะฟะพะผ ะทะฝะฐัะตะฝะธั ะฝะตั,
|
||||
// ัะพ ะฒะพะทะฒัะฐัะฐะตั ะพัะธะฑะบั ErrKeyExists
|
||||
Set(ctx context.Context, key string, valueType ValueType, value any, expiration time.Duration) error
|
||||
|
||||
// Del ัะดะฐะปัะตั ะทะฝะฐัะตะฝะธะต ะฟะพ ะบะปััั key ะธ ัะบะฐะทะฐะฝะฝะพะผั valueType
|
||||
//
|
||||
// ะัะปะธ ัะฐะบะพะณะพ ะบะปััะฐ ั ัะฐะบะธะผ ัะธะฟะพะผ ะทะฝะฐัะตะฝะธั ะฝะตั,
|
||||
// ัะพ ะฒะพะทะฒัะฐัะฐะตั ะพัะธะฑะบั ErrKeyNotFound
|
||||
Del(ctx context.Context, key string, valueType ValueType) error
|
||||
|
||||
// Get ะฒะพะทะฒัะฐัะฐะตั ะทะฝะฐัะตะฝะธะต ะฟะพ ะบะปััั key ะธ valueType
|
||||
//
|
||||
// ะัะปะธ ัะฐะบะพะณะพ ะบะปััะฐ ั ัะฐะบะธะผ ัะธะฟะพะผ ะทะฝะฐัะตะฝะธั ะฝะตั,
|
||||
// ัะพ ะฒะพะทะฒัะฐัะฐะตั ะพัะธะฑะบั ErrKeyNotFound
|
||||
//
|
||||
// ะ ัะปััะฐะต, ะตัะปะธ ะฟะพะปััะฐะตะผะพะต ะทะฝะฐัะตะฝะธะต ะฝะต ัะฒะปัะตััั ัััะพะบะพะน,
|
||||
// ะฝะตะพะฑั
ะพะดะธะผะพ ะฒะพัะฟะพะปัะทะพะฒะฐัััั ัะพะพัะฒะตััะฒัััะธะผ ะผะตัะพะดะพะผ ะฟะพะด
|
||||
// ััะพั ัะธะฟ ะดะฐะฝะฝัั
|
||||
Get(ctx context.Context, key string, valueType ValueType) (string, error)
|
||||
|
||||
// TODO: ะะพ ะฝะตะพะฑั
ะพะดะธะผะพััะธ ัะฐััะธัะธัั ะฝะตะพะฑั
ะพะดะธะผัะผะธ ะผะตัะพะดะฐะผะธ
|
||||
}
|
||||
|
||||
type (
|
||||
Type uint
|
||||
ValueType string
|
||||
Config any
|
||||
)
|
||||
|
||||
const (
|
||||
ValkeyCacheType Type = iota
|
||||
)
|
||||
|
||||
const (
|
||||
LogoValueType ValueType = "value_logo_type"
|
||||
DocumentsValueType ValueType = "value_documents_type"
|
||||
|
||||
PswResetOTPValueType ValueType = "value_otp_type"
|
||||
PswResetTokenValueType ValueType = "value_token_type"
|
||||
|
||||
IntegrationCompanyValueType ValueType = "integration:company"
|
||||
IntegrationVacancyValueType ValueType = "integration:vacancy"
|
||||
)
|
||||
143
internal/cache/valkey.go
vendored
Normal file
143
internal/cache/valkey.go
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v7"
|
||||
)
|
||||
|
||||
type ValKeyCacheConfig struct {
|
||||
Addrs []string
|
||||
Password string
|
||||
ReadOnly bool
|
||||
DialTimeout time.Duration
|
||||
PoolSize int
|
||||
DefaultTTL time.Duration
|
||||
RootCaFilePath string
|
||||
}
|
||||
|
||||
type valkeyClient interface {
|
||||
Set(key string, value any, expiration time.Duration) *redis.StatusCmd
|
||||
Get(key string) *redis.StringCmd
|
||||
Del(keys ...string) *redis.IntCmd
|
||||
Exists(keys ...string) *redis.IntCmd
|
||||
Ping() *redis.StatusCmd
|
||||
}
|
||||
|
||||
type valKeyCache struct {
|
||||
config ValKeyCacheConfig
|
||||
client valkeyClient
|
||||
}
|
||||
|
||||
func newValKeyCache(c Config) (*valKeyCache, error) {
|
||||
cfg, ok := c.(ValKeyCacheConfig)
|
||||
if !ok {
|
||||
return nil, ErrInvalidConfig
|
||||
}
|
||||
|
||||
caCert, err := os.ReadFile(cfg.RootCaFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrTlsConfig, err.Error())
|
||||
}
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
|
||||
if ok := caCertPool.AppendCertsFromPEM(caCert); !ok {
|
||||
return nil, fmt.Errorf("%w: %s", ErrTlsConfig, "failed to append CA cert to pool")
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
RootCAs: caCertPool,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
client := redis.NewUniversalClient(&redis.UniversalOptions{
|
||||
Addrs: cfg.Addrs,
|
||||
Password: cfg.Password,
|
||||
ReadOnly: cfg.ReadOnly,
|
||||
DialTimeout: cfg.DialTimeout,
|
||||
PoolSize: cfg.PoolSize,
|
||||
TLSConfig: tlsConfig,
|
||||
})
|
||||
|
||||
if err := client.Ping().Err(); err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrConnect, err)
|
||||
}
|
||||
|
||||
return &valKeyCache{
|
||||
config: cfg,
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *valKeyCache) Get(ctx context.Context, key string, valueType ValueType) (string, error) {
|
||||
if err := c.checkInstance(); err != nil {
|
||||
return "", fmt.Errorf("%w: %v", ErrGet, err)
|
||||
}
|
||||
|
||||
val, err := c.client.Get(getKey(key, valueType)).Result()
|
||||
if err == redis.Nil {
|
||||
return "", ErrKeyNotFound
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: %v", ErrGet, err)
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func (c *valKeyCache) Set(ctx context.Context, key string, valueType ValueType, value any, expiration time.Duration) error {
|
||||
if err := c.checkInstance(); err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrSet, err)
|
||||
}
|
||||
|
||||
key = getKey(key, valueType)
|
||||
|
||||
if exists, err := c.client.Exists(key).Result(); err != nil || exists == 1 {
|
||||
if exists == 1 {
|
||||
return ErrKeyExists
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: %v", ErrSet, err)
|
||||
}
|
||||
|
||||
if err := c.client.Set(key, value, expiration).Err(); err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrSet, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *valKeyCache) Del(ctx context.Context, key string, valueType ValueType) error {
|
||||
if err := c.checkInstance(); err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrDel, err.Error())
|
||||
}
|
||||
|
||||
if res, err := c.client.Del(getKey(key, valueType)).Result(); err != nil || res == 0 {
|
||||
if res == 0 {
|
||||
return ErrKeyNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: %s", ErrDel, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *valKeyCache) checkInstance() error {
|
||||
if c == nil {
|
||||
return ErrInvalidInstance
|
||||
}
|
||||
|
||||
if s := c.client.Ping(); s.Err() != nil {
|
||||
return fmt.Errorf("%w: %v", ErrInvalidInstance, s.Err().Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
53
internal/config/config.go
Normal file
53
internal/config/config.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/ilyakaznacheev/cleanenv"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInternal = errors.New("can not load config")
|
||||
ErrMarshaling = errors.New("can not marshal config path")
|
||||
)
|
||||
|
||||
func MustLoad(version, commit, date string) *Config {
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cfg.Build.Version = version
|
||||
cfg.Build.Commit = commit
|
||||
cfg.Build.Date = date
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func loadConfig() (*Config, error) {
|
||||
if _, err := os.Stat(ConfigBasePath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("no config file found")
|
||||
}
|
||||
|
||||
var localConfig LocalDeploy
|
||||
|
||||
if err := cleanenv.ReadConfig(ConfigBasePath, &localConfig); err != nil {
|
||||
return nil, fmt.Errorf("error reading local config: %w", err)
|
||||
}
|
||||
|
||||
for _, env := range localConfig.Deploy.Env {
|
||||
if err := os.Setenv(env.Name, env.Value); err != nil {
|
||||
return nil, fmt.Errorf("error setting env %s: %w", env.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
|
||||
if err := cleanenv.ReadConfig(ConfigBasePath, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
5
internal/config/constants.go
Normal file
5
internal/config/constants.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package config
|
||||
|
||||
const (
|
||||
ConfigBasePath = "/home/alexorel/work/Molva/molva-backend/.build/config/local.example.yaml"
|
||||
)
|
||||
99
internal/config/dictionary.go
Normal file
99
internal/config/dictionary.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
type (
|
||||
Config struct {
|
||||
Env string `yaml:"env" env-default:"production"`
|
||||
Host string `yaml:"host" env-default:"localhost"`
|
||||
Port int `yaml:"port" env-default:"8000"`
|
||||
Keycloak Keycloak
|
||||
Broker Broker `yaml:"broker"`
|
||||
Secret SecretConfig
|
||||
EmailVerificationService EmailVerificationService `yaml:"emailVerificationService"`
|
||||
ValkeyCache ValkeyCacheConfig `yaml:"cache"`
|
||||
S3Storage S3StorageConfig `yaml:"s3Storage"`
|
||||
Database Database `yaml:"database"`
|
||||
Integration Integration
|
||||
Build BuildInfo
|
||||
}
|
||||
|
||||
BuildInfo struct {
|
||||
Version string
|
||||
Commit string
|
||||
Date string
|
||||
}
|
||||
|
||||
ValkeyCacheConfig struct {
|
||||
Addrs string `yaml:"-" env:"CACHE_ADDRS" env-default:"localhost:6379"`
|
||||
Password string `yaml:"-" env:"CACHE_PASSWORD" env-default:""`
|
||||
ReadOnly bool `yaml:"readOnly" env-default:"false"`
|
||||
DialTimeout time.Duration `yaml:"dialTimeout" env-default:"5s"`
|
||||
PoolSize int `yaml:"poolSize" env-default:"10"`
|
||||
DefaultTTL time.Duration `yaml:"defaultTtl" env-default:"1h"`
|
||||
RootCaFilePath string `yaml:"rootCaFilePath" env-default:""`
|
||||
}
|
||||
|
||||
S3StorageConfig struct {
|
||||
Bucket string `yaml:"bucket" env-default:""`
|
||||
DefaultLinkTTL time.Duration `yaml:"defaultLinkTtl" env-default:"1h"`
|
||||
}
|
||||
|
||||
SecretConfig struct {
|
||||
Key string `env:"SHORTENER_SECRET_KEY"` // 32-byte key
|
||||
}
|
||||
|
||||
Keycloak struct {
|
||||
BaseURL string `env:"KEYCLOAK_BASE_URL" env-default:"http://localhost"`
|
||||
Realm string `env:"KEYCLOAK_REALM" env-default:""`
|
||||
ClientId string `env:"KEYCLOAK_CLIENT_ID" env-default:""`
|
||||
ClientSecret string `env:"KEYCLOAK_CLIENT_SECRET" env-default:""`
|
||||
}
|
||||
|
||||
Broker struct {
|
||||
Host string `yaml:"-" env:"BROKER_HOST" env-default:"amqp://user:password@localhost"`
|
||||
Port int `yaml:"-" env:"BROKER_PORT" env-default:"5672"`
|
||||
UserName string `yaml:"-" env:"BROKER_USERNAME" env-default:"user"`
|
||||
Password string `yaml:"-" env:"BROKER_PASSWORD" env-default:"password"`
|
||||
NotificationsQueueName string `yaml:"notificationsQueueName" env-default:"notifications"`
|
||||
}
|
||||
|
||||
// TODO: ัะต ั ััะธะผ ะดะตะปะฐัั? ะะณะพ ะฝะธะณะดะต ะฝะตั (ะฝะธ ะฝะฐ ะดะตะฒ, ะฝะธ ะฝะฐ ะฟัะพะด)
|
||||
EmailVerificationService struct {
|
||||
APIKey string `yaml:"-" env-default:""`
|
||||
}
|
||||
|
||||
Database struct {
|
||||
Host string `yaml:"-" env:"DB_HOSTS"`
|
||||
Port uint16 `yaml:"-" env:"DB_PORT"`
|
||||
Username string `yaml:"-" env:"DB_USERNAME"`
|
||||
Password string `yaml:"-" env:"DB_PASSWORD"`
|
||||
DBName string `yaml:"-" env:"DB_DBNAME"`
|
||||
Schema string `yaml:"schema"`
|
||||
SSLMode string `yaml:"sslmode" env-default:"verify-full"`
|
||||
RootCaFilePath string `yaml:"rootCaFilePath" env-default:"/root/YandexInternalRootCA.crt"`
|
||||
}
|
||||
|
||||
Integration struct {
|
||||
Vkusvill VkusvillIntegration `yaml:"vkusvill"`
|
||||
}
|
||||
|
||||
VkusvillIntegration struct {
|
||||
ApiToken string `yaml:"-" env:"INTEGRATION_VKUSVILL_API_TOKEN"`
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
LocalDeploy struct {
|
||||
Deploy LocalDeployConfig `yaml:"deploy"`
|
||||
}
|
||||
|
||||
LocalDeployConfig struct {
|
||||
Env []LocalDeployConfigElement `yaml:"env"`
|
||||
}
|
||||
|
||||
LocalDeployConfigElement struct {
|
||||
Name string `yaml:"name"`
|
||||
Value string `yaml:"value"`
|
||||
}
|
||||
)
|
||||
17
internal/constants/errors.go
Normal file
17
internal/constants/errors.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package constants
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrInternalServerError = errors.New("internal server error")
|
||||
ErrBadRequest = errors.New("bad request")
|
||||
ErrForbidden = errors.New("forbidden")
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
ErrConflict = errors.New("conflict")
|
||||
ErrRequestTimeout = errors.New("request timeout")
|
||||
ErrNotImplemented = errors.New("not implemented")
|
||||
ErrBadGateway = errors.New("bad gateway")
|
||||
ErrServiceUnavailable = errors.New("service unavailable")
|
||||
ErrIntOutOfRange = errors.New("value is out of range for int32")
|
||||
)
|
||||
11
internal/constants/feed.go
Normal file
11
internal/constants/feed.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
FeedEventsTableName = "feed_events"
|
||||
ClientTableName = "client"
|
||||
SubmissionTableName = "submission"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultFeedFilterLimit = 20
|
||||
)
|
||||
182
internal/constants/notifications.go
Normal file
182
internal/constants/notifications.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
EmailNotificationMessageType = "email"
|
||||
|
||||
RegistrationNotificationMessageSubject = "ะ ะตะณะธัััะฐัะธั ะฟะพะปัะทะพะฒะฐัะตะปั ะฒ ัะธััะตะผะต ะฟัะธะปะพะถะตะฝะธั Molva"
|
||||
RegistrationNewUserAdmin = "ะะฐัะตะณะธัััะธัะพะฒะฐะฝ ะฝะพะฒัะน ะฟะพะปัะทะพะฒะฐัะตะปั ะฟัะธะปะพะถะตะฝะธั"
|
||||
EmailVerificationMessageSubject = "ะะพะดัะฒะตัะถะดะตะฝะธะต ะฐะดัะตัะฐ ัะปะตะบััะพะฝะฝะพะน ะฟะพััั"
|
||||
TextNotificationContentType = "text/plain"
|
||||
HTMLNotificationContentType = "text/html"
|
||||
RegistrationNotificationText = `
|
||||
ะะพะฑัะพ ะฟะพะถะฐะปะพะฒะฐัั ะฒ Molva!
|
||||
|
||||
ะะดัะฐะฒััะฒัะนัะต, %s!
|
||||
|
||||
ะกะฟะฐัะธะฑะพ ะทะฐ ัะตะณะธัััะฐัะธั ะฒ Molva. ะั ัะฐะดั ะฒะธะดะตัั ะฒะฐั ะฒ ะฝะฐัะตะผ ัะพะพะฑัะตััะฒะต.
|
||||
|
||||
ะขะตะฟะตัั ะฒั ะผะพะถะตัะต:
|
||||
- ะะพะปััะฐัั ะฟะตััะพะฝะฐะปะธะทะธัะพะฒะฐะฝะฝัะต ัะฒะตะดะพะผะปะตะฝะธั.
|
||||
- ะะฐัััะฐะธะฒะฐัั ัะฒะพะธ ะฟัะตะดะฟะพััะตะฝะธั.
|
||||
- ะัะฟะพะปัะทะพะฒะฐัั ะฒัะต ะฒะพะทะผะพะถะฝะพััะธ ะฟะปะฐััะพัะผั.
|
||||
|
||||
ะัะปะธ ั ะฒะฐั ะตััั ะฒะพะฟัะพัั, ะฝะต ััะตัะฝัะนัะตัั ะพะฑัะฐัะฐัััั ะฒ ะฝะฐัั ัะปัะถะฑั ะฟะพะดะดะตัะถะบะธ!
|
||||
ะะพัะปะต ะฟัะพะฒะตัะบะธ ะฐะบะบะฐัะฝัะฐ ะฝะฐั ะฐะดะผะธะฝะธัััะฐัะพั ั ะฒะฐะผะธ ัะฒัะถะตััั
|
||||
|
||||
ะก ัะฒะฐะถะตะฝะธะตะผ, ะบะพะผะฐะฝะดะฐ Molva.
|
||||
|
||||
ะญัะพ ะฟะธััะผะพ ะพัะฟัะฐะฒะปะตะฝะพ ะฐะฒัะพะผะฐัะธัะตัะบะธ, ะฟะพะถะฐะปัะนััะฐ, ะฝะต ะพัะฒะตัะฐะนัะต ะฝะฐ ะฝะตะณะพ.
|
||||
`
|
||||
|
||||
//nolint:gosec // there are no [potentially hardcoded] credentials in this message
|
||||
ForgotPasswordNotificationMessageSubject = "ะะพัััะฐะฝะพะฒะปะตะฝะธะต ะฟะฐัะพะปั ะฒ ัะธััะตะผะต ะฟัะธะปะพะถะตะฝะธั Molva"
|
||||
//nolint:gosec // there are no [potentially hardcoded] credentials in this message
|
||||
ForgotPasswordNotificationText = `
|
||||
ะะฐั ะบะพะด ะฒะพัััะฐะฝะพะฒะปะตะฝะธั ะฟะฐัะพะปั: %s. ะะณะพ ะผะพะถะฝะพ ะธัะฟะพะปัะทะพะฒะฐัั, ััะพะฑั ัะฑัะพัะธัั ะฟะฐัะพะปั ะธ ะทะฐะนัะธ ะฒ ัะฒะพะน ะฐะบะบะฐัะฝั ะฒ Molva.
|
||||
|
||||
ะัะปะธ ะั ะฝะต ะทะฐะฟัะฐัะธะฒะฐะปะธ ะฒะพัััะฐะฝะพะฒะปะตะฝะธะต ะดะพัััะฟะฐ ะบ ะฐะบะบะฐัะฝัั, ะฟะพะถะฐะปัะนััะฐ, ะฟัะพะธะณะฝะพัะธััะนัะต ััะพ ะฟะธััะผะพ.
|
||||
|
||||
ะก ัะฒะฐะถะตะฝะธะตะผ, ะบะพะผะฐะฝะดะฐ Molva.
|
||||
`
|
||||
)
|
||||
|
||||
const EmailVerificationTemplate = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body style="margin: 0; padding: 20px; font-family: Arial, sans-serif; background-color: #f5f5f5;">
|
||||
<div style="max-width: 600px; margin: 0 auto; background-color: #ffffff; padding: 40px; border-radius: 8px;">
|
||||
<h1 style="color: #333; font-size: 24px; margin: 0 0 30px 0;">MOLVA</h1>
|
||||
|
||||
<h2 style="color: #333; font-size: 20px; margin: 0 0 20px 0;">ะะพะดัะฒะตัะดะธัะต email</h2>
|
||||
|
||||
<p style="color: #555; font-size: 16px; line-height: 24px; margin: 0 0 10px 0;">
|
||||
ะะดัะฐะฒััะฒัะนัะต, %s!
|
||||
</p>
|
||||
|
||||
<p style="color: #555; font-size: 16px; line-height: 24px; margin: 0 0 30px 0;">
|
||||
ะะปั ะทะฐะฒะตััะตะฝะธั ัะตะณะธัััะฐัะธะธ ะฟะพะดัะฒะตัะดะธัะต ะฒะฐั email ะฐะดัะตั.
|
||||
</p>
|
||||
|
||||
<a href="%s" style="display: inline-block; padding: 12px 30px; background-color: #4A90E2; color: #ffffff; text-decoration: none; border-radius: 4px; font-size: 16px;">
|
||||
ะะพะดัะฒะตัะดะธัั email
|
||||
</a>
|
||||
|
||||
<p style="color: #999; font-size: 14px; line-height: 20px; margin: 30px 0 10px 0;">
|
||||
ะะปะธ ัะบะพะฟะธััะนัะต ัััะปะบั:
|
||||
</p>
|
||||
|
||||
<p style="color: #4A90E2; font-size: 14px; line-height: 20px; word-break: break-all; margin: 0 0 30px 0;">
|
||||
%s
|
||||
</p>
|
||||
|
||||
<p style="color: #999; font-size: 13px; line-height: 18px; margin: 0;">
|
||||
ะกััะปะบะฐ ะดะตะนััะฒะธัะตะปัะฝะฐ 24 ัะฐัะฐ.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
const EmailConfirmationPage = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>ะะพะดัะฒะตัะถะดะตะฝะธะต Email</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
.container {
|
||||
background: #ffffff;
|
||||
padding: 40px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
.title {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
font-size: 22px;
|
||||
}
|
||||
.message {
|
||||
color: #555;
|
||||
font-size: 16px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.close-note {
|
||||
color: #777;
|
||||
font-size: 14px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="title" id="status-title">ะะดัั ะฟะพะดัะฒะตัะถะดะตะฝะธะต...</div>
|
||||
<div class="message" id="status-message">ะะพะถะฐะปัะนััะฐ, ะฟะพะดะพะถะดะธัะต.</div>
|
||||
<div class="close-note" id="close-note" style="display:none;">
|
||||
ะั ะผะพะถะตัะต ะทะฐะบัััั ััะพ ะพะบะฝะพ.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
fetch("{{.VerificationURL}}", {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
const titleEl = document.getElementById("status-title");
|
||||
const msgEl = document.getElementById("status-message");
|
||||
const closeEl = document.getElementById("close-note");
|
||||
|
||||
if (response.ok) {
|
||||
titleEl.innerText = "Email ััะฟะตัะฝะพ ะฟะพะดัะฒะตัะถะดัะฝ!";
|
||||
msgEl.innerText = "ะกะฟะฐัะธะฑะพ ะทะฐ ะฟะพะดัะฒะตัะถะดะตะฝะธะต. ะขะตะฟะตัั ะฒั ะผะพะถะตัะต ะฒะตัะฝััััั ะฝะฐ ัะฐะนั MOLVA!";
|
||||
msgEl.style.color = "green";
|
||||
} else if (response.status === 400 || response.status === 404) {
|
||||
titleEl.innerText = "ะะตะฒะตัะฝะฐั ัััะปะบะฐ ะธะปะธ ัะพะบะตะฝ";
|
||||
msgEl.innerText = "ะัะพะฒะตัััะต ะฟัะฐะฒะธะปัะฝะพััั ัััะปะบะธ ะธะปะธ ะทะฐะฟัะพัะธัะต ะฝะพะฒัั.";
|
||||
msgEl.style.color = "red";
|
||||
} else {
|
||||
titleEl.innerText = "ะัะธะฑะบะฐ ัะตัะฒะตัะฐ";
|
||||
msgEl.innerText = "ะะพะฟัะพะฑัะนัะต ะฟะพะฒัะพัะธัั ะฟะพะทะดะฝะตะต.";
|
||||
msgEl.style.color = "red";
|
||||
}
|
||||
|
||||
closeEl.style.display = "block";
|
||||
})
|
||||
.catch(() => {
|
||||
const titleEl = document.getElementById("status-title");
|
||||
const msgEl = document.getElementById("status-message");
|
||||
const closeEl = document.getElementById("close-note");
|
||||
|
||||
titleEl.innerText = "ะะต ัะดะฐะปะพัั ะฟะพะดะบะปััะธัััั";
|
||||
msgEl.innerText = "ะัะพะฒะตัััะต ะธะฝัะตัะฝะตั-ัะพะตะดะธะฝะตะฝะธะต ะธ ะฟะพะฟัะพะฑัะนัะต ัะฝะพะฒะฐ.";
|
||||
msgEl.style.color = "red";
|
||||
closeEl.style.display = "block";
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
const EmailNewUserRegistrationAdminMessage = `
|
||||
ะะฐัะตะณะธัััะธัะพะฒะฐะฝ ะฝะพะฒัะน ะฟะพะปัะทะพะฒะฐัะตะปั. Email ะฟะพะดัะฒะตัะถะดัะฝ
|
||||
Email: %s
|
||||
Name: %s
|
||||
Type: %s
|
||||
`
|
||||
38
internal/constants/permissions.go
Normal file
38
internal/constants/permissions.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package constants
|
||||
|
||||
type PermissionType string
|
||||
|
||||
const (
|
||||
Balance PermissionType = "balance"
|
||||
Vacancies PermissionType = "vacancies"
|
||||
Employees PermissionType = "employees"
|
||||
Profile PermissionType = "profile"
|
||||
Company PermissionType = "company"
|
||||
Submissions PermissionType = "submissions"
|
||||
)
|
||||
|
||||
func (p PermissionType) String() string {
|
||||
return string(p)
|
||||
}
|
||||
|
||||
type PermissionValue string
|
||||
|
||||
const (
|
||||
NoPermission PermissionValue = "no_permission"
|
||||
CanView PermissionValue = "can_view"
|
||||
CanEdit PermissionValue = "can_edit"
|
||||
)
|
||||
|
||||
func (p PermissionValue) String() string {
|
||||
return string(p)
|
||||
}
|
||||
|
||||
var PermissionLevels = map[PermissionValue]int{
|
||||
NoPermission: 1,
|
||||
CanView: 2,
|
||||
CanEdit: 3,
|
||||
}
|
||||
|
||||
var AllPermissions = []PermissionType{
|
||||
Vacancies, Balance, Company, Submissions, Profile, Employees, Submissions,
|
||||
}
|
||||
27
internal/constants/s3.go
Normal file
27
internal/constants/s3.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package constants
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
LogoLinkTTL = 55 * time.Minute
|
||||
LogoMaxSize = 10 << 20 // 10 MB
|
||||
BytesToDetectContentType = 512
|
||||
ParseMultipartFormAllFile = 0
|
||||
TempDirPermission = 0750
|
||||
TempLogoFilePattern = "logo-*.tmp"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultFileTTL = 30 * 24 * time.Hour //30 days
|
||||
)
|
||||
|
||||
var ValidImageTypes = map[string]bool{
|
||||
"image/png": true,
|
||||
"image/jpeg": true,
|
||||
"image/jpg": true,
|
||||
"image/webp": true,
|
||||
}
|
||||
|
||||
const (
|
||||
RefreshLogoCacheAsyncTimeout = 10 * time.Second
|
||||
)
|
||||
31
internal/constants/utils.go
Normal file
31
internal/constants/utils.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package constants
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
EmailVerificationServiceURL = "https://emailvalidation.abstractapi.com/v1/"
|
||||
DocumentsPath = "/opt/molva/docs"
|
||||
UserFilesBasePath = "/opt/molva/user_files"
|
||||
DefaultRetryTimeout = time.Second * 10
|
||||
DefaultContextTimeout = 30 * time.Second
|
||||
|
||||
DefaultPaginationPageSize = 20
|
||||
DefaultPaginationPage = 1
|
||||
)
|
||||
|
||||
const (
|
||||
AdminNotificationEmail = "information@molva.io"
|
||||
AdminNotificationId = 0
|
||||
)
|
||||
|
||||
var AdminNotificationEmails = []string{AdminNotificationEmail}
|
||||
|
||||
const (
|
||||
UserTypeAgentName = "ะะณะตะฝั"
|
||||
UserTypeDistributorName = "ะะธัััะธะฑัััะพั"
|
||||
)
|
||||
|
||||
const (
|
||||
DistributorClientType = 1
|
||||
AgentClientType = 0
|
||||
)
|
||||
62
internal/database/client.go
Normal file
62
internal/database/client.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
dberrors "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/errors"
|
||||
pgdb "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/postgres"
|
||||
dbtypes "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/types"
|
||||
)
|
||||
|
||||
type Client interface {
|
||||
CreateUser(ctx context.Context, request *dbtypes.UserSaveRequest) (*dbtypes.UserSaveResponse, error)
|
||||
GetClientValidation(ctx context.Context, request *dbtypes.ClientValidationGetRequest) (*dbtypes.ClientValidationGetResponse, error)
|
||||
|
||||
GetVacancyIntegrationInfoById(ctx context.Context, vacancyId string) (string, *dbtypes.VacancyExtraFieldsTemplate, error)
|
||||
GetCompanyMetadataById(ctx context.Context, companyId string) (*dbtypes.CompanyMetadata, *dbtypes.CompanyExtraFieldsTemplate, error)
|
||||
|
||||
GetVacancyList(ctx context.Context, request *dbtypes.VacancyListGetRequest) (*dbtypes.VacancyListGetResponse, error)
|
||||
CreateVacancy(ctx context.Context, request *dbtypes.VacancyCreateRequest) (*dbtypes.VacancyCreateResponse, error)
|
||||
UpdateVacancy(ctx context.Context, request *dbtypes.VacancyUpdateRequest) (*dbtypes.VacancyUpdateResponse, error)
|
||||
DeleteVacancy(ctx context.Context, request *dbtypes.VacancyDeleteRequest) (*dbtypes.VacancyDeleteResponse, error)
|
||||
SendVacancyToModeration(ctx context.Context, request *dbtypes.SendVacancyToModerationRequest) (*dbtypes.SendVacancyToModerationResponse, error)
|
||||
|
||||
GetSubmissionList(ctx context.Context, request *dbtypes.SubmissionListGetRequest) (*dbtypes.SubmissionListGetResponse, error)
|
||||
CreateSubmission(ctx context.Context, request *dbtypes.SubmissionCreateRequest) (*dbtypes.SubmissionCreateResponse, error)
|
||||
UpdateSubmissionStatus(ctx context.Context, request *dbtypes.SubmissionStatusUpdateRequest) (*dbtypes.SubmissionStatusUpdateResponse, error)
|
||||
DeleteSubmission(ctx context.Context, request *dbtypes.SubmissionDeleteRequest) (*dbtypes.SubmissionDeleteResponse, error)
|
||||
|
||||
GetCompanyList(ctx context.Context, request *dbtypes.CompanyListGetRequest) (*dbtypes.CompanyListGetResponse, error)
|
||||
GetCompanyById(ctx context.Context, request *dbtypes.CompanyByIdGetRequest) (*dbtypes.CompanyByIdGetResponse, error)
|
||||
CreateCompany(ctx context.Context, request *dbtypes.CompanyCreateRequest) (*dbtypes.CompanyCreateResponse, error)
|
||||
UpdateCompany(ctx context.Context, request *dbtypes.CompanyUpdateRequest) (*dbtypes.CompanyUpdateResponse, error)
|
||||
// DeleteDistributorCompany(ctx context.Context, request *dbtypes.DistributorCompanyDeleteRequest) (*dbtypes.DistributorCompanyDeleteResponse, error)
|
||||
|
||||
GetProfileById(ctx context.Context, request *dbtypes.ProfileGetRequest) (*dbtypes.ProfileGetResponse, error)
|
||||
UpdateProfile(ctx context.Context, request *dbtypes.ProfileUpdateRequest) (*dbtypes.ProfileUpdateResponse, error)
|
||||
|
||||
GetBalance(ctx context.Context, request *dbtypes.BalanceGetRequest) (*dbtypes.BalanceGetResponse, error)
|
||||
|
||||
GetTransactionList(ctx context.Context, request *dbtypes.TransactionListGetRequest) (*dbtypes.TransactionListGetResponse, error)
|
||||
CreateTransaction(ctx context.Context, request *dbtypes.TransactionCreateRequest) (*dbtypes.TransactionCreateResponse, error)
|
||||
|
||||
GetBankAccountList(ctx context.Context, request *dbtypes.BankAccountListGetRequest) (*dbtypes.BankAccountListGetResponse, error)
|
||||
CreateBankAccount(ctx context.Context, request *dbtypes.BankAccountCreateRequest) (*dbtypes.BankAccountCreateResponse, error)
|
||||
UpdateBankAccount(ctx context.Context, request *dbtypes.BankAccountUpdateRequest) (*dbtypes.BankAccountUpdateResponse, error)
|
||||
DeleteBankAccount(ctx context.Context, request *dbtypes.BankAccountDeleteRequest) (*dbtypes.BankAccountDeleteResponse, error)
|
||||
}
|
||||
|
||||
func New(cType ClientType, c Config) (Client, error) {
|
||||
switch cType {
|
||||
case PostgresClientType:
|
||||
cfg, ok := c.(pgdb.PostgresConfig)
|
||||
if !ok {
|
||||
return nil, dberrors.ErrInvalidConfig
|
||||
}
|
||||
|
||||
return pgdb.NewClient(cfg)
|
||||
|
||||
default:
|
||||
return nil, dberrors.ErrClientType
|
||||
}
|
||||
}
|
||||
10
internal/database/domain.go
Normal file
10
internal/database/domain.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package database
|
||||
|
||||
type (
|
||||
ClientType int
|
||||
Config any
|
||||
)
|
||||
|
||||
const (
|
||||
PostgresClientType ClientType = iota
|
||||
)
|
||||
16
internal/database/errors/errors.go
Normal file
16
internal/database/errors/errors.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package dberrors
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrClientType = errors.New("invalid client type")
|
||||
ErrInvalidConfig = errors.New("invalid config")
|
||||
|
||||
ErrUnimplemented = errors.New("unimplemented")
|
||||
|
||||
ErrBadRequest = errors.New("bad request")
|
||||
ErrForbidden = errors.New("forbidden")
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrConflict = errors.New("conflict")
|
||||
ErrInternal = errors.New("internal")
|
||||
)
|
||||
100
internal/database/postgres/balance.go
Normal file
100
internal/database/postgres/balance.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package pgdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
dberrors "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/errors"
|
||||
dbtypes "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/types"
|
||||
"github.com/Masterminds/squirrel"
|
||||
)
|
||||
|
||||
func (c *client) GetBalance(
|
||||
ctx context.Context,
|
||||
request *dbtypes.BalanceGetRequest,
|
||||
) (*dbtypes.BalanceGetResponse, error) {
|
||||
if request == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
balancesTable = fmt.Sprintf("%s.%s", c.config.Schema, BalancesTableName)
|
||||
)
|
||||
|
||||
getBalance := psql.Select(
|
||||
"raw_balance", "clean_balance",
|
||||
).From(balancesTable).
|
||||
Where(squirrel.Eq{"owner_id": request.OwnerId})
|
||||
|
||||
query, args, err := getBalance.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building get balance query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
row := c.db.QueryRowContext(ctx, query, args...)
|
||||
|
||||
var (
|
||||
balance dbtypes.Balance
|
||||
)
|
||||
|
||||
if err := row.Scan(&balance.RawBalance, &balance.CleanBalance); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: error scanning row: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
return &dbtypes.BalanceGetResponse{
|
||||
Balance: &balance,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *client) createBalance(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
request *dbtypes.BalanceCreateRequest,
|
||||
) error {
|
||||
if request == nil {
|
||||
return fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
balancesTable = fmt.Sprintf("%s.%s", c.config.Schema, BalancesTableName)
|
||||
)
|
||||
|
||||
createBalance := psql.Insert(balancesTable).
|
||||
Columns(
|
||||
"id", "owner_id", "raw_balance", "clean_balance",
|
||||
).
|
||||
Values(
|
||||
request.Id, request.OwnerId, request.RawBalance, request.CleanBalance,
|
||||
)
|
||||
|
||||
query, args, err := createBalance.ToSql()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error building 'create balance' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
res, err := driver.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error executing 'create balance' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error getting rows affected for 'create balance' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return dberrors.ErrInternal
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
313
internal/database/postgres/bank_account.go
Normal file
313
internal/database/postgres/bank_account.go
Normal file
@@ -0,0 +1,313 @@
|
||||
package pgdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
dberrors "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/errors"
|
||||
dbtypes "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/types"
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ALERT: intergrate safety checks
|
||||
|
||||
func (c *client) GetBankAccountList(
|
||||
ctx context.Context,
|
||||
request *dbtypes.BankAccountListGetRequest,
|
||||
) (*dbtypes.BankAccountListGetResponse, error) {
|
||||
if request == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
bankAccountsTable = fmt.Sprintf("%s.%s", c.config.Schema, BankAccountsTableName)
|
||||
)
|
||||
|
||||
getAccounts := psql.Select(
|
||||
"id", "owner_id", "account_number", "bank_name", "bik",
|
||||
"correspondent_account", "is_primary", "created_at", "updated_at",
|
||||
).From(bankAccountsTable).
|
||||
Where(squirrel.Eq{"owner_id": request.OwnerId})
|
||||
|
||||
query, args, err := getAccounts.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building get bank accounts query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rows, err := c.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error executing get bank accounts query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
var accounts []dbtypes.BankAccount
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
accountNumber, bankName, bik, correspondentAccount sql.NullString
|
||||
bankAccount dbtypes.BankAccount
|
||||
)
|
||||
|
||||
if err := rows.Scan(
|
||||
&bankAccount.Id, &bankAccount.OwnerId, &accountNumber, &bankName, &bik,
|
||||
&correspondentAccount, &bankAccount.IsPrimary, &bankAccount.CreatedAt, &bankAccount.UpdatedAt,
|
||||
); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: error scanning row: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if accountNumber.Valid {
|
||||
bankAccount.AccountName = accountNumber.String
|
||||
}
|
||||
|
||||
if bankName.Valid {
|
||||
bankAccount.BankName = bankName.String
|
||||
}
|
||||
|
||||
if bik.Valid {
|
||||
bankAccount.Bik = bik.String
|
||||
}
|
||||
|
||||
if correspondentAccount.Valid {
|
||||
bankAccount.CorrespondentAccount = correspondentAccount.String
|
||||
}
|
||||
|
||||
accounts = append(accounts, bankAccount)
|
||||
}
|
||||
|
||||
return &dbtypes.BankAccountListGetResponse{
|
||||
BankAccounts: accounts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *client) CreateBankAccount(
|
||||
ctx context.Context,
|
||||
request *dbtypes.BankAccountCreateRequest,
|
||||
) (*dbtypes.BankAccountCreateResponse, error) {
|
||||
if request == nil {
|
||||
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
bankAccountsTable = fmt.Sprintf("%s.%s", c.config.Schema, BankAccountsTableName)
|
||||
)
|
||||
|
||||
// TODO: use normal uuid after DB reengineering
|
||||
bankAccountId := fmt.Sprintf("%sBNK", strings.ReplaceAll(uuid.NewString(), "-", ""))
|
||||
|
||||
createBankAccount := psql.Insert(bankAccountsTable).
|
||||
Columns(
|
||||
"id", "owner_id", "account_number", "bank_name", "bik", "correspondent_account", "is_primary",
|
||||
).
|
||||
Values(
|
||||
bankAccountId, request.OwnerId, request.AccountNumber, request.BankName,
|
||||
request.Bik, request.CorrespondentAccount, request.IsPrimary,
|
||||
)
|
||||
|
||||
query, args, err := createBankAccount.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building create bank account query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
res, err := c.db.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error executing create bank account query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error getting rows affected for create bank account query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return nil, dberrors.ErrInternal
|
||||
}
|
||||
|
||||
return &dbtypes.BankAccountCreateResponse{
|
||||
Id: bankAccountId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *client) UpdateBankAccount(
|
||||
ctx context.Context,
|
||||
request *dbtypes.BankAccountUpdateRequest,
|
||||
) (*dbtypes.BankAccountUpdateResponse, error) {
|
||||
if request == nil {
|
||||
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
tx, err := c.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error starting transaction: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
result, err := c.updateBankAccount(ctx, tx, request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error updating bank account: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, fmt.Errorf("%w: error committing transaction: %w", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *client) updateBankAccount(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
request *dbtypes.BankAccountUpdateRequest,
|
||||
) (*dbtypes.BankAccountUpdateResponse, error) {
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
bankAccountsTable = fmt.Sprintf("%s.%s", c.config.Schema, BankAccountsTableName)
|
||||
)
|
||||
|
||||
updateAccount := psql.Update(bankAccountsTable).
|
||||
Where(squirrel.Eq{"id": request.Id})
|
||||
|
||||
if request.AccountNumber != nil {
|
||||
updateAccount = updateAccount.Set("account_number", *request.AccountNumber)
|
||||
}
|
||||
|
||||
// TODO: uncomment when DB supports it
|
||||
// if request.AccountName != nil {
|
||||
// updateAccount = updateAccount.Set("account_name", *request.AccountName)
|
||||
// }
|
||||
|
||||
if request.BankName != nil {
|
||||
updateAccount = updateAccount.Set("bank_name", *request.BankName)
|
||||
}
|
||||
|
||||
if request.Bik != nil {
|
||||
updateAccount = updateAccount.Set("bik", *request.Bik)
|
||||
}
|
||||
|
||||
if request.CorrespondentAccount != nil {
|
||||
updateAccount = updateAccount.Set("correspondent_account", *request.CorrespondentAccount)
|
||||
}
|
||||
|
||||
if request.IsPrimary != nil {
|
||||
if *request.IsPrimary {
|
||||
if err := c.unmarkPrimaryBankAccount(ctx, driver, request.Id); err != nil {
|
||||
return nil, fmt.Errorf("error unmarking currently primary bank account: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// QnA: what if the update makes all BAs non-primary?
|
||||
updateAccount = updateAccount.Set("is_primary", *request.IsPrimary)
|
||||
}
|
||||
|
||||
query, args, err := updateAccount.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building 'update bank account' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
res, err := driver.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error executing 'update bank account' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error getting rows affected for 'update bank account' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return nil, dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return &dbtypes.BankAccountUpdateResponse{}, nil
|
||||
}
|
||||
|
||||
func (c *client) unmarkPrimaryBankAccount(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
bankAccountId string,
|
||||
) error {
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
bankAccountsTable = fmt.Sprintf("%s.%s", c.config.Schema, BankAccountsTableName)
|
||||
)
|
||||
|
||||
getOwnerId := psql.Select(
|
||||
"owner_id", "is_primary",
|
||||
).From(bankAccountsTable).
|
||||
Where(squirrel.Eq{"id": bankAccountId}).
|
||||
Suffix("FOR UPDATE")
|
||||
|
||||
query, args, err := getOwnerId.ToSql()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error building 'get owner id of bank account' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
row := driver.QueryRowContext(ctx, query, args...)
|
||||
|
||||
var (
|
||||
ownerId string
|
||||
isPrimary bool
|
||||
)
|
||||
|
||||
if err := row.Scan(&ownerId, &isPrimary); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: error scanning row for 'get owner id of bank account' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if isPrimary {
|
||||
return nil
|
||||
}
|
||||
|
||||
unmarkPrimaryBA := psql.Update(bankAccountsTable).
|
||||
Set("is_primary", false).
|
||||
Where(squirrel.Eq{
|
||||
"owner_id": ownerId,
|
||||
"is_primary": true,
|
||||
})
|
||||
|
||||
query, args, err = unmarkPrimaryBA.ToSql()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error building 'unmark primary bank account' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
res, err := driver.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error executing 'unmark primary bank account' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error getting rows affected for 'unmark primary bank account' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return fmt.Errorf("%w: error unmarking primary bank account: no rows affected", dberrors.ErrInternal)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) DeleteBankAccount(
|
||||
ctx context.Context,
|
||||
request *dbtypes.BankAccountDeleteRequest,
|
||||
) (*dbtypes.BankAccountDeleteResponse, error) {
|
||||
return nil, dberrors.ErrUnimplemented
|
||||
}
|
||||
107
internal/database/postgres/client.go
Normal file
107
internal/database/postgres/client.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package pgdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// ะะฝัะตััะตะนั ะฟะตัะตะดะฐะตััั ะฒ ะฝะตัะบัะฟะพััะธััะตะผัะต ััะฝะบัะธะธ
|
||||
// ะฝัะถะตะฝ ะดะปั ัะพะณะพ, ััะพะฑั ะฝะต ะทะฐะฒะธัะธะปะพ ะพั ัะพะณะพ, ััะพ ะฟะตัะตะดะฐัะผ
|
||||
// ััะฐะฝะทะฐัะธั ะธะปะธ ะพะฑััะฝะพะต ัะพะตะดะธะฝะตะฝะธะต
|
||||
type Driver interface {
|
||||
// QueryContext executes a query that returns rows, typically a SELECT.
|
||||
// The args are for any placeholder parameters in the query.
|
||||
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
|
||||
|
||||
// QueryRowContext executes a query that is expected to return at most one row.
|
||||
// QueryRowContext always returns a non-nil value. Errors are deferred until
|
||||
// [Row]'s Scan method is called.
|
||||
// If the query selects no rows, the [*Row.Scan] will return [ErrNoRows].
|
||||
// Otherwise, [*Row.Scan] scans the first selected row and discards
|
||||
// the rest.
|
||||
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
|
||||
|
||||
// ExecContext executes a query without returning any rows.
|
||||
// The args are for any placeholder parameters in the query.
|
||||
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
|
||||
}
|
||||
|
||||
type PostgresConfig struct {
|
||||
Host string
|
||||
Port uint16
|
||||
Username string
|
||||
Password string
|
||||
Database string
|
||||
Schema string
|
||||
SSLMode string
|
||||
SSLRootCert string
|
||||
}
|
||||
|
||||
type client struct {
|
||||
config PostgresConfig
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
func NewClient(cfg PostgresConfig) (*client, error) {
|
||||
rootCertPool := x509.NewCertPool()
|
||||
|
||||
caCert, err := os.ReadFile(cfg.SSLRootCert)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read CA cert: %w", err)
|
||||
}
|
||||
|
||||
if ok := rootCertPool.AppendCertsFromPEM(caCert); !ok {
|
||||
return nil, fmt.Errorf("failed to append CA cert to pool")
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
RootCAs: rootCertPool,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
//nolint:gosec // TODO: set server name
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
|
||||
config, err := pgx.ParseConfig("")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse [empty] config: %w", err)
|
||||
}
|
||||
|
||||
config.Host = cfg.Host
|
||||
config.Port = cfg.Port
|
||||
config.Database = cfg.Database
|
||||
config.User = cfg.Username
|
||||
config.Password = cfg.Password
|
||||
config.TLSConfig = tlsConfig
|
||||
config.RuntimeParams = map[string]string{"sslmode": cfg.SSLMode}
|
||||
|
||||
db := sqlx.NewDb(stdlib.OpenDB(*config), "pgx")
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("failed to ping postgres: %w", err)
|
||||
}
|
||||
|
||||
return &client{
|
||||
config: cfg,
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func countOffset(page, pageSize uint64) uint64 {
|
||||
if page == 0 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
if pageSize == 0 {
|
||||
pageSize = DefaultPaginationPageSize
|
||||
}
|
||||
|
||||
return (page - 1) * pageSize
|
||||
}
|
||||
506
internal/database/postgres/company.go
Normal file
506
internal/database/postgres/company.go
Normal file
@@ -0,0 +1,506 @@
|
||||
package pgdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
dberrors "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/errors"
|
||||
dbtypes "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/types"
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/google/uuid"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
func CompanyModerationStatusIdToString(status int32) dbtypes.CompanyModerationStatus {
|
||||
switch status {
|
||||
case 0:
|
||||
return dbtypes.CompanyModerationStatusPending
|
||||
case 1:
|
||||
return dbtypes.CompanyModerationStatusApproved
|
||||
case 2:
|
||||
return dbtypes.CompanyModerationStatusRejected
|
||||
case 3:
|
||||
return dbtypes.CompanyModerationStatusNew
|
||||
default:
|
||||
return dbtypes.CompanyModerationStatusNew
|
||||
}
|
||||
}
|
||||
|
||||
func CompanyModerationStatusStringToId(status dbtypes.CompanyModerationStatus) int32 {
|
||||
switch status {
|
||||
case dbtypes.CompanyModerationStatusPending:
|
||||
return 0
|
||||
case dbtypes.CompanyModerationStatusApproved:
|
||||
return 1
|
||||
case dbtypes.CompanyModerationStatusRejected:
|
||||
return 2
|
||||
case dbtypes.CompanyModerationStatusNew:
|
||||
return 3
|
||||
default:
|
||||
return 3
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocognit // TODO: refactor
|
||||
func (c *client) GetCompanyList(
|
||||
ctx context.Context,
|
||||
request *dbtypes.CompanyListGetRequest,
|
||||
) (*dbtypes.CompanyListGetResponse, error) {
|
||||
if request == nil {
|
||||
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
companiesTable = fmt.Sprintf("%s.%s", c.config.Schema, CompaniesTableName)
|
||||
)
|
||||
|
||||
getComList := psql.Select(
|
||||
"id", "uid", "name", "legal_person", "description", "website",
|
||||
"physical_address", "legal_address", "inn", "is_active", // TODO: add KPP when DB supports it
|
||||
"has_moderation_ticket", "staff", "metadata", "additional_fields_tmpl",
|
||||
).
|
||||
From(companiesTable).
|
||||
Where(squirrel.Eq{"uid": request.Id})
|
||||
|
||||
query, args, err := getComList.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building get distributor company list query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rows, err := c.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error executing get distributor company list query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
var res dbtypes.CompanyListGetResponse
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
name, legalPerson, description, website, physicalAddress sql.NullString
|
||||
legalAddress, inn, metadata, additionalFieldsTmpl sql.NullString
|
||||
staff pq.StringArray
|
||||
company dbtypes.Company
|
||||
)
|
||||
|
||||
if err := rows.Scan(
|
||||
&company.Id, &company.OwnerId, &name, &legalPerson, &description, &website,
|
||||
&physicalAddress, &legalAddress, &inn, &company.IsActive,
|
||||
&company.HasModerationTicket, &staff, &metadata, &additionalFieldsTmpl,
|
||||
); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: error scanning row: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if name.Valid {
|
||||
company.Name = &name.String
|
||||
}
|
||||
|
||||
if legalPerson.Valid {
|
||||
company.LegalPerson = &legalPerson.String
|
||||
}
|
||||
|
||||
if description.Valid {
|
||||
company.Description = &description.String
|
||||
}
|
||||
|
||||
if website.Valid {
|
||||
company.Website = &website.String
|
||||
}
|
||||
|
||||
if physicalAddress.Valid {
|
||||
company.PhysicalAddress = &physicalAddress.String
|
||||
}
|
||||
|
||||
if legalAddress.Valid {
|
||||
company.LegalAddress = &legalAddress.String
|
||||
}
|
||||
|
||||
if inn.Valid {
|
||||
company.Inn = &inn.String
|
||||
}
|
||||
|
||||
company.Staff = staff
|
||||
|
||||
if metadata.Valid {
|
||||
company.Metadata = &metadata.String
|
||||
}
|
||||
|
||||
if additionalFieldsTmpl.Valid {
|
||||
company.ExtraFieldsTemplate = &additionalFieldsTmpl.String
|
||||
}
|
||||
|
||||
res.Companies = append(res.Companies, company)
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (c *client) GetCompanyById(
|
||||
ctx context.Context,
|
||||
request *dbtypes.CompanyByIdGetRequest,
|
||||
) (*dbtypes.CompanyByIdGetResponse, error) {
|
||||
if request == nil {
|
||||
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
companiesTable = fmt.Sprintf("%s.%s", c.config.Schema, CompaniesTableName)
|
||||
)
|
||||
|
||||
getComList := psql.Select(
|
||||
"id", "uid", "name", "legal_person", "description", "website",
|
||||
"physical_address", "legal_address", "inn", "is_active", // TODO: add KPP when DB supports it
|
||||
"has_moderation_ticket", "staff", "metadata", "additional_fields_tmpl",
|
||||
).
|
||||
From(companiesTable).
|
||||
Where(squirrel.And{
|
||||
squirrel.Eq{"id": request.CompanyId},
|
||||
squirrel.Eq{"uid": request.Id},
|
||||
})
|
||||
|
||||
query, args, err := getComList.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building get distributor company list query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
row := c.db.QueryRowContext(ctx, query, args...)
|
||||
|
||||
var (
|
||||
name, legalPerson, description, website, physicalAddress sql.NullString
|
||||
legalAddress, inn, metadata, additionalFieldsTmpl sql.NullString
|
||||
staff pq.StringArray
|
||||
company dbtypes.Company
|
||||
)
|
||||
|
||||
if err := row.Scan(
|
||||
&company.Id, &company.OwnerId, &name, &legalPerson, &description, &website,
|
||||
&physicalAddress, &legalAddress, &inn, &company.IsActive,
|
||||
&company.HasModerationTicket, &staff, &metadata, &additionalFieldsTmpl,
|
||||
); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: error scanning row: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if name.Valid {
|
||||
company.Name = &name.String
|
||||
}
|
||||
|
||||
if legalPerson.Valid {
|
||||
company.LegalPerson = &legalPerson.String
|
||||
}
|
||||
|
||||
if description.Valid {
|
||||
company.Description = &description.String
|
||||
}
|
||||
|
||||
if website.Valid {
|
||||
company.Website = &website.String
|
||||
}
|
||||
|
||||
if physicalAddress.Valid {
|
||||
company.PhysicalAddress = &physicalAddress.String
|
||||
}
|
||||
|
||||
if legalAddress.Valid {
|
||||
company.LegalAddress = &legalAddress.String
|
||||
}
|
||||
|
||||
if inn.Valid {
|
||||
company.Inn = &inn.String
|
||||
}
|
||||
|
||||
company.Staff = staff
|
||||
|
||||
if metadata.Valid {
|
||||
company.Metadata = &metadata.String
|
||||
}
|
||||
|
||||
if additionalFieldsTmpl.Valid {
|
||||
company.ExtraFieldsTemplate = &additionalFieldsTmpl.String
|
||||
}
|
||||
|
||||
return &dbtypes.CompanyByIdGetResponse{Company: company}, nil
|
||||
}
|
||||
|
||||
func (c *client) CreateCompany(
|
||||
ctx context.Context,
|
||||
request *dbtypes.CompanyCreateRequest,
|
||||
) (*dbtypes.CompanyCreateResponse, error) {
|
||||
if request == nil {
|
||||
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
tx, err := c.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error starting transaction: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
res, err := c.createCompany(ctx, tx, request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating company: %w", err)
|
||||
}
|
||||
|
||||
if err := c.createCompanyValidationTicket(ctx, tx, res.Id, request); err != nil {
|
||||
return nil, fmt.Errorf("error creating company validation ticket: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, fmt.Errorf("error committing transaction: %w", err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *client) createCompany(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
request *dbtypes.CompanyCreateRequest,
|
||||
) (*dbtypes.CompanyCreateResponse, error) {
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
companyTable = fmt.Sprintf("%s.%s", c.config.Schema, CompaniesTableName)
|
||||
)
|
||||
|
||||
// TODO: use normal uuid after DB reengineering
|
||||
comId := fmt.Sprintf("%sCOM", strings.ReplaceAll(uuid.NewString(), "-", ""))
|
||||
|
||||
createCompany := psql.Insert(companyTable).
|
||||
Columns(
|
||||
"id", "uid", "is_active", "has_moderation_ticket", "metadata", "additional_fields_tmpl",
|
||||
).
|
||||
Values(
|
||||
comId, request.OwnerId, false, true, request.Metadata, request.ExtraFieldsTemplate,
|
||||
)
|
||||
|
||||
query, args, err := createCompany.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building create company query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
res, err := driver.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error executing create company query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error getting rows affected for create company query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return nil, dberrors.ErrInternal
|
||||
}
|
||||
|
||||
return &dbtypes.CompanyCreateResponse{Id: comId}, nil
|
||||
}
|
||||
|
||||
func (c *client) createCompanyValidationTicket(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
companyId string,
|
||||
request *dbtypes.CompanyCreateRequest,
|
||||
) error {
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
comValTable = fmt.Sprintf("%s.%s", c.config.Schema, CompanyValidationTicketsTableName)
|
||||
)
|
||||
|
||||
var (
|
||||
ticketId = fmt.Sprintf("%sTCK", strings.ReplaceAll(uuid.NewString(), "-", ""))
|
||||
)
|
||||
|
||||
createCompany := psql.Insert(comValTable).
|
||||
Columns(
|
||||
"id", "company_id", "name", "legal_person", "description", "website",
|
||||
"physical_address", "legal_address", "inn", // TODO: add KPP when DB supports it
|
||||
"staff", "status",
|
||||
).
|
||||
Values(
|
||||
ticketId, companyId, request.Name, request.LegalPerson, request.Description, request.Website,
|
||||
request.PhysicalAddress, request.LegalAddress, request.Inn,
|
||||
request.Staff, CompanyModerationStatusStringToId(dbtypes.CompanyModerationStatusPending), // TODO: switch to status "NEW"
|
||||
)
|
||||
|
||||
query, args, err := createCompany.ToSql()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error building create company moderation ticket query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
res, err := driver.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error executing create company moderation ticket query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error getting rows affected for create company moderation ticket query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return dberrors.ErrInternal
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) UpdateCompany(
|
||||
ctx context.Context,
|
||||
request *dbtypes.CompanyUpdateRequest,
|
||||
) (*dbtypes.CompanyUpdateResponse, error) {
|
||||
if request == nil {
|
||||
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
tx, err := c.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error starting transaction: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
res, err := c.updateCompany(ctx, tx, request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error updating company: %w", err)
|
||||
}
|
||||
|
||||
if err := c.updateCompanyValidationTicket(ctx, tx, request.Id, request); err != nil {
|
||||
return nil, fmt.Errorf("error updating company validation ticket: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, fmt.Errorf("error committing transaction: %w", err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *client) updateCompany(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
request *dbtypes.CompanyUpdateRequest,
|
||||
) (*dbtypes.CompanyUpdateResponse, error) {
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
companyTable = fmt.Sprintf("%s.%s", c.config.Schema, CompaniesTableName)
|
||||
)
|
||||
|
||||
updateCompany := psql.Update(companyTable).
|
||||
SetMap(map[string]any{
|
||||
"is_active": false,
|
||||
"has_moderation_ticket": true,
|
||||
"metadata": request.Metadata,
|
||||
"additional_fields_tmpl": request.ExtraFields,
|
||||
}).
|
||||
Where(squirrel.Eq{"id": request.Id})
|
||||
|
||||
query, args, err := updateCompany.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building update company query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
res, err := driver.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error executing update company query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error getting rows affected for update company query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return nil, dberrors.ErrInternal
|
||||
}
|
||||
|
||||
return &dbtypes.CompanyUpdateResponse{}, nil
|
||||
}
|
||||
|
||||
// NOTE: do we believe that every company has a moderation ticket?
|
||||
func (c *client) updateCompanyValidationTicket(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
companyId string,
|
||||
request *dbtypes.CompanyUpdateRequest,
|
||||
) error {
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
comValTable = fmt.Sprintf("%s.%s", c.config.Schema, CompanyValidationTicketsTableName)
|
||||
)
|
||||
|
||||
updateCompany := psql.Update(comValTable).
|
||||
Where(squirrel.Eq{"company_id": companyId})
|
||||
|
||||
if request.Name != nil {
|
||||
updateCompany = updateCompany.Set("name", *request.Name)
|
||||
}
|
||||
|
||||
if request.LegalPerson != nil {
|
||||
updateCompany = updateCompany.Set("legal_person", *request.LegalPerson)
|
||||
}
|
||||
|
||||
if request.Description != nil {
|
||||
updateCompany = updateCompany.Set("description", *request.Description)
|
||||
}
|
||||
|
||||
if request.Website != nil {
|
||||
updateCompany = updateCompany.Set("website", *request.Website)
|
||||
}
|
||||
|
||||
if request.PhysicalAddress != nil {
|
||||
updateCompany = updateCompany.Set("physical_address", *request.PhysicalAddress)
|
||||
}
|
||||
|
||||
if request.LegalAddress != nil {
|
||||
updateCompany = updateCompany.Set("legal_address", *request.LegalAddress)
|
||||
}
|
||||
|
||||
if request.Inn != nil {
|
||||
updateCompany = updateCompany.Set("inn", *request.Inn)
|
||||
}
|
||||
|
||||
if len(request.Staff) > 0 {
|
||||
updateCompany = updateCompany.Set("staff", request.Staff)
|
||||
}
|
||||
|
||||
query, args, err := updateCompany.ToSql()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error building update company moderation ticket query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
res, err := driver.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error executing update company moderation ticket query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error getting rows affected for update company moderation ticket query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return dberrors.ErrInternal
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
23
internal/database/postgres/domain.go
Normal file
23
internal/database/postgres/domain.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package pgdb
|
||||
|
||||
const (
|
||||
BalancesTableName = "balances"
|
||||
BankAccountsTableName = "company_bank_accounts"
|
||||
CompaniesTableName = "company"
|
||||
CompanyValidationTicketsTableName = "company_validation"
|
||||
SubmissionsTableName = "submission"
|
||||
TransactionsTableName = "transactions"
|
||||
VacanciesTableName = "vacancy"
|
||||
VacancyCandidatesTableName = "vacancy_candidates"
|
||||
VacancyDescriptionHistoryTableName = "vacancy_moderation_descriptions"
|
||||
UsersTableName = "client"
|
||||
UserValidationTableName = "client_validation"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultPaginationPageSize = 20
|
||||
)
|
||||
|
||||
const (
|
||||
PGErrUniqueViolation = "23505"
|
||||
)
|
||||
128
internal/database/postgres/integration.go
Normal file
128
internal/database/postgres/integration.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package pgdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
dberrors "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/errors"
|
||||
dbtypes "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/types"
|
||||
"github.com/Masterminds/squirrel"
|
||||
)
|
||||
|
||||
func (c *client) GetVacancyIntegrationInfoById(
|
||||
ctx context.Context,
|
||||
vacancyId string,
|
||||
) (string, *dbtypes.VacancyExtraFieldsTemplate, error) {
|
||||
return c.getVacancyIntegrationInfoById(ctx, c.db, vacancyId)
|
||||
}
|
||||
|
||||
func (c *client) getVacancyIntegrationInfoById(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
vacancyId string,
|
||||
) (string, *dbtypes.VacancyExtraFieldsTemplate, error) {
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
vacancyTable = fmt.Sprintf("%s.%s", c.config.Schema, VacanciesTableName)
|
||||
)
|
||||
|
||||
getCompanyIdQuery := psql.Select("company_id", "additional_fields").
|
||||
From(vacancyTable).
|
||||
Where(squirrel.Eq{"id": vacancyId})
|
||||
|
||||
query, args, err := getCompanyIdQuery.ToSql()
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("%w: error building query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
row := driver.QueryRowContext(ctx, query, args...)
|
||||
|
||||
var (
|
||||
companyId string
|
||||
fieldsString sql.NullString
|
||||
fields dbtypes.VacancyExtraFieldsTemplate
|
||||
)
|
||||
|
||||
if err := row.Scan(&companyId, &fieldsString); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return "", nil, dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return "", nil, fmt.Errorf("%w: error scanning row: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if fieldsString.Valid {
|
||||
if err := json.Unmarshal([]byte(fieldsString.String), &fields); err != nil {
|
||||
return "", nil, fmt.Errorf("%w: error unmarshalling fields: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
}
|
||||
|
||||
return companyId, &fields, nil
|
||||
}
|
||||
|
||||
func (c *client) GetCompanyMetadataById(
|
||||
ctx context.Context,
|
||||
companyId string,
|
||||
) (*dbtypes.CompanyMetadata, *dbtypes.CompanyExtraFieldsTemplate, error) {
|
||||
return c.getCompanyMetadataById(ctx, c.db, companyId)
|
||||
}
|
||||
|
||||
func (c *client) getCompanyMetadataById(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
companyId string,
|
||||
) (*dbtypes.CompanyMetadata, *dbtypes.CompanyExtraFieldsTemplate, error) {
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
companyTable = fmt.Sprintf("%s.%s", c.config.Schema, CompaniesTableName)
|
||||
)
|
||||
|
||||
getCompanyMetadataQuery := psql.Select(
|
||||
"metadata",
|
||||
"additional_fields_tmpl",
|
||||
).
|
||||
From(companyTable).
|
||||
Where(squirrel.Eq{"id": companyId})
|
||||
|
||||
query, args, err := getCompanyMetadataQuery.ToSql()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("%w: error building query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
row := driver.QueryRowContext(ctx, query, args...)
|
||||
|
||||
var (
|
||||
metadataString sql.NullString
|
||||
metadata dbtypes.CompanyMetadata
|
||||
|
||||
fieldsTemplateString sql.NullString
|
||||
fieldsTemplate dbtypes.CompanyExtraFieldsTemplate
|
||||
)
|
||||
|
||||
if err := row.Scan(&metadataString, &fieldsTemplateString); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil, dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return nil, nil, fmt.Errorf("%w: error scanning row: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if metadataString.Valid {
|
||||
if err := json.Unmarshal([]byte(metadataString.String), &metadata); err != nil {
|
||||
return nil, nil, fmt.Errorf("%w: error unmarshalling metadata: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
}
|
||||
|
||||
if fieldsTemplateString.Valid {
|
||||
if err := json.Unmarshal([]byte(fieldsTemplateString.String), &fieldsTemplate); err != nil {
|
||||
return nil, nil, fmt.Errorf("%w: error unmarshalling fields template: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
}
|
||||
|
||||
return &metadata, &fieldsTemplate, nil
|
||||
}
|
||||
130
internal/database/postgres/profile.go
Normal file
130
internal/database/postgres/profile.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package pgdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
dberrors "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/errors"
|
||||
dbtypes "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/types"
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
func (c *client) GetProfileById(
|
||||
ctx context.Context,
|
||||
request *dbtypes.ProfileGetRequest,
|
||||
) (*dbtypes.ProfileGetResponse, error) {
|
||||
if request == nil {
|
||||
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
usersTable = fmt.Sprintf("%s.%s", c.config.Schema, UsersTableName)
|
||||
)
|
||||
|
||||
getProfile := psql.Select(
|
||||
"uid", "name", "phone", "email",
|
||||
).From(usersTable).
|
||||
Where(squirrel.Eq{"uid": request.Id})
|
||||
|
||||
query, args, err := getProfile.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building get profile query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
row := c.db.QueryRowContext(ctx, query, args...)
|
||||
|
||||
var (
|
||||
name, phoneNumber, email sql.NullString
|
||||
profile dbtypes.Profile
|
||||
)
|
||||
|
||||
if err := row.Scan(&profile.Id, &name, &phoneNumber, &email); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: error scanning row: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if name.Valid {
|
||||
profile.Name = name.String
|
||||
}
|
||||
|
||||
if phoneNumber.Valid {
|
||||
profile.PhoneNumber = phoneNumber.String
|
||||
}
|
||||
|
||||
if email.Valid {
|
||||
profile.Email = email.String
|
||||
}
|
||||
|
||||
return &dbtypes.ProfileGetResponse{
|
||||
Profile: &profile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *client) UpdateProfile(
|
||||
ctx context.Context,
|
||||
request *dbtypes.ProfileUpdateRequest,
|
||||
) (*dbtypes.ProfileUpdateResponse, error) {
|
||||
if request == nil {
|
||||
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
if request.Name == nil && request.PhoneNumber == nil && request.Email == nil {
|
||||
return nil, fmt.Errorf("%w: nothing to update", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
usersTable = fmt.Sprintf("%s.%s", c.config.Schema, UsersTableName)
|
||||
)
|
||||
|
||||
updateProfile := psql.Update(usersTable).
|
||||
Where(squirrel.Eq{"uid": request.Id})
|
||||
|
||||
if request.Name != nil {
|
||||
updateProfile = updateProfile.Set("name", *request.Name)
|
||||
}
|
||||
|
||||
if request.PhoneNumber != nil {
|
||||
updateProfile = updateProfile.Set("phone", *request.PhoneNumber)
|
||||
}
|
||||
|
||||
if request.Email != nil {
|
||||
updateProfile = updateProfile.Set("email", *request.Email)
|
||||
}
|
||||
|
||||
query, args, err := updateProfile.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building update profile query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
res, err := c.db.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
if pqErr, ok := err.(*pq.Error); ok {
|
||||
if pqErr.Code == PGErrUniqueViolation {
|
||||
return nil, dberrors.ErrConflict
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: error executing update profile query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error getting rows affected for update profile query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return nil, dberrors.ErrInternal
|
||||
}
|
||||
|
||||
return &dbtypes.ProfileUpdateResponse{}, nil
|
||||
}
|
||||
494
internal/database/postgres/submission.go
Normal file
494
internal/database/postgres/submission.go
Normal file
@@ -0,0 +1,494 @@
|
||||
package pgdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
dberrors "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/errors"
|
||||
dbtypes "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/types"
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func SubmissionStatusIdToString(status int32) dbtypes.SubmissionStatus {
|
||||
switch status {
|
||||
case 0:
|
||||
return dbtypes.SubStatusUnspecified
|
||||
case 1:
|
||||
return dbtypes.SubStatusNew
|
||||
case 2:
|
||||
return dbtypes.SubStatusPending
|
||||
case 3:
|
||||
return dbtypes.SubStatusOnInterview
|
||||
case 4:
|
||||
return dbtypes.SubStatusRejected
|
||||
case 5:
|
||||
return dbtypes.SubStatusCancelled
|
||||
case 6:
|
||||
return dbtypes.SubStatusApproved
|
||||
default:
|
||||
return dbtypes.SubStatusUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
func SubmissionStatusStringToId(status dbtypes.SubmissionStatus) int32 {
|
||||
switch status {
|
||||
case dbtypes.SubStatusUnspecified:
|
||||
return 0
|
||||
case dbtypes.SubStatusNew:
|
||||
return 1
|
||||
case dbtypes.SubStatusPending:
|
||||
return 2
|
||||
case dbtypes.SubStatusOnInterview:
|
||||
return 3
|
||||
case dbtypes.SubStatusRejected:
|
||||
return 4
|
||||
case dbtypes.SubStatusCancelled:
|
||||
return 5
|
||||
case dbtypes.SubStatusApproved:
|
||||
return 6
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocognit // not so hard
|
||||
func (c *client) GetSubmissionList(
|
||||
ctx context.Context,
|
||||
request *dbtypes.SubmissionListGetRequest,
|
||||
) (*dbtypes.SubmissionListGetResponse, error) {
|
||||
if request == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
submissionsTable = fmt.Sprintf("%s.%s", c.config.Schema, SubmissionsTableName)
|
||||
)
|
||||
|
||||
getSubmissions := psql.Select(
|
||||
"id", "uid", "vacancy_id", "submission_status_id",
|
||||
"cv", "name", "phone", "email", "birthday", "info",
|
||||
).From(submissionsTable).
|
||||
Limit(request.PageSize).
|
||||
Offset(countOffset(request.Page, request.PageSize))
|
||||
|
||||
if request.Filters != nil {
|
||||
if request.Filters.AgentId != nil {
|
||||
getSubmissions = getSubmissions.Where(squirrel.Eq{"uid": *request.Filters.AgentId})
|
||||
}
|
||||
|
||||
if request.Filters.VacancyId != nil {
|
||||
getSubmissions = getSubmissions.Where(squirrel.Eq{"vacancy_id": *request.Filters.VacancyId})
|
||||
}
|
||||
|
||||
if request.Filters.Status != nil && *request.Filters.Status != dbtypes.SubStatusUnspecified {
|
||||
getSubmissions = getSubmissions.Where(squirrel.Eq{"submission_status_id": SubmissionStatusStringToId(*request.Filters.Status)})
|
||||
}
|
||||
}
|
||||
|
||||
query, args, err := getSubmissions.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building get submissions query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rows, err := c.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error executing get submissions query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
submissionsList := &dbtypes.SubmissionListGetResponse{
|
||||
Submissions: make([]dbtypes.Submission, 0, request.PageSize),
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
cvLink, info sql.NullString
|
||||
statusId int32
|
||||
candidateName string
|
||||
|
||||
sub = dbtypes.Submission{
|
||||
AgentInfo: new(dbtypes.AgentInfo),
|
||||
VacancyInfo: new(dbtypes.VacancyInfo),
|
||||
CandidateInfo: new(dbtypes.CandidateInfo),
|
||||
}
|
||||
)
|
||||
|
||||
if err := rows.Scan(
|
||||
&sub.Id, &sub.AgentInfo.Id, &sub.VacancyInfo.Id, &statusId,
|
||||
&cvLink, &candidateName, &sub.CandidateInfo.PhoneNumber, &sub.CandidateInfo.Email,
|
||||
&sub.CandidateInfo.Birthday, &info,
|
||||
); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: error scanning row: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
sub.CandidateInfo.LastName, sub.CandidateInfo.FirstName, sub.CandidateInfo.MiddleName = splitName(candidateName)
|
||||
|
||||
if cvLink.Valid {
|
||||
sub.CandidateInfo.CvLink = &cvLink.String
|
||||
}
|
||||
|
||||
if info.Valid {
|
||||
sub.CandidateInfo.Resume = &info.String
|
||||
}
|
||||
|
||||
sub.Status = SubmissionStatusIdToString(statusId)
|
||||
|
||||
submissionsList.Submissions = append(submissionsList.Submissions, sub)
|
||||
}
|
||||
|
||||
return submissionsList, nil
|
||||
}
|
||||
|
||||
func splitName(name string) (string, string, string) {
|
||||
var (
|
||||
parts = strings.SplitN(name, " ", 3)
|
||||
lastName, firstName, middleName string
|
||||
)
|
||||
|
||||
if len(parts) > 0 {
|
||||
lastName = parts[0]
|
||||
}
|
||||
|
||||
if len(parts) > 1 {
|
||||
firstName = parts[1]
|
||||
}
|
||||
|
||||
if len(parts) > 2 {
|
||||
middleName = parts[2]
|
||||
}
|
||||
|
||||
return lastName, firstName, middleName
|
||||
}
|
||||
|
||||
func (c *client) CreateSubmission(
|
||||
ctx context.Context,
|
||||
request *dbtypes.SubmissionCreateRequest,
|
||||
) (*dbtypes.SubmissionCreateResponse, error) {
|
||||
if request == nil {
|
||||
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
if request.CandidateInfo == nil {
|
||||
return nil, fmt.Errorf("%w: request.CandidateInfo is nil", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
submissionsTable = fmt.Sprintf("%s.%s", c.config.Schema, SubmissionsTableName)
|
||||
)
|
||||
|
||||
var (
|
||||
// TODO: use normal uuid after DB reengineering
|
||||
subId = fmt.Sprintf("%sSUB", strings.ReplaceAll(uuid.NewString(), "-", ""))
|
||||
fullName = formatName(request.CandidateInfo.LastName, request.CandidateInfo.FirstName, request.CandidateInfo.MiddleName)
|
||||
)
|
||||
|
||||
createSubmission := psql.Insert(submissionsTable).
|
||||
Columns(
|
||||
"id", "uid", "vacancy_id", "submission_status_id",
|
||||
"cv", "name", "phone", "email", "birthday", "info",
|
||||
).
|
||||
Values(
|
||||
subId, request.AgentId, request.VacancyId, SubmissionStatusStringToId(dbtypes.SubStatusNew),
|
||||
request.CandidateInfo.CvLink, fullName, request.CandidateInfo.PhoneNumber,
|
||||
request.CandidateInfo.Email, request.CandidateInfo.Birthday, request.CandidateInfo.Resume,
|
||||
)
|
||||
|
||||
query, args, err := createSubmission.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building create submission query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
res, err := c.db.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error executing create submission query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error getting rows affected for create submission query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return nil, dberrors.ErrInternal
|
||||
}
|
||||
|
||||
return &dbtypes.SubmissionCreateResponse{
|
||||
Id: subId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NOTE: lastName and firstName are required to be non-empty
|
||||
func formatName(lastName, firstName, middleName string) string {
|
||||
name := fmt.Sprintf("%s %s", lastName, firstName)
|
||||
|
||||
if middleName != "" {
|
||||
name = fmt.Sprintf("%s %s", name, middleName)
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
//nolint:funlen,gocognit // TODO: refactor
|
||||
func (c *client) UpdateSubmissionStatus(
|
||||
ctx context.Context,
|
||||
request *dbtypes.SubmissionStatusUpdateRequest,
|
||||
) (*dbtypes.SubmissionStatusUpdateResponse, error) {
|
||||
if request == nil {
|
||||
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
submissionsTable = fmt.Sprintf("%s.%s", c.config.Schema, SubmissionsTableName)
|
||||
)
|
||||
|
||||
updateSubmissionStatus := psql.Update(submissionsTable).
|
||||
Set("submission_status_id", SubmissionStatusStringToId(request.Status)).
|
||||
Where(squirrel.Eq{"id": request.Id})
|
||||
|
||||
query, args, err := updateSubmissionStatus.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building 'update submission status' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if request.Status != dbtypes.SubStatusApproved {
|
||||
res, err := c.db.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error executing 'update submission status' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error getting rows affected for 'update submission status' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return nil, dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return &dbtypes.SubmissionStatusUpdateResponse{}, nil
|
||||
}
|
||||
|
||||
tx, err := c.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error starting transaction: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
vacancy, agentId, err := c.getSubmissionMetadata(ctx, tx, request.Id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting vacancy info: %w", err)
|
||||
}
|
||||
|
||||
if vacancy.AgentReward == nil {
|
||||
return nil, fmt.Errorf("%w: agent reward is unknown", dberrors.ErrForbidden)
|
||||
}
|
||||
|
||||
if vacancy.IsArchived {
|
||||
return nil, fmt.Errorf("%w: vacancy is closed", dberrors.ErrForbidden)
|
||||
}
|
||||
|
||||
if vacancy.CurrentCandidates+1 > vacancy.RequiredCandidates {
|
||||
return nil, fmt.Errorf("%w: vacancy quota exceeded", dberrors.ErrForbidden)
|
||||
}
|
||||
|
||||
if err := c.addSubmissionCandidate(ctx, tx, vacancy.Id); err != nil {
|
||||
return nil, fmt.Errorf("%w: error adding submission candidate: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
payload := dbtypes.TransactionPayload{
|
||||
Origin: "submission",
|
||||
CompanyId: vacancy.Company.Id,
|
||||
CompanyName: vacancy.Company.Name,
|
||||
VacancyId: vacancy.Id,
|
||||
VacancyName: vacancy.Name,
|
||||
}
|
||||
|
||||
molvaAgentTransId := uuid.NewString()
|
||||
|
||||
if _, err := c.createTransactionWithDriver(ctx, tx, &dbtypes.TransactionCreateRequest{
|
||||
OwnerId: agentId,
|
||||
Amount: int64(*vacancy.AgentReward),
|
||||
Currency: "RUB",
|
||||
RequestId: molvaAgentTransId,
|
||||
Payload: &payload,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("error creating molva -> agent transaction: %w", err)
|
||||
}
|
||||
|
||||
distMolvaTransId := uuid.NewString()
|
||||
|
||||
if _, err := c.createTransactionWithDriver(ctx, tx, &dbtypes.TransactionCreateRequest{
|
||||
OwnerId: vacancy.Company.Id,
|
||||
Amount: -int64(*vacancy.AgentReward),
|
||||
Currency: "RUB",
|
||||
RequestId: distMolvaTransId,
|
||||
Payload: &payload,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("error creating dist -> molva transaction: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, fmt.Errorf("%w: error committing transaction: %w", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
res, err := tx.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error executing 'update submission status' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error getting rows affected for 'update submission status' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return nil, dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return &dbtypes.SubmissionStatusUpdateResponse{}, nil
|
||||
}
|
||||
|
||||
func (c *client) getSubmissionMetadata(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
submissionId string,
|
||||
) (*dbtypes.Vacancy, string, error) {
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
submissionsTable = fmt.Sprintf("%s.%s s", c.config.Schema, SubmissionsTableName)
|
||||
companiesTable = fmt.Sprintf("%s.%s c", c.config.Schema, CompaniesTableName)
|
||||
vacanciesTable = fmt.Sprintf("%s.%s v", c.config.Schema, VacanciesTableName)
|
||||
vacancyCandidatesTable = fmt.Sprintf("%s.%s vc", c.config.Schema, VacancyCandidatesTableName)
|
||||
)
|
||||
|
||||
getMetadata := psql.Select(
|
||||
"s.uid", "v.id", "v.name", "v.company_id", "c.name", "v.agent_reward", "v.is_archived",
|
||||
"vc.current_candidates_amt", "vc.required_candidates_amt",
|
||||
).From(submissionsTable).
|
||||
InnerJoin(fmt.Sprintf("%s on s.vacancy_id = v.id", vacanciesTable)).
|
||||
InnerJoin(fmt.Sprintf("%s on v.id = vc.vacancy_id", vacancyCandidatesTable)).
|
||||
InnerJoin(fmt.Sprintf("%s on v.company_id = c.id", companiesTable)).
|
||||
Where(squirrel.Eq{"s.id": submissionId}).
|
||||
Suffix("FOR UPDATE OF vc")
|
||||
|
||||
query, args, err := getMetadata.ToSql()
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("%w: error building 'get submission metadata' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
row := driver.QueryRowContext(ctx, query, args...)
|
||||
|
||||
var (
|
||||
agentId string
|
||||
vacancy dbtypes.Vacancy
|
||||
)
|
||||
|
||||
if err := row.Scan(
|
||||
&agentId,
|
||||
&vacancy.Id, &vacancy.Name, &vacancy.Company.Id, &vacancy.Company.Name,
|
||||
&vacancy.AgentReward, &vacancy.IsArchived,
|
||||
&vacancy.CurrentCandidates, &vacancy.RequiredCandidates,
|
||||
); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, "", dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return nil, "", fmt.Errorf("%w: error scanning row for 'get submission metadata' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
return &vacancy, agentId, nil
|
||||
}
|
||||
|
||||
func (c *client) addSubmissionCandidate(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
vacancyId string,
|
||||
) error {
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
vacancyCandisTable = fmt.Sprintf("%s.%s", c.config.Schema, VacancyCandidatesTableName)
|
||||
)
|
||||
|
||||
addSubmissionCandidate := psql.Update(vacancyCandisTable).
|
||||
Set("current_candidates_amt", squirrel.Expr("current_candidates_amt + 1")).
|
||||
Where(squirrel.Eq{"vacancy_id": vacancyId})
|
||||
|
||||
query, args, err := addSubmissionCandidate.ToSql()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error building 'add submission candidate' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
res, err := driver.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error executing 'add submission candidate' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error getting rows affected for 'add submission candidate' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return dberrors.ErrInternal
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) DeleteSubmission(
|
||||
ctx context.Context,
|
||||
request *dbtypes.SubmissionDeleteRequest,
|
||||
) (*dbtypes.SubmissionDeleteResponse, error) {
|
||||
if request == nil {
|
||||
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
submissionsTable = fmt.Sprintf("%s.%s", c.config.Schema, SubmissionsTableName)
|
||||
)
|
||||
|
||||
deleteSubmission := psql.Update(submissionsTable).
|
||||
Set("submission_status_id", SubmissionStatusStringToId(dbtypes.SubStatusCancelled)).
|
||||
Where(squirrel.Eq{"id": request.Id})
|
||||
|
||||
query, args, err := deleteSubmission.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building delete submission query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
res, err := c.db.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error executing delete submission query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error getting rows affected for delete submission query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return nil, dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return &dbtypes.SubmissionDeleteResponse{}, nil
|
||||
}
|
||||
485
internal/database/postgres/transaction.go
Normal file
485
internal/database/postgres/transaction.go
Normal file
@@ -0,0 +1,485 @@
|
||||
package pgdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
dberrors "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/errors"
|
||||
dbtypes "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/types"
|
||||
"github.com/Masterminds/squirrel"
|
||||
)
|
||||
|
||||
func getTransactionTypeByAmount(amount int64) dbtypes.TransactionType {
|
||||
if amount > 0 {
|
||||
return dbtypes.TransactionTypeDeposit
|
||||
}
|
||||
|
||||
return dbtypes.TransactionTypeWithdrawal
|
||||
}
|
||||
|
||||
// TODO: add migration to rebind statuses
|
||||
|
||||
func TransactionStatusIdToString(status int32) dbtypes.TransactionStatus {
|
||||
switch status {
|
||||
case 0:
|
||||
return dbtypes.TransactionStatusPending
|
||||
case 1:
|
||||
return dbtypes.TransactionStatusApproved
|
||||
case 2:
|
||||
return dbtypes.TransactionStatusRejected
|
||||
case 3:
|
||||
return dbtypes.TransactionStatusNew
|
||||
default:
|
||||
return dbtypes.TransactionStatusPending
|
||||
}
|
||||
}
|
||||
|
||||
func TransactionStatusStringToId(status dbtypes.TransactionStatus) int32 {
|
||||
switch status {
|
||||
case dbtypes.TransactionStatusNew:
|
||||
return 3
|
||||
case dbtypes.TransactionStatusPending:
|
||||
return 0
|
||||
case dbtypes.TransactionStatusApproved:
|
||||
return 1
|
||||
case dbtypes.TransactionStatusRejected:
|
||||
return 2
|
||||
default:
|
||||
return 3
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocognit // not so hard
|
||||
func (c *client) GetTransactionList(
|
||||
ctx context.Context,
|
||||
request *dbtypes.TransactionListGetRequest,
|
||||
) (*dbtypes.TransactionListGetResponse, error) {
|
||||
if request == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
transactionsTable = fmt.Sprintf("%s.%s", c.config.Schema, TransactionsTableName)
|
||||
)
|
||||
|
||||
getTransactions := psql.Select(
|
||||
"id", "owner_id", "bank_account_id", "amount", "currency", "status", "created_at", "payload",
|
||||
).From(transactionsTable).
|
||||
Where(squirrel.Eq{"owner_id": request.OwnerId}).
|
||||
Limit(request.PageSize).
|
||||
Offset(countOffset(request.Page, request.PageSize))
|
||||
|
||||
getTransactions, err := c.setGetTransactionsQueryFilters(getTransactions, request.Filters)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error setting get transactions query filters: %v", dberrors.ErrBadRequest, err)
|
||||
}
|
||||
|
||||
query, args, err := getTransactions.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building get transactions query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rows, err := c.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error executing get transactions query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
transList := &dbtypes.TransactionListGetResponse{
|
||||
Transactions: make([]dbtypes.Transaction, 0, request.PageSize),
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
transStatus int32
|
||||
ownerId string
|
||||
payload, bankAccountId sql.NullString
|
||||
transaction dbtypes.Transaction
|
||||
)
|
||||
|
||||
if err := rows.Scan(
|
||||
&transaction.Id, &ownerId, &bankAccountId, &transaction.Amount, &transaction.Currency,
|
||||
&transStatus, &transaction.CreatedAt, &payload,
|
||||
); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: error scanning row: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if bankAccountId.Valid {
|
||||
bankAccountInfo, err := c.getBankAccountInfoById(ctx, c.db, bankAccountId.String)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error getting bank account info: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
transaction.BankAccountInfo = bankAccountInfo
|
||||
|
||||
ownerInfo, err := c.getOwnerInfoById(ctx, c.db, ownerId, bankAccountInfo.OwnerType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error getting owner info: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
transaction.OwnerInfo = ownerInfo
|
||||
}
|
||||
|
||||
transaction.Type = getTransactionTypeByAmount(transaction.Amount)
|
||||
transaction.Status = TransactionStatusIdToString(transStatus)
|
||||
|
||||
if payload.Valid {
|
||||
var payloadData dbtypes.TransactionPayload
|
||||
|
||||
if err := json.Unmarshal([]byte(payload.String), &payloadData); err != nil {
|
||||
return nil, fmt.Errorf("%w: error unmarshaling transaction payload: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
transaction.Payload = &payloadData
|
||||
}
|
||||
|
||||
transList.Transactions = append(transList.Transactions, transaction)
|
||||
}
|
||||
|
||||
return transList, nil
|
||||
}
|
||||
|
||||
func (c *client) setGetTransactionsQueryFilters(
|
||||
query squirrel.SelectBuilder,
|
||||
filters *dbtypes.TransactionListFilters,
|
||||
) (squirrel.SelectBuilder, error) {
|
||||
if filters == nil {
|
||||
return query, nil
|
||||
}
|
||||
|
||||
if filters.Type != nil {
|
||||
switch *filters.Type {
|
||||
case dbtypes.TransactionTypeDeposit:
|
||||
query = query.Where(squirrel.Gt{
|
||||
"amount": 0,
|
||||
})
|
||||
|
||||
case dbtypes.TransactionTypeWithdrawal:
|
||||
query = query.Where(squirrel.Lt{
|
||||
"amount": 0,
|
||||
})
|
||||
|
||||
default:
|
||||
return query, fmt.Errorf("%w: invalid transaction type: %v", dberrors.ErrBadRequest, *filters.Type)
|
||||
}
|
||||
}
|
||||
|
||||
if filters.Status != nil {
|
||||
query = query.Where(squirrel.Eq{"status": TransactionStatusStringToId(*filters.Status)})
|
||||
}
|
||||
|
||||
if filters.BankAccountId != nil {
|
||||
query = query.Where(squirrel.Eq{"bank_account_id": *filters.BankAccountId})
|
||||
}
|
||||
|
||||
return query, nil
|
||||
}
|
||||
|
||||
func (c *client) getOwnerInfoById(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
ownerId string,
|
||||
ownerType string,
|
||||
) (*dbtypes.TransactionOwnerInfo, error) {
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
clientTableName = fmt.Sprintf("%s.%s", c.config.Schema, UsersTableName)
|
||||
companyTableName = fmt.Sprintf("%s.%s", c.config.Schema, CompaniesTableName)
|
||||
)
|
||||
|
||||
var getOwnerInfoById squirrel.SelectBuilder
|
||||
|
||||
// TODO: reingeneer the DB
|
||||
switch ownerType {
|
||||
case "agent":
|
||||
getOwnerInfoById = psql.Select(
|
||||
"uid",
|
||||
"name",
|
||||
).
|
||||
From(clientTableName).
|
||||
Where(squirrel.Eq{"uid": ownerId})
|
||||
|
||||
case "company":
|
||||
getOwnerInfoById = psql.Select(
|
||||
"id",
|
||||
"name",
|
||||
).
|
||||
From(companyTableName).
|
||||
Where(squirrel.Eq{"id": ownerId})
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: invalid owner type", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
query, args, err := getOwnerInfoById.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building get owner info by id query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
row := driver.QueryRowContext(ctx, query, args...)
|
||||
|
||||
var ownerInfo dbtypes.TransactionOwnerInfo
|
||||
|
||||
if err := row.Scan(
|
||||
&ownerInfo.Id,
|
||||
&ownerInfo.Name,
|
||||
); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: error scanning row for get owner info by id query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
return &ownerInfo, nil
|
||||
}
|
||||
|
||||
func (c *client) getBankAccountInfoById(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
bankAccountId string,
|
||||
) (*dbtypes.BankAccountInfo, error) {
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
bankAccountsTableName = fmt.Sprintf("%s.%s", c.config.Schema, BankAccountsTableName)
|
||||
)
|
||||
|
||||
getBankAccountInfoById := psql.Select(
|
||||
"id",
|
||||
"account_number",
|
||||
"bank_name",
|
||||
"bik",
|
||||
"correspondent_account",
|
||||
"owner_type",
|
||||
).
|
||||
From(bankAccountsTableName).
|
||||
Where(squirrel.Eq{"id": bankAccountId})
|
||||
|
||||
query, args, err := getBankAccountInfoById.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building get bank account info by id query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
row := driver.QueryRowContext(ctx, query, args...)
|
||||
|
||||
var accountInfo dbtypes.BankAccountInfo
|
||||
|
||||
if err := row.Scan(
|
||||
&accountInfo.Id,
|
||||
&accountInfo.AccountNumber,
|
||||
&accountInfo.BankName,
|
||||
&accountInfo.Bik,
|
||||
&accountInfo.CorrespondentAccount,
|
||||
&accountInfo.OwnerType,
|
||||
); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: error scanning row for get bank account info by id query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
return &accountInfo, nil
|
||||
}
|
||||
|
||||
func (c *client) CreateTransaction(
|
||||
ctx context.Context,
|
||||
request *dbtypes.TransactionCreateRequest,
|
||||
) (*dbtypes.TransactionCreateResponse, error) {
|
||||
if request == nil {
|
||||
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
tx, err := c.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error starting transaction: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
result, err := c.createTransactionWithDriver(ctx, tx, request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating transaction: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, fmt.Errorf("%w: error committing transaction: %w", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *client) createTransactionWithDriver(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
request *dbtypes.TransactionCreateRequest,
|
||||
) (*dbtypes.TransactionCreateResponse, error) {
|
||||
if _, err := c.getRawBalanceForUpdate(ctx, driver, request.OwnerId); err != nil {
|
||||
return nil, fmt.Errorf("error getting raw balance for update: %w", err)
|
||||
}
|
||||
|
||||
result, err := c.createTransaction(ctx, driver, request)
|
||||
if err != nil {
|
||||
if errors.Is(err, dberrors.ErrConflict) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("error creating transaction: %w", err)
|
||||
}
|
||||
|
||||
if err := c.updateBalance(ctx, driver, request.Amount, request.OwnerId); err != nil {
|
||||
return nil, fmt.Errorf("error updating balance: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *client) getRawBalanceForUpdate(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
ownerId string,
|
||||
) (int64, error) {
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
balancesTable = fmt.Sprintf("%s.%s", c.config.Schema, BalancesTableName)
|
||||
)
|
||||
|
||||
getBalance := psql.Select(
|
||||
"raw_balance",
|
||||
).From(balancesTable).
|
||||
Where(squirrel.Eq{"owner_id": ownerId}).
|
||||
Suffix("FOR UPDATE")
|
||||
|
||||
query, args, err := getBalance.ToSql()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%w: error building 'get balance' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
row := driver.QueryRowContext(ctx, query, args...)
|
||||
|
||||
var rawBalance int64
|
||||
|
||||
if err := row.Scan(&rawBalance); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return 0, dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("%w: error scanning row: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
return rawBalance, nil
|
||||
}
|
||||
|
||||
func (c *client) updateBalance(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
amountDelta int64,
|
||||
ownerId string,
|
||||
) error {
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
balancesTable = fmt.Sprintf("%s.%s", c.config.Schema, BalancesTableName)
|
||||
)
|
||||
|
||||
updateBalance := psql.Update(balancesTable).
|
||||
SetMap(map[string]any{
|
||||
"raw_balance": squirrel.Expr("raw_balance + ?", amountDelta),
|
||||
"updated_at": squirrel.Expr("NOW()"),
|
||||
}).
|
||||
Where(squirrel.Eq{"owner_id": ownerId})
|
||||
|
||||
query, args, err := updateBalance.ToSql()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error building 'update balance' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
res, err := driver.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error executing 'update balance' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error getting rows affected for 'update balance' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return dberrors.ErrInternal
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) createTransaction(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
request *dbtypes.TransactionCreateRequest,
|
||||
) (*dbtypes.TransactionCreateResponse, error) {
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
transactionsTable = fmt.Sprintf("%s.%s", c.config.Schema, TransactionsTableName)
|
||||
)
|
||||
|
||||
var payload []byte
|
||||
|
||||
if request.Payload == nil {
|
||||
payload = []byte("{}")
|
||||
} else {
|
||||
payloadBytes, err := json.Marshal(request.Payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error marshaling transaction payload: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
payload = payloadBytes
|
||||
}
|
||||
|
||||
createTransaction := psql.Insert(transactionsTable).
|
||||
Columns(
|
||||
"id", "owner_id", "bank_account_id", "amount", "currency", "status", "created_at", "payload",
|
||||
).
|
||||
Values(
|
||||
request.RequestId, request.OwnerId, request.BankAccountId, request.Amount, request.Currency,
|
||||
dbtypes.TransactionStatusNew, squirrel.Expr("CURRENT_TIMESTAMP"), payload,
|
||||
).
|
||||
Suffix("ON CONFLICT (id, owner_id) DO NOTHING")
|
||||
|
||||
query, args, err := createTransaction.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building 'create transaction' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
res, err := driver.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error executing 'create transaction' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error getting rows affected for 'create transaction' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return &dbtypes.TransactionCreateResponse{
|
||||
Id: request.RequestId,
|
||||
}, dberrors.ErrConflict
|
||||
}
|
||||
|
||||
return &dbtypes.TransactionCreateResponse{
|
||||
Id: request.RequestId,
|
||||
}, nil
|
||||
}
|
||||
203
internal/database/postgres/user.go
Normal file
203
internal/database/postgres/user.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package pgdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
dberrors "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/errors"
|
||||
dbtypes "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/types"
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (c *client) GetClientValidation(
|
||||
ctx context.Context,
|
||||
request *dbtypes.ClientValidationGetRequest,
|
||||
) (*dbtypes.ClientValidationGetResponse, error) {
|
||||
if request == nil {
|
||||
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
userValidationTable = fmt.Sprintf("%s.%s", c.config.Schema, UserValidationTableName)
|
||||
)
|
||||
|
||||
getUserValidation := psql.Select(
|
||||
"status", "description",
|
||||
).
|
||||
From(userValidationTable).
|
||||
Where(squirrel.Eq{"uid": request.UserId})
|
||||
|
||||
query, args, err := getUserValidation.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building 'get user validation' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
row := c.db.QueryRowContext(ctx, query, args...)
|
||||
|
||||
var result dbtypes.ClientValidation
|
||||
|
||||
if err := row.Scan(&result.Status, &result.Description); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: error scanning row for 'get user validation' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
return &dbtypes.ClientValidationGetResponse{
|
||||
ClientValidation: &result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *client) CreateUser(
|
||||
ctx context.Context,
|
||||
request *dbtypes.UserSaveRequest,
|
||||
) (*dbtypes.UserSaveResponse, error) {
|
||||
if request == nil {
|
||||
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
tx, err := c.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error starting transaction: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
result, err := c.createUser(ctx, tx, request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating user: %w", err)
|
||||
}
|
||||
|
||||
if err := c.createUserValidationTicket(ctx, tx, request); err != nil {
|
||||
return nil, fmt.Errorf("error creating user validation ticket: %w", err)
|
||||
}
|
||||
|
||||
resp, err := c.createCompany(ctx, tx, &dbtypes.CompanyCreateRequest{
|
||||
OwnerId: request.Id,
|
||||
Staff: []string{
|
||||
request.Id,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating company: %w", err)
|
||||
}
|
||||
|
||||
var ownerId string
|
||||
|
||||
switch request.Type {
|
||||
case dbtypes.UserTypeAgent:
|
||||
ownerId = request.Id
|
||||
case dbtypes.UserTypeDistributor:
|
||||
ownerId = resp.Id
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: unknown user type: %v", dberrors.ErrBadRequest, request.Type)
|
||||
}
|
||||
|
||||
balanceId := fmt.Sprintf("%sBAL", strings.ReplaceAll(uuid.NewString(), "-", ""))
|
||||
|
||||
if err := c.createBalance(ctx, tx, &dbtypes.BalanceCreateRequest{
|
||||
Id: balanceId,
|
||||
OwnerId: ownerId,
|
||||
RawBalance: 0,
|
||||
CleanBalance: 0,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("error creating balance: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, fmt.Errorf("%w: error committing transaction: %w", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *client) createUser(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
request *dbtypes.UserSaveRequest,
|
||||
) (*dbtypes.UserSaveResponse, error) {
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
userTable = fmt.Sprintf("%s.%s", c.config.Schema, UsersTableName)
|
||||
)
|
||||
|
||||
saveUser := psql.Insert(userTable).
|
||||
Columns(
|
||||
"uid", "name", "phone", "email", "client_type_id",
|
||||
).
|
||||
Values(
|
||||
request.Id, request.FullName, request.Phone, request.Email, request.Type,
|
||||
)
|
||||
|
||||
query, args, err := saveUser.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building 'save user' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
res, err := driver.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error executing 'save user' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error getting rows affected for 'save user' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return nil, dberrors.ErrInternal
|
||||
}
|
||||
|
||||
return &dbtypes.UserSaveResponse{}, nil
|
||||
}
|
||||
|
||||
func (c *client) createUserValidationTicket(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
request *dbtypes.UserSaveRequest,
|
||||
) error {
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
userValidationTable = fmt.Sprintf("%s.%s", c.config.Schema, UserValidationTableName)
|
||||
)
|
||||
|
||||
// TODO: use normal uuid after DB reengineering
|
||||
ticketId := fmt.Sprintf("%sVAL", strings.ReplaceAll(uuid.NewString(), "-", ""))
|
||||
|
||||
createUserValidation := psql.Insert(userValidationTable).
|
||||
Columns(
|
||||
"id", "uid", "status", "description", "last_update",
|
||||
).
|
||||
Values(
|
||||
ticketId, request.Id, dbtypes.ClientValStatusNew, request.FullName, squirrel.Expr("CURRENT_TIMESTAMP"),
|
||||
)
|
||||
|
||||
query, args, err := createUserValidation.ToSql()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error building 'create user validation' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
res, err := driver.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error executing 'create user validation' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: error getting rows affected for 'create user validation' query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return dberrors.ErrInternal
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
573
internal/database/postgres/vacancy.go
Normal file
573
internal/database/postgres/vacancy.go
Normal file
@@ -0,0 +1,573 @@
|
||||
package pgdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
dberrors "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/errors"
|
||||
dbtypes "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database/types"
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func VacancyStatusIdToString(status int32) dbtypes.VacancyStatus {
|
||||
switch status {
|
||||
case 0:
|
||||
return dbtypes.VacUnspecified
|
||||
case 1:
|
||||
return dbtypes.VacNew
|
||||
case 2:
|
||||
return dbtypes.VacPending
|
||||
case 3:
|
||||
return dbtypes.VacApproved
|
||||
case 4:
|
||||
return dbtypes.VacRejected
|
||||
default:
|
||||
return dbtypes.VacUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
func VacancyStatusStringToId(status dbtypes.VacancyStatus) int32 {
|
||||
switch status {
|
||||
case dbtypes.VacUnspecified:
|
||||
return 0
|
||||
case dbtypes.VacNew:
|
||||
return 1
|
||||
case dbtypes.VacPending:
|
||||
return 2
|
||||
case dbtypes.VacApproved:
|
||||
return 3
|
||||
case dbtypes.VacRejected:
|
||||
return 4
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:funlen,gocognit // maybe refactor later
|
||||
func (c *client) GetVacancyList(
|
||||
ctx context.Context,
|
||||
request *dbtypes.VacancyListGetRequest,
|
||||
) (*dbtypes.VacancyListGetResponse, error) {
|
||||
if request == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
vacancyTable = fmt.Sprintf("%s.%s v", c.config.Schema, VacanciesTableName)
|
||||
companyTable = fmt.Sprintf("%s.%s c", c.config.Schema, CompaniesTableName)
|
||||
)
|
||||
|
||||
getVacList := psql.Select(
|
||||
"v.id", "v.company_id", "c.name", "v.name", "v.address", "v.work_format",
|
||||
"v.agent_reward", "v.region", "v.salary_top", "v.salary_bottom", "v.requirements",
|
||||
"v.responsibilities", "v.additional_info", "v.is_archived",
|
||||
"v.target_action", "v.target_action_amount", "v.publish_date",
|
||||
"v.moderation_status_id", "v.required_candidates", "v.current_candidates",
|
||||
"v.additional_fields",
|
||||
).
|
||||
From(vacancyTable).
|
||||
LeftJoin(fmt.Sprintf("%s on v.company_id = c.id", companyTable)).
|
||||
Limit(request.PageSize).
|
||||
Offset(countOffset(request.Page, request.PageSize))
|
||||
|
||||
getVacList = c.setVacancyListFilters(getVacList, request.Filters)
|
||||
|
||||
query, args, err := getVacList.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building get vacancy list query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rows, err := c.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error executing get vacancy list query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
var (
|
||||
res dbtypes.VacancyListGetResponse
|
||||
vacancyIds = make([]string, 0, request.PageSize)
|
||||
)
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
status int32
|
||||
agentReward sql.NullInt32
|
||||
comName, reqs, resps, extraInfo, extraFields sql.NullString
|
||||
vacancy dbtypes.Vacancy
|
||||
)
|
||||
|
||||
if err := rows.Scan(
|
||||
&vacancy.Id, &vacancy.Company.Id, &comName, &vacancy.Name, &vacancy.Address,
|
||||
&vacancy.WorkFormat, &agentReward, &vacancy.Region, &vacancy.SalaryTop, &vacancy.SalaryBottom,
|
||||
&reqs, &resps, &extraInfo, &vacancy.IsArchived, &vacancy.TargetAction.Action, &vacancy.TargetAction.Duration,
|
||||
&vacancy.CreatedAt, &status, &vacancy.RequiredCandidates, &vacancy.CurrentCandidates,
|
||||
&extraFields,
|
||||
); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: error scanning row: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if agentReward.Valid {
|
||||
vacancy.AgentReward = &agentReward.Int32
|
||||
}
|
||||
|
||||
if comName.Valid {
|
||||
vacancy.Company.Name = comName.String
|
||||
}
|
||||
|
||||
if reqs.Valid {
|
||||
vacancy.Requirements = &reqs.String
|
||||
}
|
||||
|
||||
if resps.Valid {
|
||||
vacancy.Responsibilities = &resps.String
|
||||
}
|
||||
|
||||
if extraInfo.Valid {
|
||||
vacancy.ExtraInfo = &extraInfo.String
|
||||
}
|
||||
|
||||
if extraFields.Valid {
|
||||
vacancy.ExtraFields = &extraFields.String
|
||||
}
|
||||
|
||||
vacancy.Moderation.Status = VacancyStatusIdToString(status)
|
||||
|
||||
res.Vacancies = append(res.Vacancies, vacancy)
|
||||
vacancyIds = append(vacancyIds, vacancy.Id)
|
||||
}
|
||||
|
||||
descriptions, err := c.getVacancyModerationDescriptionHistory(ctx, c.db, vacancyIds)
|
||||
if err != nil {
|
||||
if errors.Is(err, dberrors.ErrNotFound) {
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("error getting descriptions for vacancies: %w", err)
|
||||
}
|
||||
|
||||
for i, vacancy := range res.Vacancies {
|
||||
res.Vacancies[i].Moderation.DescriptionHistory = descriptions[vacancy.Id]
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (c *client) setVacancyListFilters(
|
||||
query squirrel.SelectBuilder,
|
||||
filters *dbtypes.VacancyListFilters,
|
||||
) squirrel.SelectBuilder {
|
||||
if filters.DistributorId != nil {
|
||||
query = query.Where(squirrel.Eq{"c.uid": *filters.DistributorId})
|
||||
}
|
||||
|
||||
if filters.CompanyId != nil {
|
||||
query = query.Where(squirrel.Eq{"v.company_id": *filters.CompanyId})
|
||||
}
|
||||
|
||||
if filters.VacancyId != nil {
|
||||
query = query.Where(squirrel.Eq{"v.id": *filters.VacancyId})
|
||||
}
|
||||
|
||||
if filters.Region != nil {
|
||||
query = query.Where(squirrel.Eq{"v.region": *filters.Region})
|
||||
}
|
||||
|
||||
if filters.SalaryBottom != nil {
|
||||
query = query.Where(squirrel.Or{
|
||||
squirrel.GtOrEq{"v.salary_bottom": *filters.SalaryBottom},
|
||||
squirrel.Eq{"v.salary_bottom": nil},
|
||||
})
|
||||
}
|
||||
|
||||
if filters.SalaryTop != nil {
|
||||
query = query.Where(squirrel.Or{
|
||||
squirrel.LtOrEq{"v.salary_top": *filters.SalaryTop},
|
||||
squirrel.Eq{"v.salary_top": nil},
|
||||
})
|
||||
}
|
||||
|
||||
if filters.IsArchived != nil {
|
||||
query = query.Where(squirrel.Eq{"v.is_archived": *filters.IsArchived})
|
||||
}
|
||||
|
||||
if filters.Status != nil {
|
||||
query = query.Where(squirrel.Eq{"v.moderation_status_id": VacancyStatusStringToId(*filters.Status)})
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
func (c *client) getVacancyModerationDescriptionHistory(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
vacancyIds []string,
|
||||
) (map[string][]dbtypes.VacancyModerationDescription, error) {
|
||||
if len(vacancyIds) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
vacancyModerDescriptionsTable = fmt.Sprintf("%s.%s", c.config.Schema, VacancyDescriptionHistoryTableName)
|
||||
)
|
||||
|
||||
getVacancyModerationDescriptionHistory := psql.Select(
|
||||
"vacancy_id",
|
||||
"description",
|
||||
"created_at",
|
||||
).
|
||||
From(vacancyModerDescriptionsTable).
|
||||
Where(squirrel.Eq{"vacancy_id": vacancyIds}).
|
||||
OrderBy("vacancy_id", "created_at ASC")
|
||||
|
||||
query, args, err := getVacancyModerationDescriptionHistory.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building getVacancyModerationDescriptionHistory query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rows, err := driver.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error executing getVacancyModerationDescriptionHistory query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
descriptionsMap := make(map[string][]dbtypes.VacancyModerationDescription)
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
vacancyId string
|
||||
description dbtypes.VacancyModerationDescription
|
||||
)
|
||||
|
||||
if err := rows.Scan(
|
||||
&vacancyId,
|
||||
&description.Description,
|
||||
&description.CreatedAt,
|
||||
); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: error scanning row for getVacancyModerationDescriptionHistory: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
descriptionsMap[vacancyId] = append(descriptionsMap[vacancyId], description)
|
||||
}
|
||||
|
||||
return descriptionsMap, nil
|
||||
}
|
||||
|
||||
func (c *client) CreateVacancy(
|
||||
ctx context.Context,
|
||||
request *dbtypes.VacancyCreateRequest,
|
||||
) (*dbtypes.VacancyCreateResponse, error) {
|
||||
if request == nil {
|
||||
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
vacancyTable = fmt.Sprintf("%s.%s", c.config.Schema, VacanciesTableName)
|
||||
)
|
||||
|
||||
// TODO: use normal uuid after DB reengineering
|
||||
vacancyId := fmt.Sprintf("%sVAC", strings.ReplaceAll(uuid.NewString(), "-", ""))
|
||||
|
||||
createVacancy := psql.Insert(vacancyTable).
|
||||
Columns(
|
||||
"id", "company_id", "name", "address", "work_format", "agent_reward",
|
||||
"salary_top", "salary_bottom", "requirements", "responsibilities", "additional_info",
|
||||
"region", "is_archived", "target_action", "target_action_amount", "moderation_status_id",
|
||||
"required_candidates", "publish_date", "current_candidates", "additional_fields",
|
||||
).
|
||||
Values(
|
||||
vacancyId, request.CompanyId, request.Name, request.Address, request.WorkFormat, request.AgentReward,
|
||||
request.SalaryTop, request.SalaryBottom, request.Requirements, request.Responsibilities, request.ExtraInfo,
|
||||
request.Region, false, request.TargetAction.Action, request.TargetAction.Duration, VacancyStatusStringToId(dbtypes.VacNew),
|
||||
request.RequiredCandidates, squirrel.Expr("now()"), request.CurrentCandidates, request.ExtraFields,
|
||||
)
|
||||
|
||||
query, args, err := createVacancy.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building create vacancy query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
res, err := c.db.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
// TODO: process conflict via err.(*pq.Error).Code
|
||||
return nil, fmt.Errorf("%w: error executing create vacancy query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error getting rows affected for create vacancy query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return nil, dberrors.ErrInternal
|
||||
}
|
||||
|
||||
return &dbtypes.VacancyCreateResponse{Id: vacancyId}, nil
|
||||
}
|
||||
|
||||
func (c *client) UpdateVacancy(
|
||||
ctx context.Context,
|
||||
request *dbtypes.VacancyUpdateRequest,
|
||||
) (*dbtypes.VacancyUpdateResponse, error) {
|
||||
if request == nil {
|
||||
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
tx, err := c.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error starting transaction: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
status, err := c.getVacancyModerStatusForUpdate(ctx, tx, request.Id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting vacancy moder status: %w", err)
|
||||
}
|
||||
|
||||
if status == dbtypes.VacApproved {
|
||||
return nil, dberrors.ErrForbidden
|
||||
}
|
||||
|
||||
res, err := c.updateVacancy(ctx, tx, request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error updating vacancy: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, fmt.Errorf("error committing transaction: %w", err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *client) getVacancyModerStatusForUpdate(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
vacancyId string,
|
||||
) (dbtypes.VacancyStatus, error) {
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
vacancyTable = fmt.Sprintf("%s.%s", c.config.Schema, VacanciesTableName)
|
||||
)
|
||||
|
||||
getVacancyModerStatus := psql.Select(
|
||||
"moderation_status_id",
|
||||
).
|
||||
From(vacancyTable).
|
||||
Where(squirrel.Eq{"id": vacancyId}).
|
||||
Suffix("FOR UPDATE")
|
||||
|
||||
query, args, err := getVacancyModerStatus.ToSql()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: error building get vacancy moder status query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
row := driver.QueryRowContext(ctx, query, args...)
|
||||
|
||||
var (
|
||||
statusId int32
|
||||
)
|
||||
|
||||
if err := row.Scan(&statusId); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return "", dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("%w: error scanning row for get vacancy moder status query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
return VacancyStatusIdToString(statusId), nil
|
||||
}
|
||||
|
||||
func (c *client) updateVacancy(
|
||||
ctx context.Context,
|
||||
driver Driver,
|
||||
request *dbtypes.VacancyUpdateRequest,
|
||||
) (*dbtypes.VacancyUpdateResponse, error) {
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
vacancyTable = fmt.Sprintf("%s.%s", c.config.Schema, VacanciesTableName)
|
||||
)
|
||||
|
||||
updateVacancy := psql.Update(vacancyTable).
|
||||
Where(squirrel.Eq{"id": request.Id})
|
||||
|
||||
if request.Name != nil {
|
||||
updateVacancy = updateVacancy.Set("name", *request.Name)
|
||||
}
|
||||
|
||||
if request.Address != nil {
|
||||
updateVacancy = updateVacancy.Set("address", *request.Address)
|
||||
}
|
||||
|
||||
if request.WorkFormat != nil {
|
||||
updateVacancy = updateVacancy.Set("work_format", *request.WorkFormat)
|
||||
}
|
||||
|
||||
if request.AgentReward != nil {
|
||||
updateVacancy = updateVacancy.Set("agent_reward", *request.AgentReward)
|
||||
}
|
||||
|
||||
if request.SalaryTop != nil {
|
||||
updateVacancy = updateVacancy.Set("salary_top", *request.SalaryTop)
|
||||
}
|
||||
|
||||
if request.SalaryBottom != nil {
|
||||
updateVacancy = updateVacancy.Set("salary_bottom", *request.SalaryBottom)
|
||||
}
|
||||
|
||||
if request.Requirements != nil {
|
||||
updateVacancy = updateVacancy.Set("requirements", *request.Requirements)
|
||||
}
|
||||
|
||||
if request.Responsibilities != nil {
|
||||
updateVacancy = updateVacancy.Set("responsibilities", *request.Responsibilities)
|
||||
}
|
||||
|
||||
if request.ExtraInfo != nil {
|
||||
updateVacancy = updateVacancy.Set("extra_info", *request.ExtraInfo)
|
||||
}
|
||||
|
||||
if request.Region != nil {
|
||||
updateVacancy = updateVacancy.Set("region", *request.Region)
|
||||
}
|
||||
|
||||
if request.TargetAction.Action != nil {
|
||||
updateVacancy = updateVacancy.Set("target_action", *request.TargetAction.Action)
|
||||
}
|
||||
|
||||
if request.TargetAction.Duration != nil {
|
||||
updateVacancy = updateVacancy.Set("target_action_amount", *request.TargetAction.Duration)
|
||||
}
|
||||
|
||||
if request.RequiredCandidates != nil {
|
||||
updateVacancy = updateVacancy.Set("required_candidates", *request.RequiredCandidates)
|
||||
}
|
||||
|
||||
if request.ExtraFields != nil {
|
||||
updateVacancy = updateVacancy.Set("additional_fields", *request.ExtraFields)
|
||||
}
|
||||
|
||||
query, args, err := updateVacancy.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building update vacancy query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
res, err := driver.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error executing update vacancy query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error getting rows affected for update vacancy query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return nil, dberrors.ErrInternal
|
||||
}
|
||||
|
||||
return &dbtypes.VacancyUpdateResponse{}, nil
|
||||
}
|
||||
|
||||
func (c *client) DeleteVacancy(
|
||||
ctx context.Context,
|
||||
request *dbtypes.VacancyDeleteRequest,
|
||||
) (*dbtypes.VacancyDeleteResponse, error) {
|
||||
if request == nil {
|
||||
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
vacancyTable = fmt.Sprintf("%s.%s", c.config.Schema, VacanciesTableName)
|
||||
)
|
||||
|
||||
deleteVacancy := psql.Update(vacancyTable).
|
||||
Set("is_archived", true).
|
||||
Where(squirrel.Eq{"id": request.Id})
|
||||
|
||||
query, args, err := deleteVacancy.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building delete vacancy query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
res, err := c.db.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error executing delete vacancy query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error getting rows affected for delete vacancy query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return nil, dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return &dbtypes.VacancyDeleteResponse{}, nil
|
||||
}
|
||||
|
||||
func (c *client) SendVacancyToModeration(
|
||||
ctx context.Context,
|
||||
request *dbtypes.SendVacancyToModerationRequest,
|
||||
) (*dbtypes.SendVacancyToModerationResponse, error) {
|
||||
if request == nil {
|
||||
return nil, fmt.Errorf("%w: request is nil", dberrors.ErrBadRequest)
|
||||
}
|
||||
|
||||
var (
|
||||
psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
||||
vacancyTable = fmt.Sprintf("%s.%s", c.config.Schema, VacanciesTableName)
|
||||
)
|
||||
|
||||
sendVacancyToModeration := psql.Update(vacancyTable).
|
||||
Set("moderation_status_id", VacancyStatusStringToId(dbtypes.VacPending)).
|
||||
Where(squirrel.Eq{"id": request.Id})
|
||||
|
||||
query, args, err := sendVacancyToModeration.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error building send vacancy to moderation query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
res, err := c.db.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error executing send vacancy to moderation query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: error getting rows affected for send vacancy to moderation query: %v", dberrors.ErrInternal, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return nil, dberrors.ErrNotFound
|
||||
}
|
||||
|
||||
return &dbtypes.SendVacancyToModerationResponse{}, nil
|
||||
}
|
||||
217
internal/database/types/balance.go
Normal file
217
internal/database/types/balance.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package dbtypes
|
||||
|
||||
import "time"
|
||||
|
||||
type (
|
||||
Balance struct {
|
||||
RawBalance int64
|
||||
CleanBalance int64
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
BalanceGetRequest struct {
|
||||
OwnerId string
|
||||
}
|
||||
|
||||
BalanceGetResponse struct {
|
||||
Balance *Balance
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
BalanceCreateRequest struct {
|
||||
Id string
|
||||
OwnerId string
|
||||
RawBalance int64
|
||||
CleanBalance int64
|
||||
}
|
||||
)
|
||||
|
||||
type TransactionStatus string
|
||||
|
||||
const (
|
||||
TransactionStatusNew TransactionStatus = "new"
|
||||
TransactionStatusPending TransactionStatus = "pending"
|
||||
TransactionStatusApproved TransactionStatus = "approved"
|
||||
TransactionStatusRejected TransactionStatus = "rejected"
|
||||
)
|
||||
|
||||
func NewTransactionStatus(status *string) *TransactionStatus {
|
||||
if status == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
res := TransactionStatus(*status)
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
type TransactionType string
|
||||
|
||||
const (
|
||||
TransactionTypeDeposit TransactionType = "deposit"
|
||||
TransactionTypeWithdrawal TransactionType = "withdrawal"
|
||||
)
|
||||
|
||||
func NewTransactionType(typ *string) *TransactionType {
|
||||
if typ == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
res := TransactionType(*typ)
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
type (
|
||||
Transaction struct {
|
||||
Id string
|
||||
OwnerInfo *TransactionOwnerInfo
|
||||
BankAccountInfo *BankAccountInfo
|
||||
Type TransactionType
|
||||
Status TransactionStatus
|
||||
Amount int64
|
||||
Currency string
|
||||
CreatedAt time.Time
|
||||
Payload *TransactionPayload
|
||||
}
|
||||
|
||||
BankAccountInfo struct {
|
||||
Id string
|
||||
AccountNumber string
|
||||
AccountName string
|
||||
BankName string
|
||||
Bik string
|
||||
CorrespondentAccount string
|
||||
OwnerType string
|
||||
}
|
||||
|
||||
TransactionOwnerInfo struct {
|
||||
Id string
|
||||
Name string
|
||||
}
|
||||
|
||||
// TODO: make DB migration to use this
|
||||
// TransactionPayload struct {
|
||||
// Origin string `json:"origin"`
|
||||
// CompanyInfo *PayloadCompanyInfo `json:"company_info"`
|
||||
// VacancyInfo *PayloadVacancyInfo `json:"vacancy_info"`
|
||||
// }
|
||||
|
||||
// PayloadCompanyInfo struct {
|
||||
// Id string `json:"id"`
|
||||
// Name string `json:"name"`
|
||||
// }
|
||||
|
||||
// PayloadVacancyInfo struct {
|
||||
// Id string `json:"id"`
|
||||
// Name string `json:"name"`
|
||||
// }
|
||||
|
||||
TransactionPayload struct {
|
||||
Origin string `json:"origin"`
|
||||
CompanyId string `json:"company_id"`
|
||||
CompanyName string `json:"company_name"`
|
||||
VacancyId string `json:"vacancy_id"`
|
||||
VacancyName string `json:"vacancy_name"`
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
TransactionListGetRequest struct {
|
||||
OwnerId string
|
||||
Filters *TransactionListFilters
|
||||
Page uint64
|
||||
PageSize uint64
|
||||
}
|
||||
|
||||
TransactionListFilters struct {
|
||||
Type *TransactionType
|
||||
Status *TransactionStatus
|
||||
BankAccountId *string
|
||||
}
|
||||
|
||||
TransactionListGetResponse struct {
|
||||
Transactions []Transaction
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
TransactionCreateRequest struct {
|
||||
OwnerId string
|
||||
Amount int64
|
||||
Currency string
|
||||
BankAccountId *string
|
||||
RequestId string
|
||||
Payload *TransactionPayload
|
||||
}
|
||||
|
||||
TransactionCreateResponse struct {
|
||||
Id string
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
BankAccount struct {
|
||||
Id string
|
||||
OwnerId string
|
||||
AccountNumber string
|
||||
AccountName string
|
||||
BankName string
|
||||
Bik string
|
||||
CorrespondentAccount string
|
||||
IsPrimary bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
BankAccountListGetRequest struct {
|
||||
OwnerId string
|
||||
}
|
||||
|
||||
BankAccountListGetResponse struct {
|
||||
BankAccounts []BankAccount
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
BankAccountCreateRequest struct {
|
||||
OwnerId string
|
||||
AccountNumber *string
|
||||
AccountName *string
|
||||
BankName *string
|
||||
Bik *string
|
||||
CorrespondentAccount *string
|
||||
IsPrimary *bool
|
||||
}
|
||||
|
||||
BankAccountCreateResponse struct {
|
||||
Id string
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
BankAccountUpdateRequest struct {
|
||||
Id string
|
||||
AccountNumber *string
|
||||
AccountName *string
|
||||
BankName *string
|
||||
Bik *string
|
||||
CorrespondentAccount *string
|
||||
IsPrimary *bool
|
||||
}
|
||||
|
||||
BankAccountUpdateResponse struct{}
|
||||
)
|
||||
|
||||
type (
|
||||
BankAccountDeleteRequest struct {
|
||||
Id string
|
||||
OwnerId string
|
||||
}
|
||||
|
||||
BankAccountDeleteResponse struct{}
|
||||
)
|
||||
101
internal/database/types/company.go
Normal file
101
internal/database/types/company.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package dbtypes
|
||||
|
||||
type CompanyModerationStatus string
|
||||
|
||||
const (
|
||||
CompanyModerationStatusNew CompanyModerationStatus = "new"
|
||||
CompanyModerationStatusPending CompanyModerationStatus = "pending"
|
||||
CompanyModerationStatusApproved CompanyModerationStatus = "approved"
|
||||
CompanyModerationStatusRejected CompanyModerationStatus = "rejected"
|
||||
)
|
||||
|
||||
type (
|
||||
Company struct {
|
||||
Id string
|
||||
OwnerId string
|
||||
Name *string
|
||||
LegalPerson *string
|
||||
Description *string
|
||||
Website *string
|
||||
PhysicalAddress *string
|
||||
LegalAddress *string
|
||||
Inn *string
|
||||
Kpp *string
|
||||
IsActive bool
|
||||
HasModerationTicket bool
|
||||
Staff []string
|
||||
Metadata *string
|
||||
ExtraFieldsTemplate *string
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
CompanyListGetRequest struct {
|
||||
Id string
|
||||
}
|
||||
|
||||
CompanyListGetResponse struct {
|
||||
Companies []Company
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
CompanyByIdGetRequest struct {
|
||||
Id string
|
||||
CompanyId string
|
||||
}
|
||||
|
||||
CompanyByIdGetResponse struct {
|
||||
Company Company
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
CompanyCreateRequest struct {
|
||||
OwnerId string
|
||||
Name *string
|
||||
LegalPerson *string
|
||||
Description *string
|
||||
Website *string
|
||||
PhysicalAddress *string
|
||||
LegalAddress *string
|
||||
Inn *string
|
||||
Kpp *string
|
||||
Staff []string
|
||||
Metadata *string
|
||||
ExtraFieldsTemplate *string
|
||||
}
|
||||
|
||||
CompanyCreateResponse struct {
|
||||
Id string
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
CompanyUpdateRequest struct {
|
||||
Id string
|
||||
Name *string
|
||||
LegalPerson *string
|
||||
Description *string
|
||||
Website *string
|
||||
PhysicalAddress *string
|
||||
LegalAddress *string
|
||||
Inn *string // can be updated in several rare cases
|
||||
Kpp *string // can be updated in several rare cases
|
||||
Staff []string
|
||||
Metadata *string
|
||||
ExtraFields *string
|
||||
}
|
||||
|
||||
CompanyUpdateResponse struct {
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
CompanyDeleteRequest struct {
|
||||
Id string
|
||||
}
|
||||
|
||||
CompanyDeleteResponse struct {
|
||||
}
|
||||
)
|
||||
53
internal/database/types/integration.go
Normal file
53
internal/database/types/integration.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package dbtypes
|
||||
|
||||
type Fieldtype string
|
||||
|
||||
type (
|
||||
CompanyMetadata map[string]string
|
||||
|
||||
ValidValue struct {
|
||||
DisplayName string `json:"display_name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
/*
|
||||
CompanyExtraFieldTemplate describes single
|
||||
additional field of company additional fields template
|
||||
*/
|
||||
CompanyExtraFieldTemplate struct {
|
||||
SystemName string `json:"system_name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Type Fieldtype `json:"type"`
|
||||
Required bool `json:"required"`
|
||||
MultipleChoice bool `json:"multiple_choice"`
|
||||
ValidValues []ValidValue `json:"valid_values"`
|
||||
}
|
||||
|
||||
/*
|
||||
CompanyExtraFieldsTemplate describes additional fields
|
||||
needed to integrate company
|
||||
*/
|
||||
CompanyExtraFieldsTemplate struct {
|
||||
Fields []CompanyExtraFieldTemplate `json:"fields"`
|
||||
}
|
||||
|
||||
/*
|
||||
VacancyExtraFieldTemplate describes single filled
|
||||
additional field of vacancy additional fields template
|
||||
*/
|
||||
VacancyExtraFieldTemplate struct {
|
||||
SystemName string `json:"system_name"`
|
||||
Type Fieldtype `json:"type"`
|
||||
IsValidValue bool `json:"is_valid_values"`
|
||||
MultipleChoice bool `json:"multiple_choice"`
|
||||
Value []string `json:"value"`
|
||||
}
|
||||
|
||||
/*
|
||||
VacancyExtraFieldsTemplate describes additional fields
|
||||
filled during vacancy creation
|
||||
*/
|
||||
VacancyExtraFieldsTemplate struct {
|
||||
Fields []VacancyExtraFieldTemplate `json:"fields"`
|
||||
}
|
||||
)
|
||||
31
internal/database/types/profile.go
Normal file
31
internal/database/types/profile.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package dbtypes
|
||||
|
||||
type (
|
||||
Profile struct {
|
||||
Id string
|
||||
Name string
|
||||
PhoneNumber string
|
||||
Email string
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
ProfileGetRequest struct {
|
||||
Id string
|
||||
}
|
||||
|
||||
ProfileGetResponse struct {
|
||||
Profile *Profile
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
ProfileUpdateRequest struct {
|
||||
Id string
|
||||
Name *string
|
||||
PhoneNumber *string
|
||||
Email *string
|
||||
}
|
||||
|
||||
ProfileUpdateResponse struct{}
|
||||
)
|
||||
100
internal/database/types/submission.go
Normal file
100
internal/database/types/submission.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package dbtypes
|
||||
|
||||
type SubmissionStatus string
|
||||
|
||||
const (
|
||||
SubStatusUnspecified SubmissionStatus = "unspecified"
|
||||
SubStatusNew SubmissionStatus = "new"
|
||||
SubStatusPending SubmissionStatus = "pending"
|
||||
SubStatusOnInterview SubmissionStatus = "on_interview"
|
||||
SubStatusApproved SubmissionStatus = "approved"
|
||||
SubStatusCancelled SubmissionStatus = "cancelled"
|
||||
SubStatusRejected SubmissionStatus = "rejected"
|
||||
)
|
||||
|
||||
func NewSubmissionStatus(status *string) *SubmissionStatus {
|
||||
if status == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
res := SubmissionStatus(*status)
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
type (
|
||||
Submission struct {
|
||||
Id string
|
||||
AgentInfo *AgentInfo
|
||||
VacancyInfo *VacancyInfo
|
||||
CandidateInfo *CandidateInfo
|
||||
Status SubmissionStatus
|
||||
}
|
||||
|
||||
AgentInfo struct {
|
||||
Id string
|
||||
}
|
||||
|
||||
VacancyInfo struct {
|
||||
Id string
|
||||
}
|
||||
|
||||
CandidateInfo struct {
|
||||
Id string
|
||||
FirstName string
|
||||
LastName string
|
||||
MiddleName string
|
||||
PhoneNumber string
|
||||
Email string
|
||||
Birthday string
|
||||
CvLink *string
|
||||
Resume *string
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
SubmissionListGetRequest struct {
|
||||
Filters *SubmissionListGetFilters
|
||||
Page uint64
|
||||
PageSize uint64
|
||||
}
|
||||
|
||||
SubmissionListGetFilters struct {
|
||||
AgentId *string
|
||||
VacancyId *string
|
||||
Status *SubmissionStatus
|
||||
}
|
||||
|
||||
SubmissionListGetResponse struct {
|
||||
Submissions []Submission
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
SubmissionCreateRequest struct {
|
||||
AgentId string
|
||||
VacancyId string
|
||||
CandidateInfo *CandidateInfo
|
||||
}
|
||||
|
||||
SubmissionCreateResponse struct {
|
||||
Id string
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
SubmissionStatusUpdateRequest struct {
|
||||
Id string
|
||||
Status SubmissionStatus
|
||||
}
|
||||
|
||||
SubmissionStatusUpdateResponse struct{}
|
||||
)
|
||||
|
||||
type (
|
||||
SubmissionDeleteRequest struct {
|
||||
Id string
|
||||
}
|
||||
|
||||
SubmissionDeleteResponse struct{}
|
||||
)
|
||||
57
internal/database/types/user.go
Normal file
57
internal/database/types/user.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package dbtypes
|
||||
|
||||
type ClientValidationStatus string
|
||||
|
||||
const (
|
||||
ClientValStatusNew ClientValidationStatus = "new"
|
||||
ClientValStatusPending ClientValidationStatus = "on review"
|
||||
ClientValStatusApproved ClientValidationStatus = "confirmed"
|
||||
ClientValStatusRejected ClientValidationStatus = "rejected"
|
||||
)
|
||||
|
||||
type (
|
||||
ClientValidation struct {
|
||||
Status ClientValidationStatus `json:"status"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
ClientValidationGetRequest struct {
|
||||
UserId string
|
||||
}
|
||||
|
||||
ClientValidationGetResponse struct {
|
||||
ClientValidation *ClientValidation `json:"moderation"`
|
||||
}
|
||||
)
|
||||
|
||||
type UserType int32
|
||||
|
||||
const (
|
||||
UserTypeAgent UserType = 0 // NOTE: no iota here
|
||||
UserTypeDistributor UserType = 1
|
||||
)
|
||||
|
||||
type (
|
||||
User struct {
|
||||
Id string
|
||||
FullName string
|
||||
Phone string
|
||||
Email string
|
||||
Type UserType
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
UserSaveRequest struct {
|
||||
Id string
|
||||
FullName string
|
||||
Phone string
|
||||
Email string
|
||||
Type UserType
|
||||
}
|
||||
|
||||
UserSaveResponse struct {
|
||||
}
|
||||
)
|
||||
184
internal/database/types/vacancy.go
Normal file
184
internal/database/types/vacancy.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package dbtypes
|
||||
|
||||
import "time"
|
||||
|
||||
type VacancyStatus string
|
||||
|
||||
const (
|
||||
VacUnspecified VacancyStatus = "unspecified"
|
||||
VacNew VacancyStatus = "new"
|
||||
VacPending VacancyStatus = "pending"
|
||||
VacApproved VacancyStatus = "approved"
|
||||
VacRejected VacancyStatus = "rejected"
|
||||
)
|
||||
|
||||
func NewVacancyStatus(status *string) *VacancyStatus {
|
||||
if status == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
res := VacancyStatus(*status)
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
// <=============================>
|
||||
// Common Vacancy types
|
||||
// <=============================>
|
||||
|
||||
type (
|
||||
Vacancy struct {
|
||||
Id string
|
||||
Company VacancyCompanyInfo
|
||||
Name string
|
||||
Address string
|
||||
WorkFormat string
|
||||
AgentReward *int32
|
||||
SalaryTop int32
|
||||
SalaryBottom int32
|
||||
Requirements *string
|
||||
Responsibilities *string
|
||||
ExtraInfo *string
|
||||
Region string
|
||||
IsArchived bool
|
||||
TargetAction VacancyTargetAction
|
||||
Moderation VacancyModeration
|
||||
RequiredCandidates int32
|
||||
CurrentCandidates int32
|
||||
ExtraFields *string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
VacancyCompanyInfo struct {
|
||||
Id string
|
||||
Name string
|
||||
LogoLink *string
|
||||
}
|
||||
|
||||
VacancyTargetAction struct {
|
||||
Action string
|
||||
Duration int32
|
||||
}
|
||||
|
||||
VacancyModeration struct {
|
||||
Status VacancyStatus
|
||||
DescriptionHistory []VacancyModerationDescription
|
||||
}
|
||||
|
||||
VacancyModerationDescription struct {
|
||||
Description *string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
)
|
||||
|
||||
// <=============================>
|
||||
// Get Vacancy List
|
||||
// <=============================>
|
||||
|
||||
type (
|
||||
VacancyListGetRequest struct {
|
||||
Filters *VacancyListFilters
|
||||
Page uint64
|
||||
PageSize uint64
|
||||
}
|
||||
|
||||
VacancyListFilters struct {
|
||||
DistributorId *string
|
||||
CompanyId *string
|
||||
VacancyId *string
|
||||
Region *string
|
||||
SalaryBottom *int32
|
||||
SalaryTop *int32
|
||||
IsArchived *bool
|
||||
Status *VacancyStatus
|
||||
}
|
||||
|
||||
VacancyListGetResponse struct {
|
||||
Vacancies []Vacancy
|
||||
}
|
||||
)
|
||||
|
||||
// <=============================>
|
||||
// Create Vacancy
|
||||
// <=============================>
|
||||
|
||||
type (
|
||||
VacancyCreateRequest struct {
|
||||
CompanyId string
|
||||
Name string
|
||||
Address string
|
||||
WorkFormat string
|
||||
AgentReward *int32
|
||||
SalaryTop int32
|
||||
SalaryBottom int32
|
||||
Requirements *string
|
||||
Responsibilities *string
|
||||
ExtraInfo *string
|
||||
Region string
|
||||
TargetAction VacancyTargetAction
|
||||
RequiredCandidates int32
|
||||
CurrentCandidates int32
|
||||
ExtraFields *string
|
||||
}
|
||||
|
||||
VacancyCreateResponse struct {
|
||||
Id string
|
||||
}
|
||||
)
|
||||
|
||||
// <=============================>
|
||||
// Update Vacancy
|
||||
// <=============================>
|
||||
|
||||
type (
|
||||
VacancyUpdateRequest struct {
|
||||
Id string
|
||||
Name *string
|
||||
Address *string
|
||||
WorkFormat *string
|
||||
AgentReward *int32
|
||||
SalaryTop *int32
|
||||
SalaryBottom *int32
|
||||
Requirements *string
|
||||
Responsibilities *string
|
||||
ExtraInfo *string
|
||||
Region *string
|
||||
TargetAction VacancyTargetActionForUpdate
|
||||
RequiredCandidates *int32
|
||||
ExtraFields *string
|
||||
}
|
||||
|
||||
VacancyTargetActionForUpdate struct {
|
||||
Action *string
|
||||
Duration *int32
|
||||
}
|
||||
|
||||
VacancyUpdateResponse struct {
|
||||
}
|
||||
)
|
||||
|
||||
// <=============================>
|
||||
// Delete Vacancy
|
||||
// <=============================>
|
||||
|
||||
type (
|
||||
VacancyDeleteRequest struct {
|
||||
Id string
|
||||
}
|
||||
|
||||
VacancyDeleteResponse struct {
|
||||
}
|
||||
)
|
||||
|
||||
// <=============================>
|
||||
// Send Vacancy to Moderation
|
||||
// <=============================>
|
||||
|
||||
type (
|
||||
SendVacancyToModerationRequest struct {
|
||||
Id string
|
||||
}
|
||||
|
||||
SendVacancyToModerationResponse struct {
|
||||
}
|
||||
)
|
||||
17
internal/feed/errors.go
Normal file
17
internal/feed/errors.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package feed
|
||||
|
||||
import "errors"
|
||||
|
||||
// ะัะธะฑะบะธ, ัะฒัะทะฐะฝะฝัะต ั ะฒะฐะปะธะดะฐัะธะตะน ะฒั
ะพะดะฝัั
ะดะฐะฝะฝัั
|
||||
var (
|
||||
// ErrCancellationReasonRequired ะะพะทะฒัะฐัะฐะตััั ะฟัะธ ะฟะพะฟััะบะต ะพัะผะตะฝั ัะพะฑััะธั ะฑะตะท ัะบะฐะทะฐะฝะธั ะฟัะธัะธะฝั
|
||||
ErrCancellationReasonRequired = errors.New("cancellation reason is required for feed event cancellation")
|
||||
// ErrAttachmentIDRequired ะะพะทะฒัะฐัะฐะตััั ะฟัะธ ะฟะพะฟััะบะต ะพัะผะตะฝั ัะพะฑััะธั ะฑะตะท ัะบะฐะทะฐะฝะธั Id ะฒะปะพะถะตะฝะธั
|
||||
ErrAttachmentIDRequired = errors.New("attachment Id is required for feed event cancellation")
|
||||
// ErrUserIDRequired ะะพะทะฒัะฐัะฐะตััั ะฟัะธ ะฟะพะฟััะบะต ะพัะผะตะฝั ัะพะฑััะธั ะฑะตะท ัะบะฐะทะฐะฝะธั Id ะฟะพะปัะทะพะฒะฐัะตะปั
|
||||
ErrUserIDRequired = errors.New("user Id cancellation is required for feed event cancellation")
|
||||
// ErrCreationInvalidData ะะพะทะฒัะฐัะฐะตััั ะฟัะธ ะฟะพะฟััะบะต ัะพะทะดะฐะฝะธั ัะพะฑััะธั ะฑะตะท ัะบะฐะทะฐะฝะธั Id ะฟะพะปัะทะพะฒะฐัะตะปั, ะธะปะธ ัะธะฟะฐ ัะพะฑััะธั, ะธะปะธ ัะพะพะฑัะตะฝะธั
|
||||
ErrCreationInvalidData = errors.New("invalid event data provided for creation (missing owner, type, or message)")
|
||||
|
||||
ErrNotFound = errors.New("not found")
|
||||
)
|
||||
9
internal/feed/filters.go
Normal file
9
internal/feed/filters.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package feed
|
||||
|
||||
type Filter struct {
|
||||
OwnerID string // Id ะฟะพะปัะทะพะฒะฐัะตะปั
|
||||
EventTypes []EventType // ะคะธะปััั ะฟะพ ัะธะฟะฐะผ ัะพะฑััะธะน
|
||||
Limit uint64 // ะะธะผะธั ะทะฐะฟะธัะตะน
|
||||
Offset uint64 // ะกะผะตัะตะฝะธะต
|
||||
ShowCancelled bool // ะะพะบะฐะทัะฒะฐัั ะพัะผะตะฝะตะฝะฝัะต
|
||||
}
|
||||
101
internal/feed/handlers.go
Normal file
101
internal/feed/handlers.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package feed
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
Service *Service
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewFeedHandler(logger *slog.Logger, service *Service) *Handler {
|
||||
return &Handler{
|
||||
Service: service,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) CreateEvent(ctx context.Context, event *Event) error {
|
||||
if event == nil {
|
||||
h.logger.Error("CreateEvent: empty event",
|
||||
slog.String("error", ErrCreationInvalidData.Error()))
|
||||
|
||||
return ErrCreationInvalidData
|
||||
}
|
||||
|
||||
h.logger.Debug("CreateEvent: Attempting to create event",
|
||||
slog.String("type", event.EventType.String()),
|
||||
slog.String("ownerID", event.OwnerId))
|
||||
|
||||
if event.OwnerId == "" || event.EventType == "" || event.Message == "" {
|
||||
h.logger.Error("CreateEvent: Validation error",
|
||||
slog.String("error", ErrCreationInvalidData.Error()),
|
||||
slog.String("type", event.EventType.String()),
|
||||
slog.String("ownerID", event.OwnerId),
|
||||
slog.String("message", event.Message),
|
||||
)
|
||||
|
||||
return ErrCreationInvalidData
|
||||
}
|
||||
|
||||
if err := h.Service.AddUserEvent(ctx, event); err != nil {
|
||||
h.logger.Error("CreateEvent: Service error",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("type", event.EventType.String()),
|
||||
slog.String("ownerID", event.OwnerId))
|
||||
|
||||
return fmt.Errorf("Error creating event: %w", err)
|
||||
}
|
||||
|
||||
h.logger.Debug("CreateEvent: Successfully created event",
|
||||
slog.String("eventID", event.Id),
|
||||
slog.String("type", event.EventType.String()),
|
||||
slog.String("ownerID", event.OwnerId))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) CancelEvent(ctx context.Context, attachmentId string, reason string) error {
|
||||
h.logger.Debug("CancelEvent: Attempting to cancel event",
|
||||
slog.String("attachmentID", attachmentId),
|
||||
)
|
||||
|
||||
if attachmentId == "" {
|
||||
h.logger.Error("CancelEvent: Validation error",
|
||||
slog.String("error", "attachment Id is required"))
|
||||
|
||||
return ErrAttachmentIDRequired
|
||||
}
|
||||
|
||||
if reason == "" {
|
||||
h.logger.Error("CancelEvent: Validation error",
|
||||
slog.String("error", "cancellation reason is required"),
|
||||
slog.String("attachmentID", attachmentId))
|
||||
|
||||
return ErrCancellationReasonRequired
|
||||
}
|
||||
|
||||
if err := h.Service.CancelEvents(ctx, attachmentId, reason); err != nil {
|
||||
h.logger.Error("CancelEvent: Service error",
|
||||
slog.String("error", err.Error()),
|
||||
slog.String("attachmentID", attachmentId))
|
||||
|
||||
return fmt.Errorf("error cancelling event: %w", err)
|
||||
}
|
||||
|
||||
h.logger.Debug("CancelEvent: Successfully cancelled event",
|
||||
slog.String("attachmentID", attachmentId))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) GetCompanyIdByUidTmp(ctx context.Context, ownerID string) ([]string, error) {
|
||||
return h.Service.GetCompanyIdsByUid(ctx, ownerID)
|
||||
}
|
||||
|
||||
func (h *Handler) GetAgentIdBySubmissionId(ctx context.Context, submissionId string) (string, error) {
|
||||
return h.Service.GetAgentIdBySubmissionId(ctx, submissionId)
|
||||
}
|
||||
91
internal/feed/models.go
Normal file
91
internal/feed/models.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package feed
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
EventType string
|
||||
UserRole string
|
||||
Visibility string
|
||||
)
|
||||
|
||||
const (
|
||||
// User Roles
|
||||
RoleAgent UserRole = "agent"
|
||||
RoleDistributor UserRole = "distributor"
|
||||
|
||||
// Visibility Types
|
||||
VisibilityPublic Visibility = "public" // ะะพะบะฐะทัะฒะฐะตััั ะฒัะตะผ ะฐะณะตะฝัะฐะผ(ะธะฝะธัะธะฐัะพั ะผะพะถะตั ะฑััั ัะพะปัะบะพ ะดะธััั)
|
||||
VisibilityPrivate Visibility = "private" // ะะพะบะฐะทัะฒะฐะตััั ัะพะปัะบะพ ะธะฝะธัะธะฐัะพัั ัะพะฑััะธั
|
||||
VisibilityCompanyWide Visibility = "company_wide" // ะะพะบะฐะทัะฒะฐะตััั ะฒัะตะผ ะฒ ะบะพะผะฟะฐะฝะธะธ
|
||||
|
||||
// Event Types (Common)
|
||||
EventWelcome EventType = "welcome"
|
||||
EventNewCompanyMember EventType = "new_company_member"
|
||||
|
||||
// Users Events
|
||||
EventProfileChanged EventType = "profile_changed"
|
||||
EventCompanyCreated EventType = "company_created"
|
||||
EventCompanyChanged EventType = "company_changed"
|
||||
EventVacancyCreated EventType = "vacancy_created"
|
||||
EventVacancyChanged EventType = "vacancy_changed"
|
||||
EventVacancyModerationSent EventType = "vacancy_moderation_sent"
|
||||
EventSubmissionStatusChanged EventType = "submission_status_changed"
|
||||
EventTransactionCreated EventType = "transaction_created"
|
||||
EventBankAccountChanged EventType = "bank_details_changed"
|
||||
EventBankAccountCreated EventType = "bank_account_created"
|
||||
EventPostAnketa EventType = "post_anketa"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Id string `json:"id" db:"id"`
|
||||
OwnerId string `json:"owner_id" db:"owner_id"`
|
||||
OwnerType UserRole `json:"owner_type" db:"owner_type"`
|
||||
Message string `json:"message" db:"message"`
|
||||
Visibility Visibility `json:"visibility" db:"visibility"`
|
||||
CompanyID *string `json:"company_id,omitempty"`
|
||||
EventType EventType `json:"event_type" db:"event_type"`
|
||||
Payload EventPayload `json:"payload" db:"payload"`
|
||||
IsCancelled bool `json:"is_cancelled" db:"is_cancelled"`
|
||||
CancellationReason *string `json:"cancellation_reason" db:"cancellation_reason"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
func (e EventType) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (u UserRole) String() string {
|
||||
return string(u)
|
||||
}
|
||||
|
||||
func (e *Event) processNullableFields(companyID, cancellationReason sql.NullString) {
|
||||
if companyID.Valid {
|
||||
e.CompanyID = &companyID.String
|
||||
}
|
||||
|
||||
if cancellationReason.Valid {
|
||||
e.CancellationReason = &cancellationReason.String
|
||||
}
|
||||
}
|
||||
|
||||
type EventPayload struct {
|
||||
AttachmentId string `json:"attachment_id" db:"attachment_id"`
|
||||
AttachmentType AttachmentType `json:"attachment_type" db:"attachment_type"`
|
||||
AdditionalReceiver string `json:"additional_receiver,omitempty" db:"additional_receiver"` // may be uid or company id
|
||||
CustomData string `json:"custom_data,omitempty"`
|
||||
}
|
||||
|
||||
type AttachmentType string
|
||||
|
||||
const (
|
||||
AttachmentTypeVacancy AttachmentType = "vacancy"
|
||||
AttachmentTypeCV AttachmentType = "cv"
|
||||
AttachmentTypeProfile AttachmentType = "profile"
|
||||
AttachmentTypeCompany AttachmentType = "company"
|
||||
AttachmentTypeBankAccount AttachmentType = "bank_account"
|
||||
AttachmentTypeSubmission AttachmentType = "submission"
|
||||
)
|
||||
325
internal/feed/service.go
Normal file
325
internal/feed/service.go
Normal file
@@ -0,0 +1,325 @@
|
||||
package feed
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/constants"
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/database"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/google/uuid"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db *sql.DB
|
||||
schemaName string
|
||||
logger *slog.Logger
|
||||
dbClient database.Client //nolint:unused // TODO: ะฟะตัะตะฟะธัะฐัั ะฝะฐ ััะพะณะพ ะบะปะธะตะฝัะฐ ะฒ ัะฐะผะบะฐั
https://tracker.yandex.ru/MOLVARAZRABOTKA-363
|
||||
}
|
||||
|
||||
func NewService(dbUrl, schemaName string, logger *slog.Logger, dbClient database.Client) (*Service, error) {
|
||||
db, err := sql.Open("postgres", dbUrl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening database connection: %w", err)
|
||||
}
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("error pinging database: %w", err)
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
schemaName: schemaName,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetUserEvents(ctx context.Context, filter Filter, userType string) ([]Event, error) {
|
||||
companyIDs, err := s.GetCompanyIdsByUid(ctx, filter.OwnerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
||||
|
||||
baseQuery := psql.Select(
|
||||
"id", "owner_id", "owner_type", "message", "event_type", "visibility", "company_id",
|
||||
"payload", "is_cancelled", "cancellation_reason", "created_at", "updated_at",
|
||||
).From(fmt.Sprintf("%s.%s", pq.QuoteIdentifier(s.schemaName), constants.FeedEventsTableName))
|
||||
|
||||
conditions := []sq.Sqlizer{
|
||||
sq.Eq{"owner_id": filter.OwnerID},
|
||||
sq.And{
|
||||
sq.Eq{"visibility": VisibilityCompanyWide},
|
||||
sq.Expr("company_id = ANY(?)", pq.Array(companyIDs)),
|
||||
},
|
||||
sq.And{
|
||||
sq.Expr("payload->>'additional_receiver' IS NOT NULL"),
|
||||
sq.Or{
|
||||
sq.Expr("(payload->>'additional_receiver')::uuid = ?::uuid", filter.OwnerID),
|
||||
sq.Expr("(payload->>'additional_receiver')::text = ANY(?)", pq.Array(companyIDs)),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if userType == RoleAgent.String() {
|
||||
conditions = append(conditions, sq.Eq{"visibility": VisibilityPublic})
|
||||
}
|
||||
|
||||
query := baseQuery.Where(sq.Or(conditions))
|
||||
|
||||
if len(filter.EventTypes) > 0 {
|
||||
query = query.Where(sq.Expr("event_type = ANY(?)", pq.Array(filter.EventTypes)))
|
||||
}
|
||||
|
||||
if !filter.ShowCancelled {
|
||||
query = query.Where(sq.Eq{"is_cancelled": false})
|
||||
}
|
||||
|
||||
query = query.OrderBy("created_at DESC").
|
||||
Limit(filter.Limit).
|
||||
Offset(filter.Offset)
|
||||
|
||||
sqlQuery, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build query: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Debug("Executing GetUserEvents query", "query", sqlQuery, "args", args)
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, sqlQuery, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query events: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var events []Event
|
||||
|
||||
for rows.Next() {
|
||||
event, err := s.scanEventRow(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error iterating event rows: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Debug("GetUserEvents completed successfully",
|
||||
slog.Int("count", len(events)),
|
||||
slog.String("owner_id", filter.OwnerID),
|
||||
slog.String("user_type", userType),
|
||||
slog.String("company_ids", fmt.Sprintf("%v", companyIDs)),
|
||||
slog.Any("filter", filter),
|
||||
)
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetCompanyIdsByUid(ctx context.Context, ownerID string) ([]string, error) {
|
||||
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
||||
query := psql.Select("company_id").
|
||||
From(fmt.Sprintf("%s.%s", pq.QuoteIdentifier(s.schemaName), constants.ClientTableName)).
|
||||
Where(sq.Eq{"uid": ownerID})
|
||||
|
||||
sqlQuery, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build query: %w", err)
|
||||
}
|
||||
|
||||
var companyID sql.NullString
|
||||
|
||||
if err = s.db.QueryRowContext(ctx, sqlQuery, args...).Scan(&companyID); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to get company Id: %w", err)
|
||||
}
|
||||
|
||||
if companyID.Valid {
|
||||
return []string{companyID.String}, nil
|
||||
}
|
||||
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
func (s *Service) scanEventRow(rows *sql.Rows) (Event, error) {
|
||||
var (
|
||||
event Event
|
||||
payload []byte
|
||||
companyIDScan sql.NullString
|
||||
cancellationReasonScan sql.NullString
|
||||
)
|
||||
|
||||
if err := rows.Scan(
|
||||
&event.Id,
|
||||
&event.OwnerId,
|
||||
&event.OwnerType,
|
||||
&event.Message,
|
||||
&event.EventType,
|
||||
&event.Visibility,
|
||||
&companyIDScan,
|
||||
&payload,
|
||||
&event.IsCancelled,
|
||||
&cancellationReasonScan,
|
||||
&event.CreatedAt,
|
||||
&event.UpdatedAt,
|
||||
); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return Event{}, nil
|
||||
}
|
||||
|
||||
return Event{}, fmt.Errorf("failed to scan event: %w", err)
|
||||
}
|
||||
|
||||
event.processNullableFields(companyIDScan, cancellationReasonScan)
|
||||
event.processPayload(payload)
|
||||
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func (e *Event) processPayload(payload []byte) {
|
||||
if len(payload) == 0 {
|
||||
e.Payload = EventPayload{}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(payload, &e.Payload); err != nil {
|
||||
e.Payload = EventPayload{}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) AddUserEvent(ctx context.Context, event *Event) error {
|
||||
if event == nil {
|
||||
return ErrCreationInvalidData
|
||||
}
|
||||
|
||||
if event.Id == "" {
|
||||
event.Id = "MSG_" + uuid.New().String()
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
event.CreatedAt = now
|
||||
event.UpdatedAt = now
|
||||
|
||||
payloadBytes, err := json.Marshal(event.Payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal payload: %w", err)
|
||||
}
|
||||
|
||||
var payloadArg interface{} = payloadBytes
|
||||
if string(payloadBytes) == "null" || string(payloadBytes) == "{}" {
|
||||
payloadArg = nil
|
||||
}
|
||||
|
||||
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
||||
query := psql.Insert(fmt.Sprintf("%s.%s", pq.QuoteIdentifier(s.schemaName), constants.FeedEventsTableName)).
|
||||
Columns(
|
||||
"id", "owner_id", "owner_type", "message", "event_type", "visibility", "company_id",
|
||||
"payload", "is_cancelled", "cancellation_reason", "created_at", "updated_at",
|
||||
).
|
||||
Values(
|
||||
event.Id,
|
||||
event.OwnerId,
|
||||
event.OwnerType,
|
||||
event.Message,
|
||||
event.EventType,
|
||||
event.Visibility,
|
||||
event.CompanyID,
|
||||
payloadArg,
|
||||
event.IsCancelled,
|
||||
event.CancellationReason,
|
||||
event.CreatedAt,
|
||||
event.UpdatedAt,
|
||||
)
|
||||
|
||||
sqlQuery, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build query: %w", err)
|
||||
}
|
||||
|
||||
_, err = s.db.ExecContext(ctx, sqlQuery, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert event: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Debug("Event added successfully", "event_id", event.Id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) CancelEvents(ctx context.Context, attachmentID, cancellationReason string) error {
|
||||
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
||||
query := psql.Update(fmt.Sprintf("%s.%s", pq.QuoteIdentifier(s.schemaName), constants.FeedEventsTableName)).
|
||||
Set("is_cancelled", true).
|
||||
Set("cancellation_reason", cancellationReason).
|
||||
Set("updated_at", sq.Expr("NOW()")).
|
||||
Where(sq.Expr("payload ->> 'attachment_id' = ?", attachmentID)).
|
||||
Where(sq.Eq{"is_cancelled": false})
|
||||
|
||||
sqlQuery, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build query: %w", err)
|
||||
}
|
||||
|
||||
result, err := s.db.ExecContext(ctx, sqlQuery, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute cancel events query for attachmentID %s: %w", attachmentID, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get affected rows for attachmentID %s: %w", attachmentID, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
s.logger.Debug("No active events found or updated for cancellation", "attachment_id", attachmentID)
|
||||
} else {
|
||||
s.logger.Debug("Events cancelled successfully", "attachment_id", attachmentID, "count", rowsAffected)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) GetAgentIdBySubmissionId(ctx context.Context, submissionId string) (string, error) {
|
||||
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
|
||||
query := psql.Select("uid").
|
||||
From(fmt.Sprintf("%s.%s", pq.QuoteIdentifier(s.schemaName), constants.SubmissionTableName)).
|
||||
Where(sq.Eq{"id": submissionId})
|
||||
|
||||
sqlQuery, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to build query: %w", err)
|
||||
}
|
||||
|
||||
var agentId string
|
||||
|
||||
if err = s.db.QueryRowContext(ctx, sqlQuery, args...).Scan(&agentId); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("failed to get agent Id: %w", err)
|
||||
}
|
||||
|
||||
if agentId == "" {
|
||||
s.logger.Debug("Agent Id not found", "submission_id", submissionId)
|
||||
|
||||
return "", ErrNotFound
|
||||
}
|
||||
|
||||
return agentId, nil
|
||||
}
|
||||
15
internal/file_manager/errors.go
Normal file
15
internal/file_manager/errors.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package filemanager
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrInvalidFileType = errors.New("invalid file type")
|
||||
ErrParameterNotFound = errors.New("parameter required, but not found")
|
||||
ErrFileNotFound = errors.New("file not found")
|
||||
|
||||
ErrFileAlreadyExists = errors.New("file already exists")
|
||||
ErrFailedUploadFile = errors.New("failed to upload file to S3")
|
||||
ErrFailedToCacheUrl = errors.New("failed to cache URL")
|
||||
ErrMsgReadFile = errors.New("failed to read file")
|
||||
ErrMsgGetPresignedURL = errors.New("failed to get presigned URL")
|
||||
)
|
||||
127
internal/file_manager/local_storage/storage.go
Normal file
127
internal/file_manager/local_storage/storage.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
filemanager "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/file_manager"
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
basePath string
|
||||
}
|
||||
|
||||
func New(basePath string) *Storage {
|
||||
return &Storage{
|
||||
basePath: basePath,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Storage) GetFilePaths(
|
||||
ctx context.Context,
|
||||
fileType filemanager.FileType,
|
||||
parameters filemanager.ParameterTable,
|
||||
) ([]string, error) {
|
||||
var dirPath string
|
||||
|
||||
fileNames := make([]string, 0)
|
||||
|
||||
switch fileType {
|
||||
case filemanager.AvatarFileType:
|
||||
userId, ok := parameters[filemanager.UserIdParam]
|
||||
if !ok {
|
||||
return nil, filemanager.ErrParameterNotFound
|
||||
}
|
||||
|
||||
dirPath = filepath.Join(s.basePath, userId.(string), "avatars")
|
||||
|
||||
case filemanager.CVFileType:
|
||||
submId, ok := parameters[filemanager.SubmissionIdParam]
|
||||
if !ok {
|
||||
return nil, filemanager.ErrParameterNotFound
|
||||
}
|
||||
|
||||
dirPath = filepath.Join(s.basePath, "CVs", submId.(string))
|
||||
|
||||
default:
|
||||
return nil, filemanager.ErrInvalidFileType
|
||||
}
|
||||
|
||||
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
|
||||
return nil, filemanager.ErrFileNotFound
|
||||
}
|
||||
|
||||
if err := filepath.WalkDir(dirPath, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
fileNames = append(fileNames, path)
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(fileNames) == 0 {
|
||||
return nil, filemanager.ErrFileNotFound
|
||||
}
|
||||
|
||||
return fileNames, nil
|
||||
}
|
||||
|
||||
func (s *Storage) SaveFile(
|
||||
ctx context.Context,
|
||||
fileType filemanager.FileType,
|
||||
file multipart.File,
|
||||
fileHeader *multipart.FileHeader,
|
||||
parameters filemanager.ParameterTable,
|
||||
) error {
|
||||
var dirPath string
|
||||
|
||||
switch fileType {
|
||||
case filemanager.AvatarFileType:
|
||||
userId, ok := parameters[filemanager.UserIdParam]
|
||||
if !ok {
|
||||
return filemanager.ErrParameterNotFound
|
||||
}
|
||||
|
||||
dirPath = filepath.Join(s.basePath, userId.(string), "avatars")
|
||||
|
||||
case filemanager.CVFileType:
|
||||
submId, ok := parameters[filemanager.SubmissionIdParam]
|
||||
if !ok {
|
||||
return filemanager.ErrParameterNotFound
|
||||
}
|
||||
|
||||
dirPath = filepath.Join(s.basePath, "CVs", submId.(string))
|
||||
|
||||
default:
|
||||
return filemanager.ErrInvalidFileType
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filePath := filepath.Join(dirPath, fileHeader.Filename)
|
||||
|
||||
dstFile, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
if _, err := io.Copy(dstFile, file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
39
internal/file_manager/manager.go
Normal file
39
internal/file_manager/manager.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package filemanager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mime/multipart"
|
||||
)
|
||||
|
||||
type FileType string
|
||||
|
||||
const (
|
||||
AvatarFileType FileType = "avatar"
|
||||
CVFileType FileType = "cv"
|
||||
)
|
||||
|
||||
type ParameterType string
|
||||
|
||||
const (
|
||||
UserIdParam ParameterType = "user_id"
|
||||
VacancyIdParam ParameterType = "vacancy_id"
|
||||
SubmissionIdParam ParameterType = "submission_id"
|
||||
)
|
||||
|
||||
type ParameterTable map[ParameterType]any
|
||||
|
||||
type UserFileManager interface {
|
||||
GetFilePaths(
|
||||
ctx context.Context,
|
||||
fileType FileType,
|
||||
parameters ParameterTable,
|
||||
) ([]string, error)
|
||||
|
||||
SaveFile(
|
||||
ctx context.Context,
|
||||
fileType FileType,
|
||||
file multipart.File,
|
||||
fileHeader *multipart.FileHeader,
|
||||
parameters ParameterTable,
|
||||
) error
|
||||
}
|
||||
156
internal/file_manager/s3_storage/storage.go
Normal file
156
internal/file_manager/s3_storage/storage.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package s3_storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/cache"
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/constants"
|
||||
filemanager "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/file_manager"
|
||||
"git-molva.ru/Molva/molva-backend/services/api_gateway/internal/object_storage"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type S3Storage struct {
|
||||
objectStorage object_storage.Client
|
||||
cacheClient cache.Client
|
||||
}
|
||||
|
||||
func NewS3Storage(objectStorage object_storage.Client, cacheClient cache.Client) *S3Storage {
|
||||
return &S3Storage{
|
||||
objectStorage: objectStorage,
|
||||
cacheClient: cacheClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *S3Storage) GetFilePaths(
|
||||
ctx context.Context,
|
||||
fileType filemanager.FileType,
|
||||
parameters filemanager.ParameterTable,
|
||||
) ([]string, error) {
|
||||
objectKey, category, err := s.buildObjectKey(fileType, parameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ะกะฝะฐัะฐะปะฐ ะฟััะฐะตะผัั ะฟะพะปััะธัั URL ะธะท ะบััะฐ
|
||||
cachedURL, err := s.getCachedURL(ctx, objectKey, category)
|
||||
if err == nil && cachedURL != "" {
|
||||
return []string{cachedURL}, nil
|
||||
}
|
||||
|
||||
// ะะพะปััะฐะตะผ ะปะธะฝะบ ะธะท s3
|
||||
url, err := s.objectStorage.GetPresignedLink(ctx, objectKey, category,
|
||||
object_storage.LinkOptions{
|
||||
TTL: ptr.Duration(constants.DefaultFileTTL),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, object_storage.ErrObjectNotFound) {
|
||||
return nil, filemanager.ErrFileNotFound
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%s: %w", filemanager.ErrMsgGetPresignedURL, err)
|
||||
}
|
||||
|
||||
// ะััะธััะตะผ URL
|
||||
if err := s.cacheURL(ctx, objectKey, category, url); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", filemanager.ErrFailedToCacheUrl, err)
|
||||
}
|
||||
|
||||
return []string{url}, nil
|
||||
}
|
||||
|
||||
func (s *S3Storage) SaveFile(
|
||||
ctx context.Context,
|
||||
fileType filemanager.FileType,
|
||||
file multipart.File,
|
||||
fileHeader *multipart.FileHeader,
|
||||
parameters filemanager.ParameterTable,
|
||||
) error {
|
||||
objectKey, category, err := s.buildObjectKey(fileType, parameters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := buf.ReadFrom(file); err != nil {
|
||||
return fmt.Errorf("%s: %w", filemanager.ErrMsgReadFile, err)
|
||||
}
|
||||
|
||||
// ะะฟัะตะดะตะปัะตะผ ัะธะฟ ะบะพะฝัะตะฝัะฐ
|
||||
contentType := s.getContentType(fileHeader.Filename)
|
||||
|
||||
// ะะฐะณััะถะฐะตะผ ัะฐะนะป ะฒ S3
|
||||
err = s.objectStorage.PutNewObject(ctx, objectKey, category,
|
||||
bytes.NewReader(buf.Bytes()),
|
||||
object_storage.PutOptions{
|
||||
ContentType: contentType,
|
||||
})
|
||||
if err != nil {
|
||||
// ะะฑัะฐะฑะฐััะฒะฐะตะผ ัะปััะฐะน ัััะตััะฒัััะตะณะพ ัะฐะนะปะฐ
|
||||
if errors.Is(err, object_storage.ErrObjectAlreadyExists) {
|
||||
return filemanager.ErrFileAlreadyExists
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s: %w", filemanager.ErrFailedUploadFile, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ะกะพะทะดะฐะตะผ ะบะปัั ะพะฑัะตะบัะฐ S3 ะฝะฐ ะพัะฝะพะฒะต ัะธะฟะฐ ัะฐะนะปะฐ ะธ ะฟะฐัะฐะผะตััะพะฒ
|
||||
func (s *S3Storage) buildObjectKey(fileType filemanager.FileType, parameters filemanager.ParameterTable) (string, object_storage.Category, error) {
|
||||
switch fileType {
|
||||
case filemanager.AvatarFileType:
|
||||
userID, ok := parameters[filemanager.UserIdParam]
|
||||
if !ok {
|
||||
return "", "", filemanager.ErrParameterNotFound
|
||||
}
|
||||
|
||||
return fmt.Sprintf(objectKeyAvatarTemplate, userID), object_storage.LogoCategory, nil
|
||||
|
||||
case filemanager.CVFileType:
|
||||
submissionID, ok := parameters[filemanager.SubmissionIdParam]
|
||||
if !ok {
|
||||
return "", "", filemanager.ErrParameterNotFound
|
||||
}
|
||||
|
||||
return fmt.Sprintf(objectKeyCVTemplate, submissionID), object_storage.DocumentCategory, nil
|
||||
|
||||
default:
|
||||
return "", "", filemanager.ErrInvalidFileType
|
||||
}
|
||||
}
|
||||
|
||||
// getContentType ะพะฟัะตะดะตะปัะตั ัะธะฟ ะบะพะฝัะตะฝัะฐ ะฟะพ ัะฐััะธัะตะฝะธั
|
||||
func (s *S3Storage) getContentType(filename string) string {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
|
||||
if ct, ok := ContentTypes[ext]; ok {
|
||||
return ct
|
||||
}
|
||||
|
||||
if ct := mime.TypeByExtension(ext); ct != "" {
|
||||
return ct
|
||||
}
|
||||
|
||||
return defaultContentType
|
||||
}
|
||||
|
||||
func (s *S3Storage) getCachedURL(ctx context.Context, objectKey string, category object_storage.Category) (string, error) {
|
||||
cacheKey := fmt.Sprintf(cacheKeyTemplate, category, objectKey)
|
||||
return s.cacheClient.Get(ctx, cacheKey, cache.DocumentsValueType)
|
||||
}
|
||||
|
||||
func (s *S3Storage) cacheURL(ctx context.Context, objectKey string, category object_storage.Category, url string) error {
|
||||
cacheKey := fmt.Sprintf(cacheKeyTemplate, category, objectKey)
|
||||
cacheDuration := constants.DefaultFileTTL - time.Hour
|
||||
|
||||
return s.cacheClient.Set(ctx, cacheKey, cache.DocumentsValueType, url, cacheDuration)
|
||||
}
|
||||
22
internal/file_manager/s3_storage/types.go
Normal file
22
internal/file_manager/s3_storage/types.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package s3_storage
|
||||
|
||||
var ContentTypes = map[string]string{
|
||||
".pdf": "application/pdf",
|
||||
".txt": "text/plain",
|
||||
".doc": "application/msword",
|
||||
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".rtf": "application/rtf",
|
||||
".odt": "application/vnd.oasis.opendocument.text",
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".webp": "image/webp",
|
||||
}
|
||||
|
||||
// ะะพะฝััะฐะฝัั ะดะปั ะบะปััะตะน
|
||||
const (
|
||||
objectKeyAvatarTemplate = "%s_avatar"
|
||||
objectKeyCVTemplate = "%s_cv"
|
||||
cacheKeyTemplate = "%s_%s"
|
||||
defaultContentType = "application/octet-stream"
|
||||
)
|
||||
65
internal/form_generator/generator.go
Normal file
65
internal/form_generator/generator.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package formgenerator
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"errors"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed templates/index.html
|
||||
var formTemplate string
|
||||
|
||||
//go:embed templates
|
||||
var formFS embed.FS
|
||||
|
||||
var (
|
||||
requiredFields []string = []string{
|
||||
"VacancyName",
|
||||
"Address",
|
||||
"WorkFormat",
|
||||
"SalaryTop",
|
||||
"SalaryBottom",
|
||||
"Requirements",
|
||||
"Responsibilities",
|
||||
"Description",
|
||||
|
||||
"VacancyId",
|
||||
"AgentId",
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("One of the required fields is not set")
|
||||
)
|
||||
|
||||
func GetFileSystem() (fs.FS, error) {
|
||||
formFs, err := fs.Sub(formFS, "templates")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return formFs, nil
|
||||
}
|
||||
|
||||
func GenerateForm(formData map[string]string) (string, error) {
|
||||
for _, field := range requiredFields {
|
||||
if _, ok := formData[field]; !ok {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
}
|
||||
|
||||
tmpl, err := template.New("index.html").Parse(formTemplate)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
|
||||
if err = tmpl.Execute(&buf, formData); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"DQUHFmFzc2V0cy9pbWFnZXMvbG9nby5zdmcMAQ0BBwVhc3NldAcWYXNzZXRzL2ltYWdlcy9sb2dvLnN2ZwcdYXNzZXRzL2ltYWdlcy9yZXN1bWUtZmlsZS5zdmcMAQ0BBwVhc3NldAcdYXNzZXRzL2ltYWdlcy9yZXN1bWUtZmlsZS5zdmcHHWFzc2V0cy9pbWFnZXMvcmVzdW1lLWxpbmsuc3ZnDAENAQcFYXNzZXQHHWFzc2V0cy9pbWFnZXMvcmVzdW1lLWxpbmsuc3ZnBxZhc3NldHMvaW1hZ2VzL3NlbmQuc3ZnDAENAQcFYXNzZXQHFmFzc2V0cy9pbWFnZXMvc2VuZC5zdmcHMnBhY2thZ2VzL2N1cGVydGlub19pY29ucy9hc3NldHMvQ3VwZXJ0aW5vSWNvbnMudHRmDAENAQcFYXNzZXQHMnBhY2thZ2VzL2N1cGVydGlub19pY29ucy9hc3NldHMvQ3VwZXJ0aW5vSWNvbnMudHRm"
|
||||
@@ -0,0 +1 @@
|
||||
{"assets/images/logo.svg":["assets/images/logo.svg"],"assets/images/resume-file.svg":["assets/images/resume-file.svg"],"assets/images/resume-link.svg":["assets/images/resume-link.svg"],"assets/images/send.svg":["assets/images/send.svg"],"packages/cupertino_icons/assets/CupertinoIcons.ttf":["packages/cupertino_icons/assets/CupertinoIcons.ttf"]}
|
||||
@@ -0,0 +1 @@
|
||||
[{"family":"MaterialIcons","fonts":[{"asset":"fonts/MaterialIcons-Regular.otf"}]},{"family":"packages/cupertino_icons/CupertinoIcons","fonts":[{"asset":"packages/cupertino_icons/assets/CupertinoIcons.ttf"}]}]
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg width="138" height="28" viewBox="0 0 138 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.1226 15.512L6.39303 0.519165H0V26.4807H7.23678V16.0636L13.2404 26.4807H16.7776L22.8462 15.7067L22.9111 26.4807H30.1154V0.519165H23.6575L15.1226 15.512Z" fill="#1454B2"/>
|
||||
<path d="M55.0384 1.7524C52.8641 0.584135 50.3978 0 47.6718 0C44.9459 0 42.4795 0.584135 40.3053 1.7524C38.131 2.92067 36.3786 4.54327 35.1454 6.58774C33.9122 8.63221 33.2632 10.9688 33.2632 13.5325C33.2632 16.0962 33.8798 18.4327 35.1454 20.4772C36.3786 22.5216 38.131 24.1442 40.3053 25.3125C42.4795 26.4808 44.9459 27.0649 47.6718 27.0649C50.3978 27.0649 52.8641 26.4808 55.0384 25.3125C57.2127 24.1442 58.9651 22.5216 60.1983 20.4772C61.4315 18.4327 62.0805 16.0962 62.0805 13.5325C62.0805 10.9688 61.4639 8.63221 60.1983 6.58774C58.9651 4.51082 57.2127 2.88822 55.0384 1.7524ZM50.9819 19.5685C50.0084 20.1526 48.8726 20.4447 47.6718 20.4447C46.4387 20.4447 45.3353 20.1526 44.3293 19.5685C43.3557 18.9844 42.5444 18.1731 41.9927 17.1346C41.4086 16.0962 41.1165 14.863 41.1165 13.5C41.1165 12.137 41.4086 10.9038 41.9927 9.86539C42.5769 8.82692 43.3557 8.01562 44.3293 7.43149C45.3028 6.84736 46.4387 6.55529 47.6718 6.55529C48.905 6.55529 50.0084 6.84736 50.9819 7.43149C51.9555 8.01562 52.7668 8.82692 53.3185 9.86539C53.9026 10.9038 54.1947 12.137 54.1947 13.5C54.1947 14.863 53.9026 16.0962 53.3185 17.1346C52.7668 18.1731 51.9879 19.0168 50.9819 19.5685Z" fill="#1454B2"/>
|
||||
<path d="M73.0167 0.519165H65.2283V26.4807H85.1213V20.185H73.0167V0.519165Z" fill="#1454B2"/>
|
||||
<path d="M90.833 0.519165H82.428L93.5265 26.4807H100.99L101.347 25.6694L90.833 0.519165Z" fill="#1454B2"/>
|
||||
<path d="M104.073 19.2439L112.121 0.519165H104.365L100.276 10.0925L104.073 19.2439Z" fill="#3B9BF2"/>
|
||||
<path d="M126.465 0.519165H119.423L125.492 15.6418H119.877H112.316L107.546 26.4807H115.529L117.541 21.548H127.861L129.84 26.4807H137.986L126.465 0.519165Z" fill="#1454B2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 365 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
File diff suppressed because one or more lines are too long
192
internal/form_generator/templates/canvaskit/canvaskit.js
Normal file
192
internal/form_generator/templates/canvaskit/canvaskit.js
Normal file
@@ -0,0 +1,192 @@
|
||||
|
||||
var CanvasKitInit = (() => {
|
||||
var _scriptName = import.meta.url;
|
||||
|
||||
return (
|
||||
function(moduleArg = {}) {
|
||||
var moduleRtn;
|
||||
|
||||
var r=moduleArg,aa,ca,da=new Promise((a,b)=>{aa=a;ca=b}),ea="object"==typeof window,ha="function"==typeof importScripts;
|
||||
(function(a){a.ce=a.ce||[];a.ce.push(function(){a.MakeSWCanvasSurface=function(b){var c=b,e="undefined"!==typeof OffscreenCanvas&&c instanceof OffscreenCanvas;if(!("undefined"!==typeof HTMLCanvasElement&&c instanceof HTMLCanvasElement||e||(c=document.getElementById(b),c)))throw"Canvas with id "+b+" was not found";if(b=a.MakeSurface(c.width,c.height))b.Ae=c;return b};a.MakeCanvasSurface||(a.MakeCanvasSurface=a.MakeSWCanvasSurface);a.MakeSurface=function(b,c){var e={width:b,height:c,colorType:a.ColorType.RGBA_8888,
|
||||
alphaType:a.AlphaType.Unpremul,colorSpace:a.ColorSpace.SRGB},f=b*c*4,k=a._malloc(f);if(e=a.Surface._makeRasterDirect(e,k,4*b))e.Ae=null,e.$e=b,e.Xe=c,e.Ye=f,e.He=k,e.getCanvas().clear(a.TRANSPARENT);return e};a.MakeRasterDirectSurface=function(b,c,e){return a.Surface._makeRasterDirect(b,c.byteOffset,e)};a.Surface.prototype.flush=function(b){a.$d(this.Zd);this._flush();if(this.Ae){var c=new Uint8ClampedArray(a.HEAPU8.buffer,this.He,this.Ye);c=new ImageData(c,this.$e,this.Xe);b?this.Ae.getContext("2d").putImageData(c,
|
||||
0,0,b[0],b[1],b[2]-b[0],b[3]-b[1]):this.Ae.getContext("2d").putImageData(c,0,0)}};a.Surface.prototype.dispose=function(){this.He&&a._free(this.He);this.delete()};a.$d=a.$d||function(){};a.Be=a.Be||function(){return null}})})(r);
|
||||
(function(a){a.ce=a.ce||[];a.ce.push(function(){function b(l,p,v){return l&&l.hasOwnProperty(p)?l[p]:v}function c(l){var p=ja(ka);ka[p]=l;return p}function e(l){return l.naturalHeight||l.videoHeight||l.displayHeight||l.height}function f(l){return l.naturalWidth||l.videoWidth||l.displayWidth||l.width}function k(l,p,v,w){l.bindTexture(l.TEXTURE_2D,p);w||v.alphaType!==a.AlphaType.Premul||l.pixelStorei(l.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!0);return p}function n(l,p,v){v||p.alphaType!==a.AlphaType.Premul||
|
||||
l.pixelStorei(l.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!1);l.bindTexture(l.TEXTURE_2D,null)}a.GetWebGLContext=function(l,p){if(!l)throw"null canvas passed into makeWebGLContext";var v={alpha:b(p,"alpha",1),depth:b(p,"depth",1),stencil:b(p,"stencil",8),antialias:b(p,"antialias",0),premultipliedAlpha:b(p,"premultipliedAlpha",1),preserveDrawingBuffer:b(p,"preserveDrawingBuffer",0),preferLowPowerToHighPerformance:b(p,"preferLowPowerToHighPerformance",0),failIfMajorPerformanceCaveat:b(p,"failIfMajorPerformanceCaveat",
|
||||
0),enableExtensionsByDefault:b(p,"enableExtensionsByDefault",1),explicitSwapControl:b(p,"explicitSwapControl",0),renderViaOffscreenBackBuffer:b(p,"renderViaOffscreenBackBuffer",0)};v.majorVersion=p&&p.majorVersion?p.majorVersion:"undefined"!==typeof WebGL2RenderingContext?2:1;if(v.explicitSwapControl)throw"explicitSwapControl is not supported";l=la(l,v);if(!l)return 0;oa(l);z.le.getExtension("WEBGL_debug_renderer_info");return l};a.deleteContext=function(l){z===pa[l]&&(z=null);"object"==typeof JSEvents&&
|
||||
JSEvents.Af(pa[l].le.canvas);pa[l]&&pa[l].le.canvas&&(pa[l].le.canvas.Ve=void 0);pa[l]=null};a._setTextureCleanup({deleteTexture:function(l,p){var v=ka[p];v&&pa[l].le.deleteTexture(v);ka[p]=null}});a.MakeWebGLContext=function(l){if(!this.$d(l))return null;var p=this._MakeGrContext();if(!p)return null;p.Zd=l;var v=p.delete.bind(p);p["delete"]=function(){a.$d(this.Zd);v()}.bind(p);return z.Je=p};a.MakeGrContext=a.MakeWebGLContext;a.GrDirectContext.prototype.getResourceCacheLimitBytes=function(){a.$d(this.Zd);
|
||||
this._getResourceCacheLimitBytes()};a.GrDirectContext.prototype.getResourceCacheUsageBytes=function(){a.$d(this.Zd);this._getResourceCacheUsageBytes()};a.GrDirectContext.prototype.releaseResourcesAndAbandonContext=function(){a.$d(this.Zd);this._releaseResourcesAndAbandonContext()};a.GrDirectContext.prototype.setResourceCacheLimitBytes=function(l){a.$d(this.Zd);this._setResourceCacheLimitBytes(l)};a.MakeOnScreenGLSurface=function(l,p,v,w,A,D){if(!this.$d(l.Zd))return null;p=void 0===A||void 0===D?
|
||||
this._MakeOnScreenGLSurface(l,p,v,w):this._MakeOnScreenGLSurface(l,p,v,w,A,D);if(!p)return null;p.Zd=l.Zd;return p};a.MakeRenderTarget=function(){var l=arguments[0];if(!this.$d(l.Zd))return null;if(3===arguments.length){var p=this._MakeRenderTargetWH(l,arguments[1],arguments[2]);if(!p)return null}else if(2===arguments.length){if(p=this._MakeRenderTargetII(l,arguments[1]),!p)return null}else return null;p.Zd=l.Zd;return p};a.MakeWebGLCanvasSurface=function(l,p,v){p=p||null;var w=l,A="undefined"!==
|
||||
typeof OffscreenCanvas&&w instanceof OffscreenCanvas;if(!("undefined"!==typeof HTMLCanvasElement&&w instanceof HTMLCanvasElement||A||(w=document.getElementById(l),w)))throw"Canvas with id "+l+" was not found";l=this.GetWebGLContext(w,v);if(!l||0>l)throw"failed to create webgl context: err "+l;l=this.MakeWebGLContext(l);p=this.MakeOnScreenGLSurface(l,w.width,w.height,p);return p?p:(p=w.cloneNode(!0),w.parentNode.replaceChild(p,w),p.classList.add("ck-replaced"),a.MakeSWCanvasSurface(p))};a.MakeCanvasSurface=
|
||||
a.MakeWebGLCanvasSurface;a.Surface.prototype.makeImageFromTexture=function(l,p){a.$d(this.Zd);l=c(l);if(p=this._makeImageFromTexture(this.Zd,l,p))p.ue=l;return p};a.Surface.prototype.makeImageFromTextureSource=function(l,p,v){p||={height:e(l),width:f(l),colorType:a.ColorType.RGBA_8888,alphaType:v?a.AlphaType.Premul:a.AlphaType.Unpremul};p.colorSpace||(p.colorSpace=a.ColorSpace.SRGB);a.$d(this.Zd);var w=z.le;v=k(w,w.createTexture(),p,v);2===z.version?w.texImage2D(w.TEXTURE_2D,0,w.RGBA,p.width,p.height,
|
||||
0,w.RGBA,w.UNSIGNED_BYTE,l):w.texImage2D(w.TEXTURE_2D,0,w.RGBA,w.RGBA,w.UNSIGNED_BYTE,l);n(w,p);this._resetContext();return this.makeImageFromTexture(v,p)};a.Surface.prototype.updateTextureFromSource=function(l,p,v){if(l.ue){a.$d(this.Zd);var w=l.getImageInfo(),A=z.le,D=k(A,ka[l.ue],w,v);2===z.version?A.texImage2D(A.TEXTURE_2D,0,A.RGBA,f(p),e(p),0,A.RGBA,A.UNSIGNED_BYTE,p):A.texImage2D(A.TEXTURE_2D,0,A.RGBA,A.RGBA,A.UNSIGNED_BYTE,p);n(A,w,v);this._resetContext();ka[l.ue]=null;l.ue=c(D);w.colorSpace=
|
||||
l.getColorSpace();p=this._makeImageFromTexture(this.Zd,l.ue,w);v=l.Yd.ae;A=l.Yd.ee;l.Yd.ae=p.Yd.ae;l.Yd.ee=p.Yd.ee;p.Yd.ae=v;p.Yd.ee=A;p.delete();w.colorSpace.delete()}};a.MakeLazyImageFromTextureSource=function(l,p,v){p||={height:e(l),width:f(l),colorType:a.ColorType.RGBA_8888,alphaType:v?a.AlphaType.Premul:a.AlphaType.Unpremul};p.colorSpace||(p.colorSpace=a.ColorSpace.SRGB);var w={makeTexture:function(){var A=z,D=A.le,I=k(D,D.createTexture(),p,v);2===A.version?D.texImage2D(D.TEXTURE_2D,0,D.RGBA,
|
||||
p.width,p.height,0,D.RGBA,D.UNSIGNED_BYTE,l):D.texImage2D(D.TEXTURE_2D,0,D.RGBA,D.RGBA,D.UNSIGNED_BYTE,l);n(D,p,v);return c(I)},freeSrc:function(){}};"VideoFrame"===l.constructor.name&&(w.freeSrc=function(){l.close()});return a.Image._makeFromGenerator(p,w)};a.$d=function(l){return l?oa(l):!1};a.Be=function(){return z&&z.Je&&!z.Je.isDeleted()?z.Je:null}})})(r);
|
||||
(function(a){function b(g){return(f(255*g[3])<<24|f(255*g[0])<<16|f(255*g[1])<<8|f(255*g[2])<<0)>>>0}function c(g){if(g&&g._ck)return g;if(g instanceof Float32Array){for(var d=Math.floor(g.length/4),h=new Uint32Array(d),m=0;m<d;m++)h[m]=b(g.slice(4*m,4*(m+1)));return h}if(g instanceof Uint32Array)return g;if(g instanceof Array&&g[0]instanceof Float32Array)return g.map(b)}function e(g){if(void 0===g)return 1;var d=parseFloat(g);return g&&-1!==g.indexOf("%")?d/100:d}function f(g){return Math.round(Math.max(0,
|
||||
Math.min(g||0,255)))}function k(g,d){d&&d._ck||a._free(g)}function n(g,d,h){if(!g||!g.length)return K;if(g&&g._ck)return g.byteOffset;var m=a[d].BYTES_PER_ELEMENT;h||=a._malloc(g.length*m);a[d].set(g,h/m);return h}function l(g){var d={he:K,count:g.length,colorType:a.ColorType.RGBA_F32};if(g instanceof Float32Array)d.he=n(g,"HEAPF32"),d.count=g.length/4;else if(g instanceof Uint32Array)d.he=n(g,"HEAPU32"),d.colorType=a.ColorType.RGBA_8888;else if(g instanceof Array){if(g&&g.length){for(var h=a._malloc(16*
|
||||
g.length),m=0,t=h/4,u=0;u<g.length;u++)for(var x=0;4>x;x++)a.HEAPF32[t+m]=g[u][x],m++;g=h}else g=K;d.he=g}else throw"Invalid argument to copyFlexibleColorArray, Not a color array "+typeof g;return d}function p(g){if(!g)return K;var d=ba.toTypedArray();if(g.length){if(6===g.length||9===g.length)return n(g,"HEAPF32",P),6===g.length&&a.HEAPF32.set(Wc,6+P/4),P;if(16===g.length)return d[0]=g[0],d[1]=g[1],d[2]=g[3],d[3]=g[4],d[4]=g[5],d[5]=g[7],d[6]=g[12],d[7]=g[13],d[8]=g[15],P;throw"invalid matrix size";
|
||||
}if(void 0===g.m11)throw"invalid matrix argument";d[0]=g.m11;d[1]=g.m21;d[2]=g.m41;d[3]=g.m12;d[4]=g.m22;d[5]=g.m42;d[6]=g.m14;d[7]=g.m24;d[8]=g.m44;return P}function v(g){if(!g)return K;var d=Y.toTypedArray();if(g.length){if(16!==g.length&&6!==g.length&&9!==g.length)throw"invalid matrix size";if(16===g.length)return n(g,"HEAPF32",ma);d.fill(0);d[0]=g[0];d[1]=g[1];d[3]=g[2];d[4]=g[3];d[5]=g[4];d[7]=g[5];d[10]=1;d[12]=g[6];d[13]=g[7];d[15]=g[8];6===g.length&&(d[12]=0,d[13]=0,d[15]=1);return ma}if(void 0===
|
||||
g.m11)throw"invalid matrix argument";d[0]=g.m11;d[1]=g.m21;d[2]=g.m31;d[3]=g.m41;d[4]=g.m12;d[5]=g.m22;d[6]=g.m32;d[7]=g.m42;d[8]=g.m13;d[9]=g.m23;d[10]=g.m33;d[11]=g.m43;d[12]=g.m14;d[13]=g.m24;d[14]=g.m34;d[15]=g.m44;return ma}function w(g,d){return n(g,"HEAPF32",d||ia)}function A(g,d,h,m){var t=Ea.toTypedArray();t[0]=g;t[1]=d;t[2]=h;t[3]=m;return ia}function D(g){for(var d=new Float32Array(4),h=0;4>h;h++)d[h]=a.HEAPF32[g/4+h];return d}function I(g,d){return n(g,"HEAPF32",d||W)}function R(g,d){return n(g,
|
||||
"HEAPF32",d||ub)}a.Color=function(g,d,h,m){void 0===m&&(m=1);return a.Color4f(f(g)/255,f(d)/255,f(h)/255,m)};a.ColorAsInt=function(g,d,h,m){void 0===m&&(m=255);return(f(m)<<24|f(g)<<16|f(d)<<8|f(h)<<0&268435455)>>>0};a.Color4f=function(g,d,h,m){void 0===m&&(m=1);return Float32Array.of(g,d,h,m)};Object.defineProperty(a,"TRANSPARENT",{get:function(){return a.Color4f(0,0,0,0)}});Object.defineProperty(a,"BLACK",{get:function(){return a.Color4f(0,0,0,1)}});Object.defineProperty(a,"WHITE",{get:function(){return a.Color4f(1,
|
||||
1,1,1)}});Object.defineProperty(a,"RED",{get:function(){return a.Color4f(1,0,0,1)}});Object.defineProperty(a,"GREEN",{get:function(){return a.Color4f(0,1,0,1)}});Object.defineProperty(a,"BLUE",{get:function(){return a.Color4f(0,0,1,1)}});Object.defineProperty(a,"YELLOW",{get:function(){return a.Color4f(1,1,0,1)}});Object.defineProperty(a,"CYAN",{get:function(){return a.Color4f(0,1,1,1)}});Object.defineProperty(a,"MAGENTA",{get:function(){return a.Color4f(1,0,1,1)}});a.getColorComponents=function(g){return[Math.floor(255*
|
||||
g[0]),Math.floor(255*g[1]),Math.floor(255*g[2]),g[3]]};a.parseColorString=function(g,d){g=g.toLowerCase();if(g.startsWith("#")){d=255;switch(g.length){case 9:d=parseInt(g.slice(7,9),16);case 7:var h=parseInt(g.slice(1,3),16);var m=parseInt(g.slice(3,5),16);var t=parseInt(g.slice(5,7),16);break;case 5:d=17*parseInt(g.slice(4,5),16);case 4:h=17*parseInt(g.slice(1,2),16),m=17*parseInt(g.slice(2,3),16),t=17*parseInt(g.slice(3,4),16)}return a.Color(h,m,t,d/255)}return g.startsWith("rgba")?(g=g.slice(5,
|
||||
-1),g=g.split(","),a.Color(+g[0],+g[1],+g[2],e(g[3]))):g.startsWith("rgb")?(g=g.slice(4,-1),g=g.split(","),a.Color(+g[0],+g[1],+g[2],e(g[3]))):g.startsWith("gray(")||g.startsWith("hsl")||!d||(g=d[g],void 0===g)?a.BLACK:g};a.multiplyByAlpha=function(g,d){g=g.slice();g[3]=Math.max(0,Math.min(g[3]*d,1));return g};a.Malloc=function(g,d){var h=a._malloc(d*g.BYTES_PER_ELEMENT);return{_ck:!0,length:d,byteOffset:h,qe:null,subarray:function(m,t){m=this.toTypedArray().subarray(m,t);m._ck=!0;return m},toTypedArray:function(){if(this.qe&&
|
||||
this.qe.length)return this.qe;this.qe=new g(a.HEAPU8.buffer,h,d);this.qe._ck=!0;return this.qe}}};a.Free=function(g){a._free(g.byteOffset);g.byteOffset=K;g.toTypedArray=null;g.qe=null};var P=K,ba,ma=K,Y,ia=K,Ea,fa,W=K,Ub,Ba=K,Vb,vb=K,Wb,wb= | ||||