saura 1 day ago
commit
3756dfc564

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
+build/
+.env
+*.xls
+*.xlsx
+*.db

+ 34 - 0
README.md

@@ -0,0 +1,34 @@
+```shell
+go build -o ./build/cmd/server/main.exe ./src/cmd/server/main.go
+./build/cmd/server/main.exe
+```
+
+```shell
+go run src/cmd/server/main.go
+```
+
+# migrations-up
+```shell
+goose -dir ./migrations/ sqlite3 ${DB_URL} up
+```
+
+# migrations-reset
+```shell
+goose -dir ./migrations/ sqlite3 ${DB_URL} reset
+```
+
+```shell
+libreoffice --headless --convert-to xlsx file.xls
+```
+
+## Зависимости
+
+```bash
+go get -u github.com/mattn/go-sqlite3@latest
+go get -u github.com/joho/godotenv/autoload@latest
+go get -u github.com/labstack/echo/v4@latest
+go get -u github.com/labstack/echo/v4/middleware@latest
+go get -u github.com/google/uuid@latest
+go get -u github.com/xuri/excelize/v2@latest
+go install github.com/pressly/goose/v3/cmd/goose@latest
+```

+ 29 - 0
go.mod

@@ -0,0 +1,29 @@
+module saura
+
+go 1.24.0
+
+require (
+	github.com/google/uuid v1.6.0
+	github.com/joho/godotenv v1.5.1
+	github.com/labstack/echo/v4 v4.14.0
+	github.com/mattn/go-sqlite3 v1.14.32
+	github.com/xuri/excelize/v2 v2.10.0
+)
+
+require (
+	github.com/labstack/gommon v0.4.2 // indirect
+	github.com/mattn/go-colorable v0.1.14 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/richardlehane/mscfb v1.0.4 // indirect
+	github.com/richardlehane/msoleps v1.0.4 // indirect
+	github.com/tiendc/go-deepcopy v1.7.2 // indirect
+	github.com/valyala/bytebufferpool v1.0.0 // indirect
+	github.com/valyala/fasttemplate v1.2.2 // indirect
+	github.com/xuri/efp v0.0.1 // indirect
+	github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
+	golang.org/x/crypto v0.46.0 // indirect
+	golang.org/x/net v0.48.0 // indirect
+	golang.org/x/sys v0.39.0 // indirect
+	golang.org/x/text v0.32.0 // indirect
+	golang.org/x/time v0.14.0 // indirect
+)

+ 73 - 0
go.sum

@@ -0,0 +1,73 @@
+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/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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
+github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
+github.com/labstack/echo/v4 v4.14.0 h1:+tiMrDLxwv6u0oKtD03mv+V1vXXB3wCqPHJqPuIe+7M=
+github.com/labstack/echo/v4 v4.14.0/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=
+github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
+github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.30 h1:bVreufq3EAIG1Quvws73du3/QgdeZ3myglJlrzSYYCY=
+github.com/mattn/go-sqlite3 v1.14.30/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
+github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+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/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
+github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
+github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
+github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
+github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/tiendc/go-deepcopy v1.6.1 h1:uVRTItFeNHkMcLueHS7OCsxgxT9P8MzGB/taUa2Y4Tk=
+github.com/tiendc/go-deepcopy v1.6.1/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
+github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
+github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
+github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
+github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
+github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
+github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
+github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4=
+github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU=
+github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
+github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
+github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
+github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
+golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
+golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
+golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
+golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
+golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
+golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
+golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
+golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
+golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
+golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
+golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
+golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
+golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
+golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
+golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
+golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 31 - 0
migrations/00001_clients_table.sql

@@ -0,0 +1,31 @@
+-- +goose Up
+CREATE TABLE clients (
+  id TEXT PRIMARY KEY,
+  id2 TEXT,
+  mark TEXT,
+  contractor INTEGER,
+  full_name TEXT,
+  type TEXT,
+  email TEXT,
+  legal_address TEXT,
+  physical_address TEXT,
+  registration_date TEXT,
+  ad_channel TEXT,
+  reg_data_1 TEXT,
+  reg_data_2 TEXT,
+  note TEXT,
+  request_count INTEGER,
+  birthday TEXT,
+  income NUMERIC -- NUMERIC (или REAL) для числового значения
+);
+
+CREATE TABLE client_phones (
+  client_id INTEGER,
+  phone TEXT,
+  FOREIGN KEY (client_id) REFERENCES clients (id)
+);
+
+-- +goose Down
+DROP TABLE client_phones;
+
+DROP TABLE clients;

+ 22 - 0
migrations/00002_products_table.sql

@@ -0,0 +1,22 @@
+-- +goose Up
+CREATE TABLE products (
+  id TEXT PRIMARY KEY,
+  id2 TEXT,
+  name TEXT,
+  serial_number TEXT,
+  article TEXT,
+  date TEXT,
+  quantity TEXT, -- Кол-во
+  retail_price TEXT, -- Розничная цена
+  purchase_price TEXT, -- Закупочная цена
+  exchange_rate_pc TEXT, -- Курс ПК
+  exchange_rate_pr TEXT, -- Курс ПР
+  warehouse TEXT, -- Склад
+  location TEXT,
+  customer_order TEXT, -- Заказ клиента
+  supplier_order TEXT, -- Заказ поставщику
+  supplier TEXT -- Поставщик
+);
+
+-- +goose Down
+DROP TABLE products;

+ 4 - 0
migrations/00003_calls_table.sql

@@ -0,0 +1,4 @@
+-- +goose Up
+-- CREATE TABLE IF NOT EXISTS calls (id TEXT PRIMARY KEY, id2 TEXT);
+-- +goose Down
+-- DROP TABLE calls;

+ 15 - 0
migrations/00004_client_ad_channels_table.sql

@@ -0,0 +1,15 @@
+-- +goose Up
+CREATE TABLE ad_channels (id INTEGER PRIMARY KEY, name TEXT);
+
+INSERT INTO
+  ad_channels (name)
+VALUES
+  ('Интернет'),
+  ('Партнер'),
+  ('По рекомендации'),
+  ('Постоянные клиенты'),
+  ('Проходящий поток'),
+  ('СЦ ТРУД');
+
+-- +goose Down
+DROP TABLE ad_channels;

+ 19 - 0
migrations/00005_client_marks_table.sql

@@ -0,0 +1,19 @@
+-- +goose Up
+CREATE TABLE marks (id INTEGER PRIMARY KEY, name TEXT);
+
+INSERT INTO
+  marks (name)
+VALUES
+  ('-5%'),
+  ('-10%'),
+  ('-20%'),
+  ('-30%'),
+  ('blacklist'),
+  ('discount'),
+  ('regular'),
+  ('VIP'),
+  ('Животное'),
+  ('Мудак');
+
+-- +goose Down
+DROP TABLE marks;

+ 11 - 0
src/app/app.go

@@ -0,0 +1,11 @@
+package app
+
+import "os"
+
+type App struct {
+	Port string
+}
+
+func Init(appCtx *App) {
+	appCtx.Port = os.Getenv("PORT")
+}

+ 144 - 0
src/cmd/immigration/clients/main.go

@@ -0,0 +1,144 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"saura/src/common"
+	clientApi "saura/src/server/client"
+	"saura/src/server/db/repo"
+	"strconv"
+	"strings"
+
+	"github.com/xuri/excelize/v2"
+
+	_ "github.com/joho/godotenv/autoload"
+)
+
+var FILE_PATH string
+
+func ReadClientsFromFile(file_path string) ([]repo.ClientInfo, error) {
+	clients := []repo.ClientInfo{}
+
+	file, err := excelize.OpenFile(file_path)
+	if err != nil {
+		fmt.Println(err)
+		return clients, err
+	}
+
+	// Get all the rows in the Sheet1.
+	rows, err := file.GetRows("Клиенты")
+	if err != nil {
+		fmt.Println(err)
+		return clients, err
+	}
+
+	// Пропускаем заголовок и обрабатываем каждую строку
+	for i, row := range rows {
+		if i == 0 {
+			continue
+		}
+		client := repo.ClientInfo{}
+		client.Id2 = row[0]
+		if len(row) > 1 {
+			client.Mark = row[1]
+		}
+		if len(row) > 2 {
+			if row[2] == "Да" {
+				client.Contractor = true
+			} else {
+				client.Contractor = false
+			}
+		}
+		if len(row) > 3 {
+			client.FullName = row[3]
+		}
+		if len(row) > 4 {
+			client.Type = row[4]
+		}
+		if len(row) > 5 {
+			if row[5] != "" {
+				phones := strings.Split(row[5], ",")
+				client.Phones = phones
+				// fmt.Println("PHones:", client.Phones)
+			}
+		}
+		if len(row) > 6 {
+			client.Email = row[6]
+		}
+		if len(row) > 7 {
+			client.LegalAddress = row[7]
+		}
+		if len(row) > 8 {
+			client.PhysicalAddress = row[8]
+		}
+		if len(row) > 9 {
+			if row[9] != "" {
+				client.RegistrationDate = row[9]
+			}
+		}
+		if len(row) > 10 {
+			client.AdChannel = row[10]
+		}
+		if len(row) > 11 {
+			client.RegData1 = row[11]
+		}
+		if len(row) > 12 {
+			client.RegData2 = row[12]
+		}
+		if len(row) > 13 {
+			client.Note = row[13]
+		}
+		if len(row) > 14 {
+			if row[14] != "" {
+				value, err := strconv.Atoi(row[14])
+				if err != nil {
+					fmt.Println("Error string to int:", err)
+					return clients, err
+				}
+				client.RequestCount = value
+			}
+		}
+		if len(row) > 15 {
+			client.Birthday = row[15]
+		}
+		if len(row) > 16 {
+			str := row[16]
+			if str != "" {
+				if strings.Contains(str, ".") {
+					res := strings.ReplaceAll(row[16], ".", "")
+					parse, _ := strconv.ParseInt(res, 10, 64)
+					client.Income = common.Money(parse)
+				} else {
+					parse, _ := strconv.ParseInt(str, 10, 64)
+					client.Income = common.Money(parse * 100)
+				}
+			}
+		}
+		// fmt.Println("Client:", client)
+		clients = append(clients, client)
+	}
+
+	return clients, err
+}
+
+func main() {
+	portEnv := os.Getenv("PORT")
+	url := "http://localhost" + portEnv + "/api/v1/client"
+	// fmt.Println(url)
+
+	if len(os.Args) > 1 {
+		FILE_PATH = os.Args[1]
+		clients, err := ReadClientsFromFile(FILE_PATH)
+		if err != nil {
+			fmt.Println(err)
+		}
+
+		for _, client := range clients {
+			// fmt.Println(client)
+			err = clientApi.PostData[repo.ClientInfo](url, client)
+			if err != nil {
+				fmt.Println(err)
+			}
+		}
+	}
+}

+ 50 - 0
src/cmd/server/main.go

@@ -0,0 +1,50 @@
+package main
+
+import (
+	"context"
+	"log"
+	"net/http"
+	"os"
+	"os/signal"
+	"saura/src/app"
+	myServer "saura/src/server"
+	"syscall"
+	"time"
+)
+
+func main() {
+	log.Println("Server running...")
+
+	appCtx := new(app.App)
+	app.Init(appCtx)
+
+	serverCtx := new(myServer.Server)
+	http_server := myServer.Init(serverCtx, appCtx)
+
+	// Channel to listen for OS signals
+	quit := make(chan os.Signal, 1)
+	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
+
+	// Start the HTTP server in a goroutine
+	go func() {
+		if err := http_server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+			log.Fatalf("Server failed to listen and serve: %v", err)
+		}
+	}()
+	log.Println("Server started on " + appCtx.Port)
+
+	// Block until a signal is received
+	<-quit
+	log.Println("Shutting down server...")
+
+	// Create a context with a timeout for shutdown
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	// Attempt to gracefully shut down the server
+	if err := http_server.Shutdown(ctx); err != nil {
+		log.Fatalf("Server shutdown failed: %v", err)
+	}
+
+	log.Println("Server gracefully shut down.")
+}

+ 11 - 0
src/common/common.go

@@ -0,0 +1,11 @@
+package common
+
+import "fmt"
+
+type Money int64
+
+func (m Money) String() string {
+	rubles := int64(m) / 100
+	kopeks := int64(m) % 100
+	return fmt.Sprintf("%d.%02d", rubles, kopeks)
+}

+ 59 - 0
src/server/client/client_api.go

@@ -0,0 +1,59 @@
+package clientApi
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"net/http"
+)
+
+func PostData[T any](url string, data any) error {
+	jsonData, err := json.Marshal(data)
+	if err != nil {
+		return err
+	}
+
+	resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return fmt.Errorf("failed to add: %s", resp.Status)
+	}
+
+	return nil
+}
+
+func FetchData[T any](url string, data T) error {
+	resp, err := http.Get(url)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return fmt.Errorf("failed to fetch data: status code %d", resp.StatusCode)
+	}
+
+	return json.NewDecoder(resp.Body).Decode(data)
+}
+
+func FetchOne[T any](params string) (data T, err error) {
+	url := params
+	err = FetchData(url, &data)
+	if err != nil {
+		return data, err
+	}
+	return data, nil
+}
+
+func Fetch[T any](params string) (data []T, err error) {
+	url := params
+	err = FetchData(url, &data)
+	if err != nil {
+		return nil, err
+	}
+	return data, nil
+}

+ 119 - 0
src/server/db/db.go

@@ -0,0 +1,119 @@
+package db
+
+import (
+	"database/sql"
+	"fmt"
+	"log"
+	"os"
+	"saura/src/server/db/repo"
+	"sync"
+
+	_ "github.com/mattn/go-sqlite3"
+)
+
+type DB struct {
+	db          *sql.DB
+	clientsRepo *repo.ClientRepo
+}
+
+var (
+	instance *DB
+	once     sync.Once
+)
+
+func (ctx *DB) Init() error {
+	dbURL := os.Getenv("DB_URL")
+	if dbURL == "" {
+		return fmt.Errorf("DB_URL environment variable is not set")
+	}
+
+	var err error
+	ctx.db, err = sql.Open("sqlite3", dbURL)
+
+	if err != nil {
+		return fmt.Errorf("failed to open database: %w", err)
+	}
+
+	if err := ctx.db.Ping(); err != nil {
+		ctx.db.Close()
+		return fmt.Errorf("failed to connect to database: %w", err)
+	}
+
+	ctx.clientsRepo = repo.NewClientRepo(ctx.db, dbURL)
+
+	log.Println("Database initialized successfully")
+	return nil
+}
+
+func (ctx *DB) Close() error {
+	if ctx.db != nil {
+		return ctx.db.Close()
+	}
+	return nil
+}
+
+func (ctx *DB) GetClientRepo() *repo.ClientRepo {
+	return ctx.clientsRepo
+}
+
+func (ctx *DB) FetchMarks() ([]string, error) {
+	query := `
+      SELECT id, name
+      FROM marks
+  `
+	rows, err := ctx.db.Query(query)
+	if err != nil {
+		return nil, fmt.Errorf("failed to execute query: %w", err)
+	}
+	defer rows.Close()
+
+	var marks []string
+
+	for rows.Next() {
+		var id string
+		var name string
+
+		if err := rows.Scan(&id, &name); err != nil {
+			return nil, fmt.Errorf("failed to scan row: %w", err)
+		}
+
+		marks = append(marks, name)
+	}
+
+	if err := rows.Err(); err != nil {
+		return nil, fmt.Errorf("error during iteration: %w", err)
+	}
+
+	return marks, nil
+}
+
+func (ctx *DB) FetchAdChannels() ([]string, error) {
+	query := `
+      SELECT id, name
+      FROM ad_channels
+  `
+	rows, err := ctx.db.Query(query)
+	if err != nil {
+		return nil, fmt.Errorf("failed to execute query: %w", err)
+	}
+	defer rows.Close()
+
+	var ad_channels []string
+
+	for rows.Next() {
+		var id string
+		var name string
+
+		if err := rows.Scan(&id, &name); err != nil {
+			return nil, fmt.Errorf("failed to scan row: %w", err)
+		}
+
+		ad_channels = append(ad_channels, name)
+	}
+
+	if err := rows.Err(); err != nil {
+		return nil, fmt.Errorf("error during iteration: %w", err)
+	}
+
+	return ad_channels, nil
+}

+ 291 - 0
src/server/db/repo/client.go

@@ -0,0 +1,291 @@
+package repo
+
+import (
+	"database/sql"
+	"fmt"
+	"saura/src/common"
+)
+
+const (
+	ClientTypePhysical     string = "Физ"
+	ClientTypeLegal        string = "Юр"
+	ClientTypeSupplier     string = "Поставщик"
+	ClientTypeEmployee     string = "Сотрудник"
+	ClientTypeCounterparty string = "Контрагент"
+	ClientTypeCustomer     string = "Покупатель"
+)
+
+type ClientInfo struct {
+	Id               string       `db:"id" json:"id" form:"id"`
+	Id2              string       `db:"id2" json:"id2" form:"id2"`
+	Mark             string       `db:"mark" json:"mark" form:"mark"`
+	Contractor       bool         `db:"contractor" json:"contractor" form:"contractor"`
+	FullName         string       `db:"full_name" json:"full_name" form:"full_name"`
+	Type             string       `db:"type" json:"type" form:"type"`
+	Phones           []string     `json:"phones" form:"phones"`
+	Email            string       `db:"email" json:"email" form:"email"`
+	LegalAddress     string       `db:"legal_address" json:"legal_address" form:"legal_address"`
+	PhysicalAddress  string       `db:"physical_address" json:"physical_address" form:"physical_address"`
+	RegistrationDate string       `db:"registration_date" json:"registration_date" form:"registration_date"`
+	AdChannel        string       `db:"ad_channel" json:"ad_channel" form:"ad_channel"`
+	RegData1         string       `db:"reg_data_1" json:"reg_data_1" form:"reg_data_1"`
+	RegData2         string       `db:"reg_data_2" json:"reg_data_2" form:"reg_data_2"`
+	Note             string       `db:"note" json:"note" form:"note"`
+	RequestCount     int          `db:"request_count" json:"request_count" form:"request_count"`
+	Birthday         string       `db:"birthday" json:"birthday" form:"birthday"`
+	Income           common.Money `db:"income" json:"income" form:"income"`
+}
+
+type ClientRepo struct {
+	db    *sql.DB
+	dbUrl string
+}
+
+func NewClientRepo(db *sql.DB, dbUrl string) *ClientRepo {
+	return &ClientRepo{db: db, dbUrl: dbUrl}
+}
+
+func (r *ClientRepo) InsertClient(client ClientInfo) error {
+	var err error
+	var exists bool
+
+	err = r.db.QueryRow("SELECT EXISTS(SELECT 1 FROM clients WHERE id = ? OR id2 = ?)", client.Id, client.Id2).Scan(&exists)
+	if err != nil {
+		return fmt.Errorf("failed to check if client exists: %w", err)
+	}
+	if exists {
+		return fmt.Errorf("client with id %s already exists", client.Id)
+	}
+
+	_, err = r.db.Exec(
+		`
+    INSERT INTO clients (
+      id, id2, mark, contractor, full_name, type, email,
+      legal_address, physical_address, registration_date, ad_channel,
+      reg_data_1, reg_data_2, note, request_count, birthday, income
+    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+    `,
+		client.Id, client.Id2, client.Mark, client.Contractor, client.FullName,
+		client.Type, client.Email, client.LegalAddress,
+		client.PhysicalAddress, client.RegistrationDate, client.AdChannel,
+		client.RegData1, client.RegData2, client.Note, client.RequestCount,
+		client.Birthday, client.Income,
+	)
+	if err != nil {
+		return fmt.Errorf("failed to insert client: %w", err)
+	}
+
+	return nil
+}
+
+func (r *ClientRepo) FetchClientByID(id string) (*ClientInfo, error) {
+	var err error
+
+	row := r.db.QueryRow(`
+		SELECT
+    	id, id2, mark, contractor, full_name, type, email,
+      legal_address, physical_address, registration_date, ad_channel,
+      reg_data_1, reg_data_2, note, request_count, birthday, income
+    FROM clients
+    WHERE id = ?
+    `, id)
+
+	var client ClientInfo
+	err = row.Scan(
+		&client.Id, &client.Id2, &client.Mark, &client.Contractor, &client.FullName,
+		&client.Type, &client.Email, &client.LegalAddress,
+		&client.PhysicalAddress, &client.RegistrationDate, &client.AdChannel,
+		&client.RegData1, &client.RegData2, &client.Note, &client.RequestCount,
+		&client.Birthday, &client.Income,
+	)
+
+	if err == sql.ErrNoRows {
+		return nil, fmt.Errorf("client with id %s not found", id)
+	} else if err != nil {
+		return nil, fmt.Errorf("failed to scan row: %w", err)
+	}
+
+	phones, err := r.FetchClientPhones(client.Id)
+	if err != nil {
+		return nil, fmt.Errorf("error: %w", err)
+	}
+	client.Phones = phones
+
+	return &client, nil
+}
+
+func (r *ClientRepo) InsertClientPhones(client_id string, phones []string) error {
+	for _, phone := range phones {
+		_, err := r.db.Exec("INSERT INTO client_phones VALUES(?, ?)", client_id, phone)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (r *ClientRepo) FetchClientPhones(client_id string) ([]string, error) {
+	rows, err := r.db.Query("SELECT phone FROM client_phones WHERE client_id = ?", client_id)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	var tags []string
+	for rows.Next() {
+		var tag string
+		rows.Scan(&tag)
+		tags = append(tags, tag)
+	}
+	return tags, nil
+}
+
+func (r *ClientRepo) FetchClients(count int, offset int) ([]ClientInfo, error) {
+	rows, err := r.db.Query(`
+        SELECT
+            id, id2, mark, contractor, full_name, type, email,
+            legal_address, physical_address, registration_date, ad_channel,
+            reg_data_1, reg_data_2, note, request_count, birthday, income
+        FROM clients
+        LIMIT ? OFFSET ?
+    `, count, offset)
+	if err != nil {
+		return nil, fmt.Errorf("failed to execute query: %w", err)
+	}
+	defer rows.Close()
+
+	var clients []ClientInfo
+	for rows.Next() {
+		var client ClientInfo
+		err := rows.Scan(
+			&client.Id,
+			&client.Id2,
+			&client.Mark,
+			&client.Contractor,
+			&client.FullName,
+			&client.Type,
+			&client.Email,
+			&client.LegalAddress,
+			&client.PhysicalAddress,
+			&client.RegistrationDate, // time.Time или sql.NullTime
+			&client.AdChannel,
+			&client.RegData1,
+			&client.RegData2,
+			&client.Note,         // sql.NullString
+			&client.RequestCount, // int или sql.NullInt32
+			&client.Birthday,     // time.Time или sql.NullTime
+			&client.Income,       // float64 или sql.NullFloat64
+		)
+		if err != nil {
+			return nil, fmt.Errorf("failed to scan row: %w", err)
+		}
+
+		phones, err := r.FetchClientPhones(client.Id)
+		if err != nil {
+			return nil, fmt.Errorf("rows error: %w", err)
+		}
+		client.Phones = phones
+
+		clients = append(clients, client)
+	}
+
+	// Проверка ошибок после итерации
+	if err := rows.Err(); err != nil {
+		return nil, fmt.Errorf("rows error: %w", err)
+	}
+
+	return clients, nil
+}
+
+func (r *ClientRepo) FetchClientsCount() (int, error) {
+	var count int
+	query := "SELECT COUNT(*) FROM clients"
+	err := r.db.QueryRow(query).Scan(&count)
+	if err != nil {
+		return 0, fmt.Errorf("failed to fetch count: %w", err)
+	}
+	return count, nil
+}
+
+func (r *ClientRepo) DeleteClient(id string) error {
+	query := "DELETE FROM clients WHERE id = ?"
+	result, err := r.db.Exec(query, id)
+	if err != nil {
+		return fmt.Errorf("failed to delete client: %w", err)
+	}
+
+	rowsAffected, err := result.RowsAffected()
+	if err != nil {
+		return fmt.Errorf("failed to get rows affected: %w", err)
+	}
+
+	if rowsAffected == 0 {
+		return fmt.Errorf("client with id %s not found", id)
+	}
+
+	return nil
+}
+
+func (r *ClientRepo) UpdateClient(id string, client ClientInfo) error {
+	// Проверим, существует ли клиент
+	var exists string
+	err := r.db.QueryRow("SELECT id FROM clients WHERE id = ?", id).Scan(&exists)
+	if err == sql.ErrNoRows {
+		return fmt.Errorf("client with id %s not found", id)
+	} else if err != nil {
+		return fmt.Errorf("failed to check client existence: %w", err)
+	}
+
+	query := `
+        UPDATE clients SET
+            id = ?,
+            id2 = ?,
+            mark = ?,
+            contractor = ?,
+            full_name = ?,
+            type = ?,
+            email = ?,
+            legal_address = ?,
+            physical_address = ?,
+            registration_date = ?,
+            ad_channel = ?,
+            reg_data_1 = ?,
+            reg_data_2 = ?,
+            note = ?,
+            request_count = ?,
+            birthday = ?,
+            income = ?
+        WHERE id = ?
+    `
+
+	contractorInt := 0
+	if client.Contractor {
+		contractorInt = 1
+	}
+
+	_, err = r.db.Exec(query,
+		id,
+		client.Id2,
+		client.Mark,
+		contractorInt,
+		client.FullName,
+		client.Type,
+		client.Email,
+		client.LegalAddress,
+		client.PhysicalAddress,
+		client.RegistrationDate,
+		client.AdChannel,
+		client.RegData1,
+		client.RegData2,
+		client.Note,
+		client.RequestCount,
+		client.Birthday,
+		client.Income,
+		id,
+	)
+	if err != nil {
+		return fmt.Errorf("failed to update client: %w", err)
+	}
+
+	return nil
+}

+ 75 - 0
src/server/server.go

@@ -0,0 +1,75 @@
+package server
+
+import (
+	"net/http"
+	"strconv"
+	"strings"
+	"time"
+
+	"saura/src/app"
+	"saura/src/server/db"
+
+	"github.com/labstack/echo/v4"
+	"github.com/labstack/echo/v4/middleware"
+
+	_ "github.com/joho/godotenv/autoload"
+)
+
+type Server struct {
+	db *db.DB
+}
+
+func Init(ctx *Server, appCtx *app.App) *http.Server {
+	ctx.db = new(db.DB)
+	ctx.db.Init()
+
+	http_server := &http.Server{
+		Addr:         appCtx.Port,
+		Handler:      RegisterHandlers(ctx.db),
+		IdleTimeout:  time.Minute,
+		ReadTimeout:  10 * time.Second,
+		WriteTimeout: 30 * time.Second,
+	}
+
+	return http_server
+}
+
+func RegisterHandlers(db *db.DB) http.Handler {
+	e := echo.New()
+	e.Use(middleware.RequestLogger())
+	e.Use(middleware.Recover())
+
+	e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
+		AllowOrigins:     []string{"https://*", "http://*"},
+		AllowMethods:     []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"},
+		AllowHeaders:     []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
+		AllowCredentials: true,
+		MaxAge:           300,
+	}))
+
+	apiGroup := e.Group("", func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			if strings.HasPrefix(c.Request().Host, "api.") {
+				return next(c)
+			}
+			return echo.ErrNotFound
+		}
+	})
+
+	e.GET("/", func(c echo.Context) error {
+		return c.String(http.StatusOK, "is live")
+	})
+
+	apiGroup.GET("/v1/clients", func(c echo.Context) error {
+		limit, _ := strconv.Atoi(c.QueryParam("limit"))
+		offset, _ := strconv.Atoi(c.QueryParam("offset"))
+
+		clients, err := db.GetClientRepo().FetchClients(limit, offset)
+		if err != nil {
+			return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		}
+		return c.JSON(http.StatusOK, clients)
+	})
+
+	return e
+}