diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..8ae4d470d17eb9b5cdb095896b4a17eea7e9f581 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.idea +*.iml +.git/ +.gradle +gradle +out/ +Dockerfile \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000000000000000000000000000000000000..d9afc9929fb00baeae0d224af2bea6d1326586c8 --- /dev/null +++ b/.env @@ -0,0 +1,8 @@ +SERVER_ADDRESS="localhost:8080" +POSTGRES_CONN="postgres://postgres:postgres@172.18.1.2:5432/avito?sslmode=disable" +POSTGRES_JDBC_URL="jdbc:postgresql://your_host:your_port/your_db" +POSTGRES_USERNAME="postgres" +POSTGRES_PASSWORD="postgres" +POSTGRES_HOST="172.18.1.2" +POSTGRES_PORT="5432" +POSTGRES_DATABASE="avito" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..63177e3dfcbc9db2a754b7936ac2b395a3186e4b --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +HELP.md +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..ed563e8f35f13197e3262fe2e79de79a3575b210 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,153 @@ +variables: + CALLBACK_URL: "https://codenrock.com/api/gitlab/check" + CONTEXT: config.yaml + DOMAIN: avito2024.codenrock.com + +stages: + - build + - deploy + +build: + stage: build + image: docker:27.0.3-dind + services: + - docker:27.0.3-dind + before_script: + - mkdir -pv ${HOME}/.docker + - cp -vf $YCR_DOCKER_CONFIG_JSON ${HOME}/.docker/config.json + - chmod 0600 ${HOME}/.docker/config.json + - sleep 20 + - docker info + variables: + DOCKER_HOST: tcp://docker:2375 + DOCKER_TLS_CERTDIR: "" + only: + - main + - master + script: + - docker build . -f ./Dockerfile -t ${CI_REGISTRY_IMAGE}:$CI_COMMIT_SHA + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - docker push ${CI_REGISTRY_IMAGE}:$CI_COMMIT_SHA + tags: + - runner + - build + - k8s + +deploy: + environment: + name: production + url: https://$KUBE_NAMESPACE-$CI_PROJECT_NAMESPACE_ID.$DOMAIN + image: + name: bitnami/kubectl:latest + entrypoint: [''] + stage: deploy + only: + - main + - master + script: + - export KUBE_NAMESPACE=$(echo $CI_PROJECT_DIR|sed 's/.*\/\(.*\)\/.*/\1/') + - export HOST_NAME=$KUBE_NAMESPACE-$CI_PROJECT_NAMESPACE_ID.$DOMAIN + - if [ "$KUBE_NAMESPACE" == "" ]; then echo "Namespace is not configured"; exit 1; fi + - echo "URL https://$HOST_NAME" + - kubectl config get-contexts + - kubectl config use-context $CONTEXT + - kubectl create ns $KUBE_NAMESPACE || true + - kubectl -n default get secret avito2024-tls -o yaml|sed 's/ namespace:.*//g'|sed 's/ uid:.*//g'|sed 's/ resourceVersion:.*//g'|sed 's/ creationTimestamp:.*//g' | kubectl apply --namespace $KUBE_NAMESPACE -f - + - kubectl -n default get secret regcred -o yaml|sed 's/ namespace:.*//g'|sed 's/ uid:.*//g'|sed 's/ resourceVersion:.*//g'|sed 's/ creationTimestamp:.*//g' | kubectl apply --namespace $KUBE_NAMESPACE -f - + - | + cat <<EOF | kubectl -n $KUBE_NAMESPACE apply -f - + --- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: $KUBE_NAMESPACE-backend + annotations: + app.gitlab.com/app: $CI_PROJECT_PATH_SLUG + app.gitlab.com/env: $CI_ENVIRONMENT_SLUG + spec: + replicas: 1 + selector: + matchLabels: + app: $KUBE_NAMESPACE-backend + template: + metadata: + labels: + app: $KUBE_NAMESPACE-backend + annotations: + app.gitlab.com/app: $CI_PROJECT_PATH_SLUG + app.gitlab.com/env: $CI_ENVIRONMENT_SLUG + spec: + imagePullSecrets: + - name: regcred + containers: + - name: $KUBE_NAMESPACE + image: ${CI_REGISTRY_IMAGE}:$CI_COMMIT_SHA + imagePullPolicy: Always + resources: + requests: + cpu: "100m" + memory: "256M" + env: + - name: POSTGRES_CONN + value: "postgres://$KUBE_NAMESPACE:$KUBE_NAMESPACE@rc1b-5xmqy6bq501kls4m.mdb.yandexcloud.net:6432/$KUBE_NAMESPACE" + - name: POSTGRES_JDBC_URL + value: "jdbc:postgresql://rc1b-5xmqy6bq501kls4m.mdb.yandexcloud.net:6432/$KUBE_NAMESPACE" + - name: POSTGRES_USERNAME + value: "$KUBE_NAMESPACE" + - name: POSTGRES_PASSWORD + value: "$KUBE_NAMESPACE" + - name: POSTGRES_HOST + value: "rc1b-5xmqy6bq501kls4m.mdb.yandexcloud.net" + - name: POSTGRES_PORT + value: "6432" + - name: POSTGRES_DATABASE + value: "$KUBE_NAMESPACE" + - name: SERVER_ADDRESS + value: "0.0.0.0:8080" + ports: + - containerPort: 8080 + protocol: TCP + --- + apiVersion: v1 + kind: Service + metadata: + name: ${KUBE_NAMESPACE}-backend + spec: + selector: + app: ${KUBE_NAMESPACE}-backend + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 + --- + apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: $KUBE_NAMESPACE-backend + annotations: + kubernetes.io/ingress.class: nginx + #cert-manager.io/cluster-issuer: "letsencrypt" + nginx.ingress.kubernetes.io/ssl-redirect: "false" + nginx.ingress.kubernetes.io/force-ssl-redirect: "false" + spec: + tls: + - hosts: + - $HOST_NAME + secretName: avito2024-tls + rules: + - host: $HOST_NAME + http: + paths: + - path: / + pathType: ImplementationSpecific + backend: + service: + name: $KUBE_NAMESPACE-backend + port: + number: 8080 + --- + EOF + tags: + - runner + - build + - k8s diff --git a/.gitlab/agents/yc/config.yaml b/.gitlab/agents/yc/config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7499bcafdf084e0fdb580a381f2cc500a6c8b4be --- /dev/null +++ b/.gitlab/agents/yc/config.yaml @@ -0,0 +1,3 @@ +ci_access: + groups: + - id: avito-testirovanie-na-backend-1270 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..39b92eeb459ae5b9ab8694ac36e1e5a67b563898 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM gradle:4.7.0-jdk8-alpine AS build +COPY --chown=gradle:gradle . /home/gradle/src +WORKDIR /home/gradle/src +RUN gradle build --no-daemon + +FROM openjdk:8-jre-slim + +EXPOSE 8080 + +RUN mkdir /app + +COPY --from=build /home/gradle/src/build/libs/*.jar /app/spring-boot-application.jar + +ENTRYPOINT ["java", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap", "-Djava.security.egd=file:/dev/./urandom","-jar","/app/spring-boot-application.jar"] + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..4bc5c53f8eb2bf4fa1af31aa72ebd8680f8b4a44 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +run: + go run ./cmd/app/main.go -l + +migrateup: + migrate -path ./migrations -database postgres://postgres:postgres@172.18.1.2:5432/avito?sslmode=disable -verbose up + +migratedown: + migrate -path ./migrations -database postgres://postgres:postgres@172.18.1.2:5432/avito?sslmode=disable -verbose down diff --git a/README.md b/README.md index 4f67d158636bf267600a7580459c831d5f83e23e..94884e06f4ffd4b4d8183ba2bb14791fefa7f327 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,31 @@ -# Zadanie 6105 +## Структура проекта +Ð’ данном проекте находитÑÑ Ñ‚Ð¸Ð¿Ð¾Ð²Ð¾Ð¹ пример Ð´Ð»Ñ Ñборки Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð² докере из находÑщÑщегоÑÑ Ð² проекте Dockerfile. Пример на Gradle иÑпользуетÑÑ Ð¸Ñключительно в качеÑтве шаблона, вы можете перепиÑать проект как вам хочетÑÑ - главное, что бы Dockerfile находилÑÑ Ð² корне проекта и приложение отвечало по порту 8080. Других требований нет. +## Задание +Ð’ папке "задание" размещена задача. +## Сбор и развертывание Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ +Приложение должно отвечать по порту `8080` (жеÑтко задано в наÑтройках деплоÑ). ПоÑле Ð´ÐµÐ¿Ð»Ð¾Ñ Ð¾Ð½Ð¾ будет доÑтупно по адреÑу: `https://<имÑ_проекта>-<уникальный_идентификатор_группы_группы>.avito2024.codenrock.com` -## Getting started - -To make it easy for you to get started with GitLab, here's a list of recommended next steps. - -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! - -## Add your files - -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: +Пример: Ð”Ð»Ñ ÐºÐ¾Ð´Ð° из Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ `/avito2024/cnrprod-team-27437/task1` ÑформируетÑÑ Ð´Ð¾Ð¼ÐµÐ½ ``` -cd existing_repo -git remote add origin https://git.codenrock.com/cnrprod1713782508-user-78446/zadanie-6105.git -git branch -M main -git push -uf origin main +task1-5447.avito2024.codenrock.com ``` -## Integrate with your tools - -- [ ] [Set up project integrations](https://git.codenrock.com/cnrprod1713782508-user-78446/zadanie-6105/-/settings/integrations) - -## Collaborate with your team - -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) - -## Test and Deploy - -Use the built-in continuous integration in GitLab. - -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) - -*** - -# Editing this README - -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template. - -## Suggestions for a good README - -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. - -## Name -Choose a self-explaining name for your project. - -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. - -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. - -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. - -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. +**Ð”Ð»Ñ ÑƒÐ´Ð¾Ð±Ñтва домен указываетÑÑ Ð² логе Ñборки** -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. +Логи Ñборки проекта находÑÑ‚ÑÑ Ð½Ð° вкладке **CI/CD** -> **Jobs**. -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. +СÑылка на Ñобранный проект находитÑÑ Ð½Ð° вкладке **Deployments** -> **Environment**. Ð’Ñ‹ можете Ñразу открыть URL по кнопке "Open". -## Contributing -State if you are open to contributions and what your requirements are for accepting them. +## ДоÑтуп к ÑервиÑам -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. +### Kubernetes +Ðа вашу команду выделен kubernetes namespace. Ð”Ð»Ñ Ð¿Ð¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ Ðº нему иÑпользуйте утилиту `kubectl` и `*.kube.config` файл, который вам выдадут организаторы. -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. +СоÑтоÑние namespace, работающие pods и логи приложений можно поÑмотреть по адреÑу [https://dashboard.avito2024.codenrock.com/](https://dashboard.avito2024.codenrock.com/). Ð”Ð»Ñ Ð¾Ñ‚ÐºÑ€Ñ‹Ñ‚Ð¸Ñ Ð´Ð°ÑˆÐ±Ð¾Ñ€Ð´Ð° необходимо выбрать авторизацию через Kubeconfig и указать путь до выданного вам `*.kube.config` файла -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. -## License -For open source projects, say how it is licensed. -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. diff --git a/SOMESTRUCT.md b/SOMESTRUCT.md new file mode 100644 index 0000000000000000000000000000000000000000..2c47f4611c2e4070401597acb8d62ad0c6208585 --- /dev/null +++ b/SOMESTRUCT.md @@ -0,0 +1,98 @@ +<pre><code> +package main + +import ( +"database/sql" +"fmt" +"os" + + "github.com/gin-gonic/gin" + _ "github.com/lib/pq" +) + +func main() { +// Чтение переменных Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ +serverAddress := os.Getenv("SERVER_ADDRESS") +postgresConnURL := os.Getenv("POSTGRES_CONN") + + // Подключение к базе данных PostgreSQL + db, err := sql.Open("postgres", postgresConnURL) + if err != nil { + panic(err) + } + defer db.Close() + + // Создание ÑкземплÑра Gin + r := gin.Default() + + // Маршруты API + r.GET("/api/ping", func(c *gin.Context) { + c.JSON(200, gin.H{ + "message": "ok", + }) + }) + + tenders := r.Group("/api/tenders") + { + tenders.GET("", listTenders) + tenders.POST("/new", createTender) + tenders.GET("/my", getTendersByUser) + tenders.PATCH("/:id/edit", editTender) + tenders.PUT("/:id/rollback/:version", rollbackTender) + } + + bids := r.Group("/api/bids") + { + bids.POST("/new", createBid) + bids.GET("/my", getBidsByUser) + bids.GET("/:tenderId/list", getBidsForTender) + bids.PATCH("/:id/edit", editBid) + bids.PUT("/:id/rollback/:version", rollbackBid) + } + + // ЗапуÑк Ñервера + r.Run(serverAddress) +} + +// Обработчики маршрутов +func listTenders(c *gin.Context) { +// Логика Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ ÑпиÑка тендеров +} + +func createTender(c *gin.Context) { +// Логика ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð½Ð¾Ð²Ð¾Ð³Ð¾ тендера +} + +func getTendersByUser(c *gin.Context) { +// Логика Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ñ‚ÐµÐ½Ð´ÐµÑ€Ð¾Ð² Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ +} + +func editTender(c *gin.Context) { +// Логика Ñ€ÐµÐ´Ð°ÐºÑ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ñ‚ÐµÐ½Ð´ÐµÑ€Ð° +} + +func rollbackTender(c *gin.Context) { +// Логика отката верÑии тендера +} + +func createBid(c *gin.Context) { +// Логика ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð½Ð¾Ð²Ð¾Ð³Ð¾ Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ +} + +func getBidsByUser(c *gin.Context) { +// Логика Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ð¹ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ +} + +func getBidsForTender(c *gin.Context) { +// Логика Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ð¹ Ð´Ð»Ñ Ñ‚ÐµÐ½Ð´ÐµÑ€Ð° +} + +func editBid(c *gin.Context) { +// Логика Ñ€ÐµÐ´Ð°ÐºÑ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ +} + +func rollbackBid(c *gin.Context) { +// Логика отката верÑии Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ +} + +</code></pre> \ No newline at end of file diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e47823eeed09876f4d3850f90e1c7a81bdaede78 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,38 @@ +# Gradle Docker Codefresh example + +This is an example Java application that uses Spring Boot 2, Gradle and Docker +It is compiled using Codefresh. + +If you are looking for Maven, then see this [example](https://github.com/codefresh-contrib/spring-boot-2-sample-app) + +## Create a multi-stage docker image + +To compile and package using Docker multi-stage builds + +```bash +docker build . -t my-app +``` + +## Create a Docker image packaging an existing jar + +```bash +./gradlew build +docker build . -t my-app -f Dockerfile.only-package +``` + +## To run the docker image + +```bash +docker run -p 8080:8080 my-app +``` + +And then visit http://localhost:8080 in your browser. + +## To use this project in Codefresh + +There is also a [codefresh.yml](codefresh.yml) for easy usage with the [Codefresh](codefresh.io) CI/CD platform. + +For the simple packaging pipeline see [codefresh-package-only.yml](codefresh-package-only.yml) + +More details can be found in [Codefresh documentation](https://codefresh.io/docs/docs/learn-by-example/java/gradle/) + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..3f91239dabe9f72de0925d3404fa027cb8139de8 --- /dev/null +++ b/build.gradle @@ -0,0 +1,26 @@ +plugins { + id 'org.springframework.boot' version '2.1.4.RELEASE' + id 'java' +} + +apply plugin: 'io.spring.dependency-management' + +group = 'io.codefresh' +version = '0.0.1-SNAPSHOT' +sourceCompatibility = '1.8' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-web' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +test { + useJUnit() + + maxHeapSize = '1G' +} \ No newline at end of file diff --git a/cmd/app/main.go b/cmd/app/main.go new file mode 100644 index 0000000000000000000000000000000000000000..4cda59a11a909fc44539a1b968ca4920e364a0db --- /dev/null +++ b/cmd/app/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "flag" + "log" + "os" + + "github.com/DarRo9/Tenders/internal/app" + "github.com/DarRo9/Tenders/internal/config" +) + +func main() { + fl := flag.Bool("l", false, "Show logs") + flag.Parse() + + cfg := config.TakeConfig(*fl) + err := config.ConfirmConfig(cfg) + if err != nil { + log.Fatal(err) + os.Exit(1) + } + + app.Start(cfg) +} diff --git a/codefresh-package-only.yml b/codefresh-package-only.yml new file mode 100644 index 0000000000000000000000000000000000000000..ecd6076e2d92d421d1848d3ccf739d6c5211f1de --- /dev/null +++ b/codefresh-package-only.yml @@ -0,0 +1,34 @@ +version: '1.0' +stages: + - prepare + - test + - package + - build +steps: + main_clone: + title: Cloning main repository... + stage: prepare + type: git-clone + repo: 'codefresh-contrib/gradle-sample-app' + revision: master + git: github + MyUnitTests: + title: Compile/Unit test + stage: test + image: gradle:4.7.0-jdk8-alpine + commands: + - gradle test --no-daemon --build-cache --gradle-user-home=/codefresh/volume/.gradle -Dmaven.repo.local=/codefresh/volume/m2 + BuildMyJar: + title: Packaging Jar file + stage: package + image: gradle:4.7.0-jdk8-alpine + commands: + - gradle build --no-daemon --build-cache --gradle-user-home=/codefresh/volume/.gradle -Dmaven.repo.local=/codefresh/volume/m2 + MyAppDockerImage: + title: Building Docker Image + type: build + stage: build + image_name: gradle-sample-app + working_directory: ./ + tag: 'non-multi-stage' + dockerfile: Dockerfile.only-package diff --git a/codefresh.yml b/codefresh.yml new file mode 100644 index 0000000000000000000000000000000000000000..118117352cacaab57aca08a0e702fd8904fc8afd --- /dev/null +++ b/codefresh.yml @@ -0,0 +1,20 @@ +version: '1.0' +stages: + - prepare + - build +steps: + main_clone: + title: Cloning main repository... + stage: prepare + type: git-clone + repo: 'codefresh-contrib/gradle-sample-app' + revision: master + git: github + BuildingDockerImage: + title: Building Docker Image + stage: build + type: build + image_name: gradle-sample-app + working_directory: ./ + tag: 'multi-stage' + dockerfile: Dockerfile \ No newline at end of file diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7499bcafdf084e0fdb580a381f2cc500a6c8b4be --- /dev/null +++ b/config.yaml @@ -0,0 +1,3 @@ +ci_access: + groups: + - id: avito-testirovanie-na-backend-1270 diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3dd1f630bb3fcfd1688f8278584fe7bff5ade64a --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,26 @@ +version: '3.7' + +services: + postgres: + image: postgres:latest + container_name: postgres + restart: always + volumes: + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DATABASE=avito + - POSTGRES_SSL_MODE=disable + networks: + my_net: + ipv4_address: 172.18.1.2 +volumes: + post_avito_data: + +networks: + my_net: + driver: bridge + ipam: + config: + - subnet: 172.18.1.0/24 \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..6bce5514e00074db40fbf71b84356ab82466e1da --- /dev/null +++ b/go.mod @@ -0,0 +1,44 @@ +module github.com/DarRo9/Tenders/test_avito + +go 1.22.5 + +require ( + github.com/DarRo9/Tenders v0.0.0-20240913084324-41d3c82db720 + github.com/gin-gonic/gin v1.10.0 + github.com/golang-migrate/migrate/v4 v4.17.1 + github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 + github.com/sirupsen/logrus v1.9.2 +) + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/atomic v1.7.0 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..1ce2e552e88ca2d61551377f7a52d0467574691b --- /dev/null +++ b/go.sum @@ -0,0 +1,137 @@ +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/DarRo9/Tenders v0.0.0-20240913084324-41d3c82db720 h1:1diOHc5Hu7zYEC8itbWMeM6tY/ec/+8P55KGIGdwH64= +github.com/DarRo9/Tenders v0.0.0-20240913084324-41d3c82db720/go.mod h1:h9GY5WyXvI8v6hrvt9Piv7te13CNedBr1WyVmq/LTTM= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +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/dhui/dktest v0.4.1 h1:/w+IWuDXVymg3IrRJCHHOkMK10m9aNVMOyD0X12YVTg= +github.com/dhui/dktest v0.4.1/go.mod h1:DdOqcUpL7vgyP4GlF3X3w7HbSlz8cEQzwewPveYEQbA= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0= +github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4= +github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +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-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/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +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/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= +github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= +golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..87b738cbd051603d91cc39de6cb000dd98fe6b02 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000000000000000000000000000000000..f4d7b2bf616f7674854ff527df47b371b72472da --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000000000000000000000000000000000000..af6708ff229fda75da4f7cc4da4747217bac4d53 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000000000000000000000000000000000..6d57edc706c93465988754383a2d7ff353d4e79f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/init.sql b/init.sql new file mode 100644 index 0000000000000000000000000000000000000000..31440f7702e634fa900b83d7e39af86dd5423da6 --- /dev/null +++ b/init.sql @@ -0,0 +1,73 @@ +CREATE DATABASE avito; +\c avito; +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE TABLE employee ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + username VARCHAR(50) UNIQUE NOT NULL, + first_name VARCHAR(50), + last_name VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TYPE organization_type AS ENUM ( + 'IE', + 'LLC', + 'JSC' +); + +CREATE TABLE organization ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(100) NOT NULL, + description TEXT, + type organization_type, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE organization_responsible ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + organization_id UUID REFERENCES organization(id) ON DELETE CASCADE, + user_id UUID REFERENCES employee(id) ON DELETE CASCADE +); + +-- Ð’Ñтавка данных в таблицу employee +INSERT INTO employee (username, first_name, last_name) +VALUES + ('john_doe', 'John', 'Doe'), + ('jane_smith', 'Jane', 'Smith'), + ('alex_brown', 'Alex', 'Brown'), + ('emily_jones', 'Emily', 'Jones'), + ('michael_white', 'Michael', 'White'), + ('jdoe', 'John', 'Doe'), + ('asmith', 'Alice', 'Smith'), + ('bjackson', 'Bob', 'Jackson'), + ('cjohnson', 'Charlie', 'Johnson'), + ('dlee', 'Diana', 'Lee'); + +-- Ð’Ñтавка данных в таблицу organization +INSERT INTO organization (name, description, type) +VALUES + ('Tech Innovations', 'A company focused on technological advancements.', 'LLC'), + ('Green Solutions', 'An organization dedicated to environmental sustainability.', 'JSC'), + ('Fast Delivery Services', 'Logistics and delivery company.', 'IE'), + ('Creative Designs', 'A design agency specializing in branding.', 'LLC'), + ('Health First', 'A health and wellness organization.', 'JSC'), + ('Tech Innovations LLC', 'A company focused on technological advancements.', 'LLC'), + ('Green Energy JSC', 'A joint stock company specializing in renewable energy.', 'JSC'), + ('Health Solutions IE', 'An individual enterprise providing health services.', 'IE'), + ('Global Logistics LLC', 'Logistics and supply chain management services.', 'LLC'), + ('Creative Media JSC', 'A joint stock company in the media and entertainment industry.', 'JSC');; + +INSERT INTO organization_responsible (organization_id, user_id) VALUES +((SELECT id FROM organization WHERE name = 'Tech Innovations LLC'), (SELECT id FROM employee WHERE username = 'jdoe')), +((SELECT id FROM organization WHERE name = 'Green Energy JSC'), (SELECT id FROM employee WHERE username = 'asmith')), +((SELECT id FROM organization WHERE name = 'Health Solutions IE'), (SELECT id FROM employee WHERE username = 'bjackson')), +((SELECT id FROM organization WHERE name = 'Global Logistics LLC'), (SELECT id FROM employee WHERE username = 'cjohnson')), +((SELECT id FROM organization WHERE name = 'Creative Media JSC'), (SELECT id FROM employee WHERE username = 'dlee')), +((SELECT id FROM organization WHERE name = 'Tech Innovations'), (SELECT id FROM employee WHERE username = 'john_doe')), +((SELECT id FROM organization WHERE name = 'Green Solutions'), (SELECT id FROM employee WHERE username = 'jane_smith')), +((SELECT id FROM organization WHERE name = 'Fast Delivery Services'), (SELECT id FROM employee WHERE username = 'alex_brown')), +((SELECT id FROM organization WHERE name = 'Creative Designs'), (SELECT id FROM employee WHERE username = 'emily_jones')), +((SELECT id FROM organization WHERE name = 'Health First'), (SELECT id FROM employee WHERE username = 'michael_white')); \ No newline at end of file diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000000000000000000000000000000000000..c80b91c031adc62994ac527f3b3dd315429d2cea --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,52 @@ +package app + +import ( + "os" + "os/signal" + "syscall" + + "github.com/DarRo9/Tenders/internal/config" + "github.com/DarRo9/Tenders/internal/handler" + "github.com/DarRo9/Tenders/internal/repo" + "github.com/DarRo9/Tenders/internal/service" + "github.com/DarRo9/Tenders/pkg/server" + log "github.com/sirupsen/logrus" +) + +var ( + migrationsFile = "file://migrations" +) + +func Start(cfg *config.Config) { + MakeLogs("Start") + mgr := MakeMigration(migrationsFile, cfg.Postgres.ConnString) + err := mgr.Use() + if err != nil { + log.Printf("%v\n", err) + } + pdb, err := repo.MakeDB(cfg.Postgres) + if err != nil { + log.Printf("%v\n", err) + return + } + repo := repo.MakeRepo(pdb) + serv := service.MakeService(repo) + handler := handler.MakeHandler(serv) + route := handler.MakeRoutes() + server := server.Server{} + + go func() { + if err = server.Start(cfg.HttpServer, route); err != nil { + log.Printf("Error: can't start server: %v", err) + return + } + }() + + defer server.Shutdown(nil) + log.Printf("Server successfully started on %s\n", "http://"+cfg.Address) + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + log.Println("End of servers work") + +} diff --git a/internal/app/logger.go b/internal/app/logger.go new file mode 100644 index 0000000000000000000000000000000000000000..59ec833ab555044c44b6d8d830c4726d3ba762af --- /dev/null +++ b/internal/app/logger.go @@ -0,0 +1,19 @@ +package app + +import ( + "os" + "time" + + log "github.com/sirupsen/logrus" +) + +func MakeLogs(level string) { + logrusLevel, err := log.ParseLevel(level) + if err != nil { + log.SetLevel(log.DebugLevel) + } else { + log.SetLevel(logrusLevel) + } + log.SetFormatter(&log.TextFormatter{TimestampFormat: time.RFC3339}) + log.SetOutput(os.Stdout) +} diff --git a/internal/app/migration.go b/internal/app/migration.go new file mode 100644 index 0000000000000000000000000000000000000000..428eaa0d57a8fe816e07d4f3f2d05c84f0f3c7b8 --- /dev/null +++ b/internal/app/migration.go @@ -0,0 +1,52 @@ +package app + +import ( + "errors" + "fmt" + "time" + + "github.com/golang-migrate/migrate/v4" + _ "github.com/golang-migrate/migrate/v4/database/postgres" + _ "github.com/golang-migrate/migrate/v4/source/file" +) + +var ( + defaultAttempts = 10 + defaultTimeout = time.Second +) + +type Migrator struct { + migrationsFile string + pdbectionString string +} + +func MakeMigration(migrationFile, pdb string) *Migrator { + return &Migrator{ + migrationsFile: migrationFile, + pdbectionString: pdb, + } +} + +func (m *Migrator) Use() error { + var mgr *migrate.Migrate + var err error + + for attempt := 0; attempt < defaultAttempts; attempt++ { + mgr, err = migrate.New(m.migrationsFile, m.pdbectionString) + if err == nil { + break + } + + time.Sleep(defaultTimeout) + } + + if err != nil { + return fmt.Errorf("Can't create migration: %v", err) + } + + defer mgr.Close() + if err = mgr.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { + return fmt.Errorf("Can't to apply migrations: %v", err) + } + return nil +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000000000000000000000000000000000000..259c462a18321edb89882578af030d7ece1cce68 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,73 @@ +package config + +import ( + "errors" + "fmt" + "log" + "os" + "reflect" + + "github.com/joho/godotenv" +) + +type Postgres struct { + ConnString string `env:"POSTGRES_CONN"` + JDBCUrl string `env:"POSTGRES_JDBC_URL"` + Username string `env:"POSTGRES_USERNAME"` + Password string `env:"POSTGRES_PASSWORD"` + Host string `env:"POSTGRES_HOST" env-default:"localhost"` + Port string `env:"POSTGRES_PORT" env-default:"5432"` + Database string `env:"POSTGRES_DATABASE" env-default:"postgres"` +} + +type HttpServer struct { + Address string `env:"SERVER_ADDRESS" env-default:"0.0.0.0:8080"` +} + +type Config struct { + HttpServer `yaml:"http_server"` + Postgres `yaml:"postgres"` +} + +func TakeConfig(loader bool) *Config { + var conf Config + if loader { + err := godotenv.Load() + if err != nil { + log.Fatalf("error loading env variables: %s", err) + } + } + + conf.HttpServer.Address = os.Getenv("SERVER_ADDRESS") + conf.Postgres.ConnString = os.Getenv("POSTGRES_CONN") + conf.Postgres.JDBCUrl = os.Getenv("POSTGRES_JDBC_URL") + conf.Postgres.Username = os.Getenv("POSTGRES_USERNAME") + conf.Postgres.Password = os.Getenv("POSTGRES_PASSWORD") + conf.Postgres.Host = os.Getenv("POSTGRES_HOST") + conf.Postgres.Port = os.Getenv("POSTGRES_PORT") + conf.Postgres.Database = os.Getenv("POSTGRES_DATABASE") + + return &conf +} + +func ConfirmConfig(cfg *Config) error { + return confirmConfig(reflect.ValueOf(*cfg)) +} + +func confirmConfig(v reflect.Value) error { + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + if field.Kind() == reflect.String { + if field.String() == "" { + return errors.New(fmt.Sprintf("The %s field must not be empty", v.Type().Field(i).Name)) + } + } + + if field.Kind() == reflect.Struct { + if err := confirmConfig(field); err != nil { + return err + } + } + } + return nil +} diff --git a/internal/domain/bid.go b/internal/domain/bid.go new file mode 100644 index 0000000000000000000000000000000000000000..e270606366b21757ee6cafa70d290f40ae32eb9a --- /dev/null +++ b/internal/domain/bid.go @@ -0,0 +1,15 @@ +package domain + +import "time" + +type Bid struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Status string `json:"status"` + TenderID string `json:"tender_id"` + AuthorType string `json:"author_type"` + AuthorID string `json:"author_id"` + Version int `json:"version"` + CreatedAt time.Time `json:"created_at"` +} diff --git a/internal/domain/tender.go b/internal/domain/tender.go new file mode 100644 index 0000000000000000000000000000000000000000..8170501ec0252338b9bba3a9c2930c3ebf570db5 --- /dev/null +++ b/internal/domain/tender.go @@ -0,0 +1,28 @@ +package domain + +import "time" + +type Tender struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Status string `json:"status"` + ServiceType string `json:"service_type"` + Version int `json:"version"` + CreatedAt time.Time `json:"created_at"` +} + +type TenderCreator struct { + Name string `json:"name"` + Description string `json:"description"` + Status string `json:"status"` + ServiceType string `json:"serviceType"` + OrganizationID string `json:"organizationId"` + CreatorUsername string `json:"creatorUsername"` +} + +type TenderEditor struct { + Name string `json:"name"` + Description string `json:"description"` + ServiceType string `json:"serviceType"` +} diff --git a/internal/handler/err.go b/internal/handler/err.go new file mode 100644 index 0000000000000000000000000000000000000000..d94eb6d9aaa0816b99e125ea334ab198052d457a --- /dev/null +++ b/internal/handler/err.go @@ -0,0 +1,8 @@ +package handler + +import "errors" + +var ( + ErrUserIdType = errors.New("invalid type of user id") + ErrUnnknownRequest = errors.New("unknown request") +) diff --git a/internal/handler/handler.go b/internal/handler/handler.go new file mode 100644 index 0000000000000000000000000000000000000000..28ab8dc482fbbfbc07d0b863eebe848643c5dfcf --- /dev/null +++ b/internal/handler/handler.go @@ -0,0 +1,47 @@ +package handler + +import ( + "net/http" + + "github.com/DarRo9/Tenders/internal/service" + "github.com/gin-gonic/gin" +) + +const ( + userIDCtx = "userid" +) + +type Handler struct { + service *service.Service +} + +func ping(c *gin.Context) { + c.String(http.StatusOK, "ok") +} + +func (h *Handler) MakeRoutes() *gin.Engine { + router := gin.New() + api := router.Group("/api") + { + api.GET("/ping", ping) + + tenders := api.Group("/tenders") + { + tenders.GET("/", h.getTenders) + tenders.POST("/new", h.makeTender) + } + + tendersWithAuth := api.Group("/tenders", h.userAuth) + { + tendersWithAuth.GET("/my", h.getUserTenders) + tendersWithAuth.GET("/:tenderid/status", h.getStatusTender) + tendersWithAuth.PUT("/:tenderid/status", h.setStatusTender) + } + } + + return router +} + +func MakeHandler(service *service.Service) *Handler { + return &Handler{service: service} +} diff --git a/internal/handler/middleware.go b/internal/handler/middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..97dea719095c363b1b2a28b407d0268f40da734b --- /dev/null +++ b/internal/handler/middleware.go @@ -0,0 +1,56 @@ +package handler + +import ( + "net/http" + + "github.com/DarRo9/Tenders/internal/service" + "github.com/gin-gonic/gin" +) + +func (h *Handler) userAuth(c *gin.Context) { + username := c.DefaultQuery("username", "") + if username == "" { + c.JSON(http.StatusUnauthorized, gin.H{"cause": service.ErrUnauthorizedError.Error()}) + return + } + userUUID, err := h.service.Auth.GetUserId(username) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"cause": err.Error()}) + return + } + + _, err = h.service.Auth.GetUserChargeId(userUUID) + if err != nil { + c.JSON(http.StatusForbidden, gin.H{"cause": err.Error()}) + return + } + + c.Set(userIDCtx, userUUID) +} + +func getUserId(c *gin.Context) (string, error) { + id, ok := c.Get(userIDCtx) + if !ok { + return "", service.ErrUserNotFound + } + idS, ok := id.(string) + if !ok { + return "", ErrUserIdType + } + return idS, nil +} + +func (h *Handler) createdUserIdentity(c *gin.Context) { + username := c.DefaultQuery("name", "") + if username == "" { + c.JSON(http.StatusUnauthorized, gin.H{"cause": service.ErrUnauthorizedError.Error()}) + return + } + userUUID, err := h.service.Auth.GetUserId(username) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"cause": err.Error()}) + return + } + + c.Set(userIDCtx, userUUID) +} diff --git a/internal/handler/tenders.go b/internal/handler/tenders.go new file mode 100644 index 0000000000000000000000000000000000000000..21ddc0214b4c1e85a5ccfa080c96cbe3b7991660 --- /dev/null +++ b/internal/handler/tenders.go @@ -0,0 +1,162 @@ +package handler + +import ( + "net/http" + "strconv" + + "github.com/DarRo9/Tenders/internal/domain" + "github.com/gin-gonic/gin" +) + +func (h *Handler) setStatusTender(c *gin.Context) { + userUUID, err := getUserId(c) + if err != nil { + return + } + tenderid := c.Param("tenderid") + if tenderid == "" { + c.JSON(http.StatusBadRequest, gin.H{"cause": ErrUnnknownRequest.Error()}) + return + } + status, isExist := c.GetQuery("status") + if !isExist { + c.JSON(http.StatusBadRequest, gin.H{"cause": ErrUnnknownRequest.Error()}) + return + } + tender, err := h.service.Tender.UpdateStatusTender(tenderid, status, userUUID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"cause": err.Error()}) + return + } + + c.JSON(http.StatusOK, tender) +} + +func (h *Handler) getStatusTender(c *gin.Context) { + userUUID, err := getUserId(c) + if err != nil { + return + } + + tenderid := c.Param("tenderid") + if tenderid == "" { + c.JSON(http.StatusBadRequest, gin.H{"cause": ErrUnnknownRequest.Error()}) + return + } + + status, err := h.service.Tender.GetStatusTenderByTenderID(tenderid, userUUID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"cause": err.Error()}) + return + } + c.String(http.StatusOK, status) +} + +func (h *Handler) editTender(c *gin.Context) { + var tenderEditor domain.TenderEditor + userUUID, err := getUserId(c) + if err != nil { + return + } + tenderid := c.Param("tenderid") + if tenderid == "" { + c.JSON(http.StatusBadRequest, gin.H{"cause": ErrUnnknownRequest.Error()}) + return + } + + if err := c.BindJSON(&tenderEditor); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"cause": err.Error()}) + return + } + + tender, err := h.service.Tender.EditTender(tenderid, userUUID, &tenderEditor) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"cause": err.Error()}) + return + } + c.JSON(http.StatusOK, tender) +} + +func (h *Handler) getTenders(c *gin.Context) { + serviceTypes := c.DefaultQuery("service_type", "") + offset, err := strconv.Atoi(c.DefaultQuery("offset", "0")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"cause": err.Error()}) + return + } + limit, err := strconv.Atoi(c.DefaultQuery("limit", "5")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"cause": err.Error()}) + return + } + + tenders, err := h.service.Tender.GetTenders(limit, offset, serviceTypes) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"cause": err.Error()}) + return + } + + c.JSON(http.StatusOK, tenders) +} + +func (h *Handler) getUserTenders(c *gin.Context) { + var tenders []domain.Tender + + userUUID, err := getUserId(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"cause": err.Error()}) + return + } + + offset, err := strconv.Atoi(c.DefaultQuery("offset", "0")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"cause": err.Error()}) + return + } + limit, err := strconv.Atoi(c.DefaultQuery("limit", "5")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"cause": err.Error()}) + return + } + + tenders, err = h.service.Tender.GetTendersByUserUUID(limit, offset, userUUID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"cause": err.Error()}) + return + } + c.JSON(http.StatusOK, tenders) +} + +func (h *Handler) makeTender(c *gin.Context) { + var tenderCreator domain.TenderCreator + if err := c.BindJSON(&tenderCreator); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"cause": err.Error()}) + return + } + userUUID, err := h.service.Auth.GetUserId(tenderCreator.CreatorUsername) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"cause": err.Error()}) + return + } + _, err = h.service.Auth.CheckUserCharge(userUUID, tenderCreator.OrganizationID) + if err != nil { + c.JSON(http.StatusForbidden, gin.H{"cause": err.Error()}) + return + } + var userChargeId string + + if !h.service.Auth.IsUserChargeExist(tenderCreator.CreatorUsername) { + userChargeId, err = h.service.Auth.CreateUserCharge(userUUID, tenderCreator.CreatorUsername) + } else { + userChargeId, err = h.service.Auth.GetUserChargeId(tenderCreator.CreatorUsername) + } + + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"cause": err.Error()}) + return + } + + tender, err := h.service.CreateTender(tenderCreator, userChargeId) + + c.JSON(http.StatusOK, tender) +} diff --git a/internal/repo/auth.go b/internal/repo/auth.go new file mode 100644 index 0000000000000000000000000000000000000000..0f671c3019c5789c225240454f71aeaa6811d8fd --- /dev/null +++ b/internal/repo/auth.go @@ -0,0 +1,71 @@ +package repo + +import ( + "context" + "database/sql" + + log "github.com/sirupsen/logrus" +) + +type AuthRepo struct { + db *sql.DB +} + +func NewAuthRepo(db *sql.DB) *AuthRepo { + return &AuthRepo{db: db} +} + +func (a *AuthRepo) GetUserChargeUUID(username string) (string, error) { + var uuid string + ctx, cancelFn := context.WithTimeout(context.Background(), timeoutCtx) + defer cancelFn() + query := `SELECT id FROM user_charges WHERE username = $1;` + err := a.db.QueryRowContext(ctx, query, username).Scan(&uuid) + if err != nil { + log.Debugf("%s: %v", ErrUserNotFound, err) + return "", ErrUserNotFound + } + + return uuid, nil +} + +func (a *AuthRepo) CheckUserCharge(userid, organisationid string) (string, error) { + query := `SELECT user_id FROM organization_responsible WHERE user_id = $1 AND organization_id = $2;` + + var uuid string + ctx, cancelFn := context.WithTimeout(context.Background(), timeoutCtx) + defer cancelFn() + err := a.db.QueryRowContext(ctx, query, userid, organisationid).Scan(&uuid) + if err != nil { + log.Debugf("%s: %v", ErrUserChargeNotFound, err) + return "", ErrUserChargeNotFound + } + + return uuid, nil +} + +func (a *AuthRepo) GetUserUUID(username string) (string, error) { + var uuid string + ctx, cancelFn := context.WithTimeout(context.Background(), timeoutCtx) + defer cancelFn() + query := `SELECT id FROM employee WHERE username = $1;` + err := a.db.QueryRowContext(ctx, query, username).Scan(&uuid) + if err != nil { + log.Debugf("%s: %v", ErrUserNotFound, err) + return "", ErrUserNotFound + } + return uuid, nil +} + +func (a *AuthRepo) CreateUserCharge(userid, username string) (string, error) { + var createdUserId string + query := `INSERT INTO user_charges(user_id, username) VALUES ($1, $2) RETURNING id;` + ctx, cancelFn := context.WithTimeout(context.Background(), timeoutCtx) + defer cancelFn() + err := a.db.QueryRowContext(ctx, query, userid, username).Scan(&createdUserId) + if err != nil { + return "", err + } + + return createdUserId, nil +} diff --git a/internal/repo/err.go b/internal/repo/err.go new file mode 100644 index 0000000000000000000000000000000000000000..101d0a7fbaefbad68b4c09019977f3c999d08721 --- /dev/null +++ b/internal/repo/err.go @@ -0,0 +1,17 @@ +package repo + +import "errors" + +var ( + ErrDatabaseConnectionFailed = errors.New("db pdbection failed") + ErrUserChargeNotFound = errors.New("user charge not found") + ErrUserNotFound = errors.New("user not found") + ErrCreationTender = errors.New("can't create tender") + ErrScanDataTender = errors.New("can't scan data tender") + ErrTenderNotFound = errors.New("tender not found") + ErrTenderStatusNotFound = errors.New("tenders status not found") + ErrInFailedTransaction = errors.New("transaction failed") + ErrUpdatedTender = errors.New("updating tender failed") + ErrFetchingTender = errors.New("fetching tender faled") + ErrCommittingTransaction = errors.New("committing tender failed") +) diff --git a/internal/repo/postgres.go b/internal/repo/postgres.go new file mode 100644 index 0000000000000000000000000000000000000000..5793130cebf91e1805b0e1088e48178e4cdb072b --- /dev/null +++ b/internal/repo/postgres.go @@ -0,0 +1,25 @@ +package repo + +import ( + "database/sql" + + "github.com/DarRo9/Tenders/internal/config" + _ "github.com/lib/pq" + log "github.com/sirupsen/logrus" +) + +func MakeDB(cfg config.Postgres) (*sql.DB, error) { + db, err := sql.Open("postgres", cfg.ConnString) + if err != nil { + log.Fatalf("%s: %v", ErrDatabaseConnectionFailed, err) + return nil, ErrDatabaseConnectionFailed + } + + err = db.Ping() + if err != nil { + log.Fatalf("%s: %v", ErrDatabaseConnectionFailed, err) + return nil, ErrDatabaseConnectionFailed + } + + return db, nil +} diff --git a/internal/repo/repo.go b/internal/repo/repo.go new file mode 100644 index 0000000000000000000000000000000000000000..3527928de75c9a80c59f7d5b1721f56505f1bc25 --- /dev/null +++ b/internal/repo/repo.go @@ -0,0 +1,37 @@ +package repo + +import ( + "database/sql" + + "github.com/DarRo9/Tenders/internal/domain" +) + +type Tender interface { + CreateTender(tender domain.Tender, orgID, creatorID string) (domain.Tender, error) + GetTenders(limit, offset int, serviceType string) ([]domain.Tender, error) + GetTendersByUserID(limit, offset int, uuid string) ([]domain.Tender, error) + GetStatusTenderById(tenderUUID, userUUID string) (string, error) + UpdateStatusTenderById(tenderUUID, status, userUUID string) (domain.Tender, error) + UpdateTender(tenderUUID, userUUID string, tenderEditor *domain.TenderEditor) (domain.Tender, error) + GetTenderById(tenderUUID string) (domain.Tender, error) + //RollbackTender(tenderUUID, userUUID string, version int) (domain.Tender, error) +} + +type Auth interface { + CheckUserCharge(userid, organisationid string) (string, error) + CreateUserCharge(userid, username string) (string, error) + GetUserUUID(username string) (string, error) + GetUserChargeUUID(username string) (string, error) +} + +type Repository struct { + Auth + Tender +} + +func MakeRepo(db *sql.DB) *Repository { + return &Repository{ + Tender: NewTenderRepo(db), + Auth: NewAuthRepo(db), + } +} diff --git a/internal/repo/tender.go b/internal/repo/tender.go new file mode 100644 index 0000000000000000000000000000000000000000..d80515e5d184df916e63a3c549c26ec0f34ebfc6 --- /dev/null +++ b/internal/repo/tender.go @@ -0,0 +1,334 @@ +package repo + +import ( + "context" + "database/sql" + "strconv" + "time" + + "github.com/DarRo9/Tenders/internal/domain" + log "github.com/sirupsen/logrus" +) + +const timeoutCtx = 5 * time.Second + +type TenderRepo struct { + db *sql.DB +} + +func NewTenderRepo(db *sql.DB) *TenderRepo { + return &TenderRepo{db: db} +} + +func (t *TenderRepo) CreateTender(tender domain.Tender, orgID, userUUID string) (domain.Tender, error) { + query := `INSERT INTO tenders (name, description, service_type, status, organization_id, creator_id, version) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id` + ctx, cancelFn := context.WithTimeout(context.Background(), timeoutCtx) + defer cancelFn() + var uuid string + err := t.db.QueryRowContext(ctx, query, + tender.Name, + tender.Description, + tender.ServiceType, + tender.Status, + orgID, + userUUID, + tender.Version).Scan(&uuid) + + if err != nil { + log.Debugf("%s: %v", ErrCreationTender, err) + return domain.Tender{}, ErrCreationTender + } + tender.ID = uuid + return tender, nil +} + +func (t *TenderRepo) GetStatusTenderById(tenderUUID, userUUID string) (string, error) { + var status string + + query := `SELECT status + FROM tenders WHERE creator_id = $1 AND id = $2;` + + ctx, cancelFn := context.WithTimeout(context.Background(), timeoutCtx) + defer cancelFn() + + err := t.db.QueryRowContext(ctx, query, userUUID, tenderUUID).Scan(&status) + if err != nil { + log.Printf("%s: %v", ErrTenderStatusNotFound, err) + return "", ErrTenderStatusNotFound + } + + return status, nil +} + +func (t *TenderRepo) UpdateStatusTenderById(tenderUUID, status, userUUID string) (domain.Tender, error) { + var tender domain.Tender + tx, err := t.db.Begin() + if err != nil { + log.Debugf("%s: %v", ErrInFailedTransaction, err) + return domain.Tender{}, ErrInFailedTransaction + } + ctx, cancelFn := context.WithTimeout(context.Background(), timeoutCtx) + defer cancelFn() + + updateStatusQuery := `UPDATE tenders SET status = $1, + version = COALESCE((SELECT MAX(version) FROM tenders_history WHERE tender_id = $3), 0) + 1 + WHERE creator_id = $2 AND id = $3;` + + _, err = tx.ExecContext(ctx, updateStatusQuery, status, userUUID, tenderUUID) + if err != nil { + tx.Rollback() + log.Debugf("%s: %v", ErrUpdatedTender, err) + return domain.Tender{}, ErrUpdatedTender + } + + getTenderByIdQuery := `SELECT id, name, description, + service_type, status, + version, created_at + FROM tenders WHERE id = $1;` + + err = tx.QueryRowContext(ctx, getTenderByIdQuery, tenderUUID).Scan( + &tender.ID, + &tender.Name, + &tender.Description, + &tender.ServiceType, + &tender.Status, + &tender.Version, + &tender.CreatedAt, + ) + if err != nil { + tx.Rollback() + if err == sql.ErrNoRows { + log.Debugf("%s: %v", ErrTenderNotFound, err) + return domain.Tender{}, ErrTenderNotFound + } + log.Debugf("%s: %v", ErrFetchingTender, err) + return domain.Tender{}, ErrFetchingTender + } + + if err := tx.Commit(); err != nil { + log.Debugf("%s: %v", ErrCommittingTransaction, err) + return domain.Tender{}, ErrCommittingTransaction + } + + return tender, nil +} + +func (t *TenderRepo) GetTenderById(tenderUUID string) (domain.Tender, error) { + var tender domain.Tender + query := `SELECT id, name, description, + service_type, status, + version, created_at + FROM tenders WHERE id = $1;` + + ctx, cancelFn := context.WithTimeout(context.Background(), timeoutCtx) + defer cancelFn() + err := t.db.QueryRowContext(ctx, query, tenderUUID).Scan(&tender.ID, + &tender.Name, + &tender.Description, + &tender.ServiceType, + &tender.Status, + &tender.Version, + &tender.CreatedAt) + if err != nil { + return domain.Tender{}, ErrTenderNotFound + } + + return tender, nil +} + +func (t *TenderRepo) GetTenders(limit, offset int, serviceType string) ([]domain.Tender, error) { + var tenders []domain.Tender + + query := `SELECT id, name, description, + service_type, status, + version, created_at + FROM tenders` + + params := []interface{}{} + paramsIndex := 1 + if serviceType != "" { + query += " WHERE service_type = $" + strconv.Itoa(paramsIndex) + params = append(params, serviceType) + paramsIndex++ + } + query += " ORDER BY name DESC" + query += " LIMIT $" + strconv.Itoa(paramsIndex) + " OFFSET $" + strconv.Itoa(paramsIndex+1) + params = append(params, limit, offset) + + ctx, cancelFn := context.WithTimeout(context.Background(), timeoutCtx) + defer cancelFn() + rows, err := t.db.QueryContext(ctx, query, params...) + if err != nil { + log.Debugf("%s: %v", ErrTenderNotFound, err) + return []domain.Tender{}, ErrTenderNotFound + } + + defer rows.Close() + for rows.Next() { + var tender domain.Tender + err = rows.Scan(&tender.ID, + &tender.Name, + &tender.Description, + &tender.ServiceType, + &tender.Status, + &tender.Version, + &tender.CreatedAt) + + if err != nil { + log.Debugf("%s: %v", ErrScanDataTender, err) + return []domain.Tender{}, ErrScanDataTender + } + tenders = append(tenders, tender) + } + return tenders, nil +} + +func (t *TenderRepo) GetTendersByUserID(limit, offset int, uuid string) ([]domain.Tender, error) { + var tenders []domain.Tender + query := `SELECT id, name, description, + service_type, status, + version, created_at + FROM tenders WHERE creator_id = $1 ORDER BY name DESC LIMIT $2 OFFSET $3;` + + ctx, cancelFn := context.WithTimeout(context.Background(), timeoutCtx) + defer cancelFn() + rows, err := t.db.QueryContext(ctx, query, uuid, limit, offset) + if err != nil { + log.Debugf("%s: %v", ErrTenderNotFound, err) + return []domain.Tender{}, ErrTenderNotFound + } + defer rows.Close() + + for rows.Next() { + var tender domain.Tender + err = rows.Scan(&tender.ID, + &tender.Name, + &tender.Description, + &tender.ServiceType, + &tender.Status, + &tender.Version, + &tender.CreatedAt) + + if err != nil { + log.Debugf("%s: %v", ErrScanDataTender, err) + return []domain.Tender{}, ErrScanDataTender + } + tenders = append(tenders, tender) + } + + return tenders, nil +} + +func (r *TenderRepo) RollbackTender(tenderUUID, userUUID string, version int) (domain.Tender, error) { + var tender domain.Tender + tx, err := r.db.Begin() + if err != nil { + log.Debugf("%s: %v", ErrInFailedTransaction, err) + return domain.Tender{}, ErrInFailedTransaction + } + ctx, cancelFn := context.WithTimeout(context.Background(), timeoutCtx) + defer cancelFn() + query := `UPDATE tenders t SET name = th.name, + description = th.description, + status = th.status, + service_type = th.service_type + FROM (SELECT tender_id, name, description, status, service_type + FROM tenders_history + WHERE tender_id = $1 AND version = $2) th + WHERE t.id = th.tender_id;` + + _, err = tx.ExecContext(ctx, query, tenderUUID, version) + if err != nil { + tx.Rollback() + log.Debugf("%s: %v", ErrTenderNotFound, err) + return domain.Tender{}, ErrTenderNotFound + } + getTenderByIdQuery := `SELECT id, name, description, + service_type, status, + version, created_at, updated_at + FROM tenders WHERE id = $1;` + + err = tx.QueryRowContext(ctx, getTenderByIdQuery, tenderUUID).Scan( + &tender.ID, + &tender.Name, + &tender.Description, + &tender.ServiceType, + &tender.Status, + &tender.Version, + &tender.CreatedAt, + ) + if err != nil { + tx.Rollback() + if err == sql.ErrNoRows { + log.Debugf("%s: %v", ErrTenderNotFound, err) + return domain.Tender{}, ErrTenderNotFound + } + log.Debugf("%s: %v", ErrFetchingTender, err) + return domain.Tender{}, ErrFetchingTender + } + + if err = tx.Commit(); err != nil { + log.Debugf("%s: %v", ErrCommittingTransaction, err) + return domain.Tender{}, ErrCommittingTransaction + } + + return tender, nil +} + +func (t *TenderRepo) UpdateTender(tenderUUID, userUUID string, tenderEditor *domain.TenderEditor) (domain.Tender, error) { + var tender domain.Tender + tx, err := t.db.Begin() + if err != nil { + log.Debugf("%s: %v", ErrInFailedTransaction, err) + return domain.Tender{}, ErrInFailedTransaction + } + ctx, cancelFn := context.WithTimeout(context.Background(), timeoutCtx) + defer cancelFn() + + updateStatusQuery := `UPDATE tenders SET name = $1, description = $2, service_type = $3, version = COALESCE((SELECT MAX(version) FROM tenders_history WHERE tender_id = $4), 0) + 1 + WHERE creator_id = $5 AND id = $6;` + _, err = tx.ExecContext(ctx, + updateStatusQuery, tenderEditor.Name, + tenderEditor.Description, + tenderEditor.ServiceType, + tenderUUID, + userUUID, tenderUUID) + + if err != nil { + tx.Rollback() + log.Debugf("%s: %v", ErrTenderNotFound, err) + return domain.Tender{}, ErrTenderNotFound + } + + getTenderByIdQuery := `SELECT id, name, description, + service_type, status, + version, created_at, updated_at + FROM tenders WHERE id = $1;` + + err = tx.QueryRowContext(ctx, getTenderByIdQuery, tenderUUID).Scan( + &tender.ID, + &tender.Name, + &tender.Description, + &tender.ServiceType, + &tender.Status, + &tender.Version, + &tender.CreatedAt, + ) + if err != nil { + tx.Rollback() + if err == sql.ErrNoRows { + log.Debugf("%s: %v", ErrTenderNotFound, err) + return domain.Tender{}, ErrTenderNotFound + } + log.Debugf("%s: %v", ErrFetchingTender, err) + return domain.Tender{}, ErrFetchingTender + } + + if err := tx.Commit(); err != nil { + log.Debugf("%s: %v", ErrCommittingTransaction, err) + return domain.Tender{}, ErrCommittingTransaction + } + + return tender, nil +} diff --git a/internal/service/auth.go b/internal/service/auth.go new file mode 100644 index 0000000000000000000000000000000000000000..2a31204e83c549f41f13047514ecf03a0fe71a66 --- /dev/null +++ b/internal/service/auth.go @@ -0,0 +1,35 @@ +package service + +import "github.com/DarRo9/Tenders/internal/repo" + +type AuthService struct { + repo *repo.Repository +} + +func (a *AuthService) GetUserChargeId(username string) (string, error) { + return a.repo.Auth.GetUserChargeUUID(username) +} + +func (a *AuthService) CheckUserCharge(userUUID, organisationid string) (string, error) { + return a.repo.Auth.CheckUserCharge(userUUID, organisationid) +} + +func NewAuthService(repo *repo.Repository) *AuthService { + return &AuthService{repo: repo} +} + +func (a *AuthService) GetUserId(username string) (string, error) { + return a.repo.Auth.GetUserUUID(username) +} + +func (a *AuthService) IsUserChargeExist(username string) bool { + _, err := a.repo.Auth.GetUserChargeUUID(username) + if err != nil { + return false + } + return true +} + +func (a *AuthService) CreateUserCharge(userUUID, username string) (string, error) { + return a.repo.Auth.CreateUserCharge(userUUID, username) +} diff --git a/internal/service/err.go b/internal/service/err.go new file mode 100644 index 0000000000000000000000000000000000000000..b1645d15828d0148c2f5c3c9d423aeec69abcafb --- /dev/null +++ b/internal/service/err.go @@ -0,0 +1,13 @@ +package service + +import ( + "errors" +) + +var ( + ErrUserNotFound = errors.New("user not found") + ErrTenderNotFound = errors.New("tender not found") + ErrServiceTypeError = errors.New("service type error") + ErrStatusError = errors.New("status error") + ErrUnauthorizedError = errors.New("unauthorized error") +) diff --git a/internal/service/service.go b/internal/service/service.go new file mode 100644 index 0000000000000000000000000000000000000000..9e39b03cfa26871cad6bf9fe3ec456000a3f8fd2 --- /dev/null +++ b/internal/service/service.go @@ -0,0 +1,35 @@ +package service + +import ( + "github.com/DarRo9/Tenders/internal/domain" + "github.com/DarRo9/Tenders/internal/repo" +) + +type Tender interface { + CreateTender(tenderCreator domain.TenderCreator, userUUID string) (domain.Tender, error) + GetTenders(limit, offset int, serviceType string) ([]domain.Tender, error) + GetTendersByUserUUID(limit, offset int, userUUID string) ([]domain.Tender, error) + GetStatusTenderByTenderID(tenderID, userUUID string) (string, error) + UpdateStatusTender(tenderUUID, status, userUUID string) (domain.Tender, error) + EditTender(tenderUUID, userUUID string, tenderEditor *domain.TenderEditor) (domain.Tender, error) +} + +type Auth interface { + GetUserId(username string) (string, error) + GetUserChargeId(username string) (string, error) + CheckUserCharge(userUUID, organisationid string) (string, error) + CreateUserCharge(userUUID, username string) (string, error) + IsUserChargeExist(username string) bool +} + +type Service struct { + Auth + Tender +} + +func MakeService(repo *repo.Repository) *Service { + return &Service{ + NewAuthService(repo), + NewTenderService(repo), + } +} diff --git a/internal/service/tender.go b/internal/service/tender.go new file mode 100644 index 0000000000000000000000000000000000000000..ac456d93106ef3bdedcd4cd535ba1ec57a43dbd1 --- /dev/null +++ b/internal/service/tender.go @@ -0,0 +1,72 @@ +package service + +import ( + "errors" + "time" + + "github.com/DarRo9/Tenders/internal/domain" + "github.com/DarRo9/Tenders/internal/repo" + log "github.com/sirupsen/logrus" +) + +type TenderService struct { + repo *repo.Repository +} + +func NewTenderService(repo *repo.Repository) *TenderService { + return &TenderService{repo: repo} +} + +func (t *TenderService) GetTenders(limit, offset int, serviceType string) ([]domain.Tender, error) { + return t.repo.Tender.GetTenders(limit, offset, serviceType) +} + +func (t *TenderService) CreateTender(tenderCreator domain.TenderCreator, userUUID string) (domain.Tender, error) { + tenderCreator.Status = "Created" + if !checkServiceType(tenderCreator.ServiceType) { + log.Debugf("%s: %v", ErrStatusError, errors.New("service type not found")) + return domain.Tender{}, ErrServiceTypeError + } + + var tender domain.Tender + tender = t.initTender(tenderCreator) + return t.repo.CreateTender(tender, tenderCreator.OrganizationID, userUUID) +} + +func (t *TenderService) initTender(creator domain.TenderCreator) domain.Tender { + return domain.Tender{ + Name: creator.Name, + Description: creator.Description, + ServiceType: creator.ServiceType, + Status: creator.Status, + Version: 1, + CreatedAt: time.Now(), + } +} + +func (t *TenderService) GetTendersByUserUUID(limit, offset int, uuid string) ([]domain.Tender, error) { + return t.repo.GetTendersByUserID(limit, offset, uuid) +} + +func (t *TenderService) EditTender(tenderUUID, userUUID string, tenderEditor *domain.TenderEditor) (domain.Tender, error) { + if tenderEditor == nil { + return t.repo.GetTenderById(tenderUUID) + } + if !checkServiceType(tenderEditor.ServiceType) { + return domain.Tender{}, ErrServiceTypeError + } + + return t.repo.UpdateTender(tenderUUID, userUUID, tenderEditor) +} + +func (t *TenderService) GetStatusTenderByTenderID(tenderID, userUUID string) (string, error) { + status, err := t.repo.Tender.GetStatusTenderById(tenderID, userUUID) + if err != nil { + return "", ErrTenderNotFound + } + return status, nil +} + +func (t *TenderService) UpdateStatusTender(tenderUUID, status, userUUID string) (domain.Tender, error) { + return t.repo.UpdateStatusTenderById(tenderUUID, status, userUUID) +} diff --git a/internal/service/validation.go b/internal/service/validation.go new file mode 100644 index 0000000000000000000000000000000000000000..49e49ee82904f48b5a5acf4eb9272ff8512c4e63 --- /dev/null +++ b/internal/service/validation.go @@ -0,0 +1,22 @@ +package service + +func checkServiceType(serviceType string) bool { + serviceTypes := []string{"Construction", "Delivery", "Manufacture"} + for _, sT := range serviceTypes { + if sT == serviceType { + return true + } + } + + return false +} + +func checkStatus(status string) bool { + allStatuses := []string{"Created", "Published", "Closed"} + for _, s := range allStatuses { + if status == s { + return true + } + } + return false +} diff --git a/migrations/20240908102818_init.down.sql b/migrations/20240908102818_init.down.sql new file mode 100644 index 0000000000000000000000000000000000000000..6decd4fd69db99878e620a15d781f129cb23d92d --- /dev/null +++ b/migrations/20240908102818_init.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS bids; +DROP TABLE IF EXISTS tenders; diff --git a/migrations/20240908102818_init.up.sql b/migrations/20240908102818_init.up.sql new file mode 100644 index 0000000000000000000000000000000000000000..4e69c08056cf4a0d800f89a2d36a103030542336 --- /dev/null +++ b/migrations/20240908102818_init.up.sql @@ -0,0 +1,70 @@ +CREATE TABLE user_charges ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + username VARCHAR(50) UNIQUE NOT NULL, + user_id UUID NOT NULL +); + +CREATE TABLE tenders ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(100) NOT NULL, + description VARCHAR(500), + service_type VARCHAR(50) CHECK (service_type IN ('Construction', 'Delivery', 'Manufacture')), + status VARCHAR(20) CHECK (status IN ('Created', 'Published', 'Closed')), + organization_id UUID NOT NULL, + creator_id UUID NOT NULL REFERENCES user_charges(id) ON DELETE CASCADE, + version INT DEFAULT 1 CHECK (version >= 1), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + + + +CREATE TABLE bids ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(100) NOT NULL, + description VARCHAR(500), + status VARCHAR(20) CHECK (status IN ('Created', 'Published', 'Canceled', 'Approved', 'Rejected')), + tender_id UUID REFERENCES tenders(id) ON DELETE CASCADE, + author_type VARCHAR(50), + author_id UUID, + version INT DEFAULT 1 CHECK (version >= 1), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + + + +CREATE TABLE tenders_history ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tender_id UUID NOT NULL, + name VARCHAR(100) NOT NULL, + description VARCHAR(500), + service_type VARCHAR(50) CHECK (service_type IN ('Construction', 'Delivery', 'Manufacture')), + status VARCHAR(20) CHECK (status IN ('Created', 'Published', 'Closed')), + organization_id UUID NOT NULL, + creator_id UUID NOT NULL, + version INT DEFAULT 1 CHECK (version >= 1), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE OR REPLACE FUNCTION tender_versions() +RETURNS TRIGGER AS $$ +BEGIN + IF (TG_OP = 'INSERT') THEN + INSERT INTO tenders_history (tender_id, name, description, service_type, status, organization_id, creator_id) + VALUES (NEW.id, NEW.name, NEW.description, NEW.service_type, NEW.status, NEW.organization_id, NEW.creator_id); + RETURN NEW; + ELSIF (TG_OP = 'UPDATE') THEN + INSERT INTO tenders_history (tender_id, name, description, service_type, status, organization_id, creator_id, version, created_at) + VALUES (NEW.id, NEW.name, NEW.description, NEW.service_type, NEW.status, NEW.organization_id, NEW.creator_id, NEW.version, NEW.created_at); + RETURN NEW; +END IF; +END; +$$ +LANGUAGE plpgsql; + + +CREATE TRIGGER tender_versions_trigger + AFTER INSERT OR UPDATE ON tenders + FOR EACH ROW + EXECUTE PROCEDURE tender_versions(); \ No newline at end of file diff --git a/pkg/server/server.go b/pkg/server/server.go new file mode 100644 index 0000000000000000000000000000000000000000..4ea082739c9db6cad7681f09edd2851471def86a --- /dev/null +++ b/pkg/server/server.go @@ -0,0 +1,29 @@ +package server + +import ( + "context" + "net/http" + "time" + + "github.com/DarRo9/Tenders/internal/config" +) + +type Server struct { + httpServer *http.Server +} + +func (s *Server) Start(cfg config.HttpServer, httpHandler http.Handler) error { + s.httpServer = &http.Server{ + Addr: cfg.Address, + Handler: httpHandler, + MaxHeaderBytes: 1 << 20, + ReadTimeout: 5 * time.Second, + WriteTimeout: 5 * time.Second, + IdleTimeout: 10 * time.Second, + } + return s.httpServer.ListenAndServe() +} + +func (s *Server) Shutdown(ctx context.Context) error { + return s.httpServer.Shutdown(ctx) +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000000000000000000000000000000000000..864bc1ce858cd0bbe9c639fa777b535b78bf419b --- /dev/null +++ b/settings.gradle @@ -0,0 +1,6 @@ +pluginManagement { + repositories { + gradlePluginPortal() + } +} +rootProject.name = 'gradle-example' diff --git a/src/main/java/io/codefresh/gradleexample/GradleExampleApplication.java b/src/main/java/io/codefresh/gradleexample/GradleExampleApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..e41614137ae15478bc2c81fcc7fc8c93ccc0e9e5 --- /dev/null +++ b/src/main/java/io/codefresh/gradleexample/GradleExampleApplication.java @@ -0,0 +1,13 @@ +package io.codefresh.gradleexample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class GradleExampleApplication { + + public static void main(String[] args) { + SpringApplication.run(GradleExampleApplication.class, args); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000000000000000000000000000000000000..b5d2a9e434a6c67b8b1daf5550ee4ea0dab31f83 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ +server.servlet.context-path=/ diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 0000000000000000000000000000000000000000..57b69df28429111acc46e6304efcd0e1cd61161e --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Gradle Docker example</title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> +</head> +<body> + <p>Hello. I am a Java project, compiled by Gradle running inside Docker</a></p> + + <p>See a quick <a href="actuator/health">status report</a></p> +</body> +</html> \ No newline at end of file diff --git a/src/test/java/io/codefresh/gradleexample/GradleExampleApplicationTest.java b/src/test/java/io/codefresh/gradleexample/GradleExampleApplicationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..5a881f99bd047e57db4f1fb6f07f14a82d81b4a7 --- /dev/null +++ b/src/test/java/io/codefresh/gradleexample/GradleExampleApplicationTest.java @@ -0,0 +1,19 @@ +package io.codefresh.gradleexample; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class GradleExampleApplicationTest { + + @Test + public void contextLoads() { + assertEquals("Expected correct message","Hello World","Hello "+"World"); + } + +} diff --git a/test_docker.sh b/test_docker.sh new file mode 100644 index 0000000000000000000000000000000000000000..4f7f8d3adc707a8bb5de13d5ad68789cf97afae4 --- /dev/null +++ b/test_docker.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +SERVER_ADDRESS="0.0.0.0:8080" +POSTGRES_CONN="host=your_host user=your_user password=your_password dbname=your_db sslmode=disable" +POSTGRES_JDBC_URL="jdbc:postgresql://your_host:your_port/your_db" +POSTGRES_USERNAME="your_user" +POSTGRES_PASSWORD="your_password" +POSTGRES_HOST="" +POSTGRES_PORT="5432" +POSTGRES_DATABASE="" + +docker run \ + -e SERVER_ADDRESS="$SERVER_ADDRESS" \ + -e POSTGRES_CONN="$POSTGRES_CONN" \ + -e POSTGRES_JDBC_URL="$POSTGRES_JDBC_URL" \ + -e POSTGRES_USERNAME="$POSTGRES_USERNAME" \ + -e POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \ + -e POSTGRES_HOST="$POSTGRES_HOST" \ + -e POSTGRES_PORT="$POSTGRES_PORT" \ + -e POSTGRES_DATABASE="$POSTGRES_DATABASE" \ + avito \ No newline at end of file diff --git "a/\320\267\320\260\320\264\320\260\320\275\320\270\320\265/README.md" "b/\320\267\320\260\320\264\320\260\320\275\320\270\320\265/README.md" new file mode 100644 index 0000000000000000000000000000000000000000..231f71076f0cc1e893349e0920d76a6bdece9c9e --- /dev/null +++ "b/\320\267\320\260\320\264\320\260\320\275\320\270\320\265/README.md" @@ -0,0 +1,473 @@ +# Ð¡ÐµÑ€Ð²Ð¸Ñ Ð¿Ñ€Ð¾Ð²ÐµÐ´ÐµÐ½Ð¸Ñ Ñ‚ÐµÐ½Ð´ÐµÑ€Ð¾Ð² +## Проблема: + +Ðвито — Ð±Ð¾Ð»ÑŒÑˆÐ°Ñ ÐºÐ¾Ð¼Ð¿Ð°Ð½Ð¸Ñ, в рамках которой пользователи не только продают/покупают товары и уÑлуги, но и предоÑтавлÑÑŽÑ‚ помощь крупному бизнеÑу и предприÑтиÑм. + +ПоÑтому ребÑта из Ðвито решили Ñделать ÑервиÑ, который позволит бизнеÑу Ñоздать тендер на оказание каких-либо уÑлуг. Рпользователи/другие бизнеÑÑ‹ будут предлагать Ñвои выгодные уÑÐ»Ð¾Ð²Ð¸Ñ Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð´Ð°Ð½Ð½Ð¾Ð³Ð¾ тендера. + +Помогите ребÑтам из Ðвито реализовать новое HTTP API! + +## Про приложение + +### Стек +- Любой Ñзык Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¼Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ +- Любые библиотеки +- Postgres + +### ÐаÑтройка Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð¸Ð·Ð²Ð¾Ð´Ð¸Ñ‚ÑÑ Ñ‡ÐµÑ€ÐµÐ· переменные Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ + +- `SERVER_ADDRESS` — Ð°Ð´Ñ€ÐµÑ Ð¸ порт, который будет Ñлушать HTTP Ñервер при запуÑке. Пример: 0.0.0.0:8080. +- `POSTGRES_CONN` — URL-Ñтрока Ð´Ð»Ñ Ð¿Ð¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ Ðº PostgreSQL в формате postgres://{username}:{password}@{host}:{5432}/{dbname}. +- `POSTGRES_JDBC_URL` — JDBC-Ñтрока Ð´Ð»Ñ Ð¿Ð¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ Ðº PostgreSQL в формате jdbc:postgresql://{host}:{port}/{dbname}. +- `POSTGRES_USERNAME` — Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð´Ð»Ñ Ð¿Ð¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ Ðº PostgreSQL. +- `POSTGRES_PASSWORD` — пароль Ð´Ð»Ñ Ð¿Ð¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ Ðº PostgreSQL. +- `POSTGRES_HOST` — хоÑÑ‚ Ð´Ð»Ñ Ð¿Ð¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ Ðº PostgreSQL (например, localhost). +- `POSTGRES_PORT` — порт Ð´Ð»Ñ Ð¿Ð¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ Ðº PostgreSQL (например, 5432). +- `POSTGRES_DATABASE` — Ð¸Ð¼Ñ Ð±Ð°Ð·Ñ‹ данных PostgreSQL, которую будет иÑпользовать приложение. + +## ОÑновные Ñ‚Ñ€ÐµÐ±Ð¾Ð²Ð°Ð½Ð¸Ñ +### СущноÑти +#### Пользователь и Ð¾Ñ€Ð³Ð°Ð½Ð¸Ð·Ð°Ñ†Ð¸Ñ + +СущноÑти Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð¸ организации уже Ñозданы и предÑтавлены в базе данных Ñледующим образом: + +Пользователь (User): + +```sql +CREATE TABLE employee ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + username VARCHAR(50) UNIQUE NOT NULL, + first_name VARCHAR(50), + last_name VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +ÐžÑ€Ð³Ð°Ð½Ð¸Ð·Ð°Ñ†Ð¸Ñ (Organization): +```sql +CREATE TYPE organization_type AS ENUM ( + 'IE', + 'LLC', + 'JSC' +); + +CREATE TABLE organization ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(100) NOT NULL, + description TEXT, + type organization_type, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE organization_responsible ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + organization_id UUID REFERENCES organization(id) ON DELETE CASCADE, + user_id UUID REFERENCES employee(id) ON DELETE CASCADE +); +``` + +### API +Ð’Ñе Ñндпоинты начинаютÑÑ Ñ Ð¿Ñ€ÐµÑ„Ð¸ÐºÑа /api. + +Обратите внимание, что уÑпешное выполнение запроÑа GET /api/ping обÑзательно Ð´Ð»Ñ Ð½Ð°Ñ‡Ð°Ð»Ð° теÑÑ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ. + +Ð’Ñе запроÑÑ‹ и ответы должны ÑоответÑтвовать Ñтруктуре и требованиÑм Ñпецификации Open API, Ð²ÐºÐ»ÑŽÑ‡Ð°Ñ ÑтатуÑ-коды, Ð¾Ð³Ñ€Ð°Ð½Ð¸Ñ‡ÐµÐ½Ð¸Ñ Ð¿Ð¾ длине и допуÑтимые Ñимволы в Ñтроках. + +ЕÑли Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ðµ ÑоответÑтвует требованиÑм, возвращайте ÑтатуÑ-код 400. ЕÑли же имеетÑÑ Ð±Ð¾Ð»ÐµÐµ Ñпецифичный код ответа, иÑпользуйте его. + +ЕÑли в запроÑе еÑть Ñ…Ð¾Ñ‚Ñ Ð±Ñ‹ один некорректный параметр, веÑÑŒ Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð´Ð¾Ð»Ð¶ÐµÐ½ быть отклонён. + +### БизнеÑ-логика +#### Тендер + +Тендеры могут Ñоздавать только пользователи от имени Ñвоей организации. + +ДоÑтупные дейÑÑ‚Ð²Ð¸Ñ Ñ Ñ‚ÐµÐ½Ð´ÐµÑ€Ð¾Ð¼: + +- **Создание**: + + - Тендер будет Ñоздан. + + - ДоÑтупен только ответÑтвенным за организацию. + + - СтатуÑ: `CREATED`. + +- **ПубликациÑ**: + + - Тендер ÑтановитÑÑ Ð´Ð¾Ñтупен вÑем пользователÑм. + + - СтатуÑ: `PUBLISHED`. + +- **Закрытие**: + + - Тендер больше не доÑтупен пользователÑм, кроме ответÑтвенных за организацию. + + - СтатуÑ: `CLOSED`. + +- **Редактирование**: + + - ИзменÑÑŽÑ‚ÑÑ Ñ…Ð°Ñ€Ð°ÐºÑ‚ÐµÑ€Ð¸Ñтики тендера. + + - УвеличиваетÑÑ Ð²ÐµÑ€ÑиÑ. + +#### Предложение + +ÐŸÑ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð¼Ð¾Ð³ÑƒÑ‚ Ñоздавать пользователи от имени Ñвоей организации. + +Предложение ÑвÑзано только Ñ Ð¾Ð´Ð½Ð¸Ð¼ тендером. Один пользователь может быть ответÑтвенным в одной организации. + +ДоÑтупные дейÑÑ‚Ð²Ð¸Ñ Ñ Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñми: + +- **Создание**: + + - Предложение будет Ñоздано. + + - ДоÑтупно только автору и ответÑтвенным за организацию. + + - СтатуÑ: `CREATED`. + +- **ПубликациÑ**: + + - Предложение ÑтановитÑÑ Ð´Ð¾Ñтупно ответÑтвенным за организацию и автору. + + - СтатуÑ: `PUBLISHED`. + +- **Отмена**: + + - Виден только автору и ответÑтвенным за организацию. + + - СтатуÑ: `CANCELED`. + +- **Редактирование**: + + - ИзменÑÑŽÑ‚ÑÑ Ñ…Ð°Ñ€Ð°ÐºÑ‚ÐµÑ€Ð¸Ñтики предложениÑ. + + - УвеличиваетÑÑ Ð²ÐµÑ€ÑиÑ. + +- **СоглаÑование/отклонение**: + + - ДоÑтупно только ответÑтвенным за организацию, ÑвÑзанной Ñ Ñ‚ÐµÐ½Ð´ÐµÑ€Ð¾Ð¼. + + - Решение может быть принÑто любым ответÑтвенным. + + - При ÑоглаÑовании одного предложениÑ, тендер автоматичеÑки закрываетÑÑ. + +## Дополнительные Ñ‚Ñ€ÐµÐ±Ð¾Ð²Ð°Ð½Ð¸Ñ + +1. РаÑширенный процеÑÑ ÑоглаÑованиÑ: + + - ЕÑли еÑть Ñ…Ð¾Ñ‚Ñ Ð±Ñ‹ одно решение reject, предложение отклонÑетÑÑ. +  + - Ð”Ð»Ñ ÑоглаÑÐ¾Ð²Ð°Ð½Ð¸Ñ Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð½ÑƒÐ¶Ð½Ð¾ получить Ñ€ÐµÑˆÐµÐ½Ð¸Ñ Ð±Ð¾Ð»ÑŒÑˆÐµ или равно кворуму. +  + - Кворум = min(3, количеÑтво ответÑтвенных за организацию). + +3. ПроÑмотр отзывов на прошлые предложениÑ: + + - ОтветÑтвенный за организацию может проÑмотреть отзывы на Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð°Ð²Ñ‚Ð¾Ñ€Ð°, который Ñоздал предложение Ð´Ð»Ñ ÐµÐ³Ð¾ тендера. + +5. ОÑтавление отзывов на предложение: + + - ОтветÑтвенный за организацию может оÑтавить отзыв на предложение. + +7. Добавить возможноÑть отката по верÑии (Тендер и Предложение): + + - ПоÑле отката, ÑчитаетÑÑ Ð½Ð¾Ð²Ð¾Ð¹ правкой Ñ ÑƒÐ²ÐµÐ»Ð¸Ñ‡ÐµÐ½Ð¸ÐµÐ¼ верÑии. + +9. ОпиÑание конфигурации линтера. + +## ТеÑтирование + +### 1. Проверка доÑтупноÑти Ñервера +- **Ðндпоинт:** GET /ping +- **Цель:** УбедитьÑÑ, что Ñервер готов обрабатывать запроÑÑ‹. +- **Ожидаемый результат:** Ð¡Ñ‚Ð°Ñ‚ÑƒÑ ÐºÐ¾Ð´ 200 и текÑÑ‚ "ok". + +```yaml +GET /api/ping + +Response: + + 200 OK + + Body: ok +``` + +### 2. ТеÑтирование функциональноÑти тендеров +#### Получение ÑпиÑка тендеров +- **Ðндпоинт:** GET /tenders +- **ОпиÑание:** Возвращает ÑпиÑок тендеров Ñ Ð²Ð¾Ð·Ð¼Ð¾Ð¶Ð½Ð¾Ñтью фильтрации по типу уÑлуг. +- **Ожидаемый результат:** Ð¡Ñ‚Ð°Ñ‚ÑƒÑ ÐºÐ¾Ð´ 200 и корректный ÑпиÑок тендеров. + +```yaml +GET /api/tenders + +Response: + + 200 OK + + Body: [ {...}, {...}, ... ] +``` + +#### Создание нового тендера +- **Ðндпоинт:** POST /tenders/new +- **ОпиÑание:** Создает новый тендер Ñ Ð·Ð°Ð´Ð°Ð½Ð½Ñ‹Ð¼Ð¸ параметрами. +- **Ожидаемый результат:** Ð¡Ñ‚Ð°Ñ‚ÑƒÑ ÐºÐ¾Ð´ 200 и данные Ñозданного тендера. + +```yaml +POST /api/tenders/new + +Request Body: + + { + +  "name": "Тендер 1", + +  "description": "ОпиÑание тендера", + +  "serviceType": "Construction", + +  "status": "Open", + +  "organizationId": 1, + +  "creatorUsername": "user1" + + } + +Response: + + 200 OK + + Body: + + { + "id": 1, + "name": "Тендер 1", + "description": "ОпиÑание тендера", + ... + } +``` + +#### Получение тендеров Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ +- **Ðндпоинт:** GET /tenders/my +- **ОпиÑание:** Возвращает ÑпиÑок тендеров текущего пользователÑ. +- **Ожидаемый результат:** Ð¡Ñ‚Ð°Ñ‚ÑƒÑ ÐºÐ¾Ð´ 200 и ÑпиÑок тендеров пользователÑ. + +```yaml +GET /api/tenders/my?username=user1 + +Response: + + 200 OK + + Body: [ {...}, {...}, ... ] +``` + +#### Редактирование тендера +- **Ðндпоинт:** PATCH /tenders/{tenderId}/edit +- **ОпиÑание:** Изменение параметров ÑущеÑтвующего тендера. +- **Ожидаемый результат:** Ð¡Ñ‚Ð°Ñ‚ÑƒÑ ÐºÐ¾Ð´ 200 и обновленные данные тендера. + +```yaml +PATCH /api/tenders/1/edit + +Request Body: + + { + +  "name": "Обновленный Тендер 1", + +  "description": "Обновленное опиÑание" + + } + +Response: + + 200 OK + + Body: + { + "id": 1, + "name": "Обновленный Тендер 1", + "description": "Обновленное опиÑание", + ... + } +``` + +#### Откат верÑии тендера +- **Ðндпоинт:** PUT /tenders/{tenderId}/rollback/{version} +- **ОпиÑание:** Откатить параметры тендера к указанной верÑии. +- **Ожидаемый результат:** Ð¡Ñ‚Ð°Ñ‚ÑƒÑ ÐºÐ¾Ð´ 200 и данные тендера на указанной верÑии. + +```yaml +PUT /api/tenders/1/rollback/2 + +Response: + + 200 OK + + Body: + { + "id": 1, + "name": "Тендер 1 верÑÐ¸Ñ 2", + ... + } +``` + +### 3. ТеÑтирование функциональноÑти предложений +#### Создание нового Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ +- **Ðндпоинт:** POST /bids/new +- **ОпиÑание:** Создает новое предложение Ð´Ð»Ñ ÑущеÑтвующего тендера. +- **Ожидаемый результат:** Ð¡Ñ‚Ð°Ñ‚ÑƒÑ ÐºÐ¾Ð´ 200 и данные Ñозданного предложениÑ. + +```yaml +POST /api/bids/new + +Request Body: + + { + +  "name": "Предложение 1", + +  "description": "ОпиÑание предложениÑ", + +  "status": "Submitted", + +  "tenderId": 1, + +  "organizationId": 1, + +  "creatorUsername": "user1" + + } + +Response: + + 200 OK + + Body: + { + "id": 1, + "name": "Предложение 1", + "description": "ОпиÑание предложениÑ", + ... + } +``` + +#### Получение ÑпиÑка предложений Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ +- **Ðндпоинт:** GET /bids/my +- **ОпиÑание:** Возвращает ÑпиÑок предложений текущего пользователÑ. +- **Ожидаемый результат:** Ð¡Ñ‚Ð°Ñ‚ÑƒÑ ÐºÐ¾Ð´ 200 и ÑпиÑок предложений пользователÑ. + +```yaml +GET /api/bids/my?username=user1 + +Response: + + 200 OK + + Body: [ {...}, {...}, ... ] + ``` + +#### Получение ÑпиÑка предложений Ð´Ð»Ñ Ñ‚ÐµÐ½Ð´ÐµÑ€Ð° +- **Ðндпоинт:** GET /bids/{tenderId}/list +- **ОпиÑание:** Возвращает предложениÑ, ÑвÑзанные Ñ ÑƒÐºÐ°Ð·Ð°Ð½Ð½Ñ‹Ð¼ тендером. +- **Ожидаемый результат:** Ð¡Ñ‚Ð°Ñ‚ÑƒÑ ÐºÐ¾Ð´ 200 и ÑпиÑок предложений Ð´Ð»Ñ Ñ‚ÐµÐ½Ð´ÐµÑ€Ð°. + +```yaml +GET /api/bids/1/list + +Response: + + 200 OK + + Body: [ {...}, {...}, ... ] + ``` + +#### Редактирование Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ +- **Ðндпоинт:** PATCH /bids/{bidId}/edit +- **ОпиÑание:** Редактирование ÑущеÑтвующего предложениÑ. +- **Ожидаемый результат:** Ð¡Ñ‚Ð°Ñ‚ÑƒÑ ÐºÐ¾Ð´ 200 и обновленные данные предложениÑ. + +```yaml +PATCH /api/bids/1/edit + +Request Body: + + { + +  "name": "Обновленное Предложение 1", + +  "description": "Обновленное опиÑание" + + } + +Response: + + 200 OK + + Body: + { + "id": 1, + "name": "Обновленное Предложение 1", + "description": "Обновленное опиÑание", + ..., + } +``` + +#### Откат верÑии Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ +- **Ðндпоинт:** PUT /bids/{bidId}/rollback/{version} +- **ОпиÑание:** Откатить параметры Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ðº указанной верÑии. +- **Ожидаемый результат:** Ð¡Ñ‚Ð°Ñ‚ÑƒÑ ÐºÐ¾Ð´ 200 и данные Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð½Ð° указанной верÑии. + +```yaml +PUT /api/bids/1/rollback/2 + +Response: + + 200 OK + + Body: + { + "id": 1, + "name": "Предложение 1 верÑÐ¸Ñ 2", + ... + } +``` + +### 4. ТеÑтирование функциональноÑти отзывов +#### ПроÑмотр отзывов на прошлые Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ +- **Ðндпоинт:** GET /bids/{tenderId}/reviews +- **ОпиÑание:** ОтветÑтвенный за организацию может поÑмотреть прошлые отзывы на Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð°Ð²Ñ‚Ð¾Ñ€Ð°, который Ñоздал предложение Ð´Ð»Ñ ÐµÐ³Ð¾ тендера. +- **Ожидаемый результат:** Ð¡Ñ‚Ð°Ñ‚ÑƒÑ ÐºÐ¾Ð´ 200 и ÑпиÑок отзывов на Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ ÑƒÐºÐ°Ð·Ð°Ð½Ð½Ð¾Ð³Ð¾ автора. + +```yaml +GET /api/bids/1/reviews?authorUsername=user2&organizationId=1 + +Response: + + 200 OK + + Body: [ {...}, {...}, ... ] +``` + +### Оценивание + + +| Ðазвание группы | Ручки | Баллы | От каких групп завиÑит | +| ------------------ | -------------------------------------- | ----- | ---------------------- | +| 01/ping | - /ping | 1 | | +| 02/tenders/new | - /tenders/new | 2 | | +| 03/tenders/list | - /tenders<br>- /tenders/my | 5 | - 02/tenders/new | +| 04/tenders/status | - /tenders/status | 3 | - 02/tenders/new | +| 05/tenders/version | - /tenders/edit<br>- /tenders/rollback | 6 | - 02/tenders/new | +| 06/bids/new | - /bids/new | 2 | | +| 07/bids/decision | - /bids/submit_decision | 3/6 | - 06/bids/new | +| 08/bids/list | - /bids/list<br>- /bids/my | 5 | - 06/bids/new | +| 09/bids/status | - /bids/status | 3 | - 06/bids/new | +| 10/bids/version | - /bids/edit<br>- /bids/rollback | 6 | - 06/bids/new | +| 11/bids/feedback | - /bids/reviews<br>- /bids/feedback | 7 | - 06/bids/new | + diff --git "a/\320\267\320\260\320\264\320\260\320\275\320\270\320\265/openapi.yml" "b/\320\267\320\260\320\264\320\260\320\275\320\270\320\265/openapi.yml" new file mode 100644 index 0000000000000000000000000000000000000000..b8e901980f6a627fc5bd7c25b1e1de713e8c099c --- /dev/null +++ "b/\320\267\320\260\320\264\320\260\320\275\320\270\320\265/openapi.yml" @@ -0,0 +1,1132 @@ +openapi: "3.0.1" +info: + title: Tender Management API + version: "1.0" + description: | + API Ð´Ð»Ñ ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ñ‚ÐµÐ½Ð´ÐµÑ€Ð°Ð¼Ð¸ и предложениÑми. + + ОÑновные функции API включают управление тендерами (Ñоздание, изменение, получение ÑпиÑка) и управление предложениÑми (Ñоздание, изменение, получение ÑпиÑка). +servers: + - url: http://localhost:8080/api + description: Локальный Ñервер API + +paths: + /ping: + get: + summary: Проверка доÑтупноÑти Ñервера + description: | + Ðтот Ñндпоинт иÑпользуетÑÑ Ð´Ð»Ñ Ð¿Ñ€Ð¾Ð²ÐµÑ€ÐºÐ¸ готовноÑти Ñервера обрабатывать запроÑÑ‹. + + Чекер программа будет ждать первый уÑпешный ответ и затем начнет выполнение теÑтовых Ñценариев. + operationId: checkServer + responses: + "200": + description: | + Сервер готов обрабатывать запроÑÑ‹, еÑли отвечает "200 OK". + Тело ответа не важно, доÑтаточно вернуть "ok". + content: + text/plain: + schema: + type: string + example: ok + "500": + description: Сервер не готов обрабатывать запроÑÑ‹, еÑли ответ ÑтатуÑом 500 или любой другой, кроме 200. + + /tenders: + get: + summary: Получение ÑпиÑка тендеров + description: | + СпиÑок тендеров Ñ Ð²Ð¾Ð·Ð¼Ð¾Ð¶Ð½Ð¾Ñтью фильтрации по типу уÑлуг. + + ЕÑли фильтры не заданы, возвращаютÑÑ Ð²Ñе тендеры. + operationId: getTenders + parameters: + - $ref: "#/components/parameters/paginationLimit" + - $ref: "#/components/parameters/paginationOffset" + - name: service_type + description: | + Возвращенные тендеры должны ÑоответÑтвовать указанным видам уÑлуг. + + ЕÑли ÑпиÑок пуÑтой, фильтры не применÑÑŽÑ‚ÑÑ. + in: query + schema: + type: array + items: + $ref: "#/components/schemas/tenderServiceType" + example: + - Construction + - Delivery + responses: + "200": + description: СпиÑок тендеров, отÑортированных по алфавиту по названию. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/tender" + "400": + description: Ðеверный формат запроÑа или его параметры. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + + /tenders/new: + post: + summary: Создание нового тендера + description: Создание нового тендера Ñ Ð·Ð°Ð´Ð°Ð½Ð½Ñ‹Ð¼Ð¸ параметрами. + operationId: createTender + requestBody: + description: Данные нового тендера. + required: true + content: + application/json: + schema: + type: object + properties: + name: + $ref: "#/components/schemas/tenderName" + description: + $ref: "#/components/schemas/tenderDescription" + serviceType: + $ref: "#/components/schemas/tenderServiceType" + organizationId: + $ref: "#/components/schemas/organizationId" + creatorUsername: + $ref: "#/components/schemas/username" + required: + - name + - description + - serviceType + - organizationId + - creatorUsername + responses: + "200": + description: Тендер уÑпешно Ñоздан. Сервер приÑваивает уникальный идентификатор и Ð²Ñ€ÐµÐ¼Ñ ÑозданиÑ. + content: + application/json: + schema: + $ref: "#/components/schemas/tender" + "401": + description: Пользователь не ÑущеÑтвует или некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "403": + description: ÐедоÑтаточно прав Ð´Ð»Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð´ÐµÐ¹ÑтвиÑ. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + + /tenders/my: + get: + summary: Получить тендеры Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ + description: | + Получение ÑпиÑка тендеров текущего пользователÑ. + + Ð”Ð»Ñ ÑƒÐ´Ð¾Ð±Ñтва иÑÐ¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ð½Ð¸Ñ Ð²ÐºÐ»ÑŽÑ‡ÐµÐ½Ð° поддержка пагинации. + operationId: getUserTenders + parameters: + - $ref: "#/components/parameters/paginationLimit" + - $ref: "#/components/parameters/paginationOffset" + - name: username + in: query + schema: + $ref: "#/components/schemas/username" + responses: + "200": + description: СпиÑок тендеров пользователÑ, отÑортированный по алфавиту. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/tender" + "401": + description: Пользователь не ÑущеÑтвует или некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + + /tenders/{tenderId}/status: + get: + summary: Получение текущего ÑтатуÑа тендера + description: Получить ÑÑ‚Ð°Ñ‚ÑƒÑ Ñ‚ÐµÐ½Ð´ÐµÑ€Ð° по его уникальному идентификатору. + operationId: getTenderStatus + parameters: + - name: tenderId + in: path + required: true + schema: + $ref: "#/components/schemas/tenderId" + - name: username + in: query + schema: + $ref: "#/components/schemas/username" + responses: + "200": + description: Текущий ÑÑ‚Ð°Ñ‚ÑƒÑ Ñ‚ÐµÐ½Ð´ÐµÑ€Ð°. + content: + application/json: + schema: + $ref: "#/components/schemas/tenderStatus" + "401": + description: Пользователь не ÑущеÑтвует или некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "403": + description: ÐедоÑтаточно прав Ð´Ð»Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð´ÐµÐ¹ÑтвиÑ. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "404": + description: Тендер не найден. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + put: + summary: Изменение ÑтатуÑа тендера + description: Изменить ÑÑ‚Ð°Ñ‚ÑƒÑ Ñ‚ÐµÐ½Ð´ÐµÑ€Ð° по его идентификатору. + operationId: updateTenderStatus + parameters: + - name: tenderId + in: path + required: true + schema: + $ref: "#/components/schemas/tenderId" + - name: status + in: query + required: true + schema: + $ref: "#/components/schemas/tenderStatus" + - name: username + in: query + required: true + schema: + $ref: "#/components/schemas/username" + responses: + "200": + description: Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Ñ‚ÐµÐ½Ð´ÐµÑ€Ð° уÑпешно изменен. + content: + application/json: + schema: + $ref: "#/components/schemas/tender" + "400": + description: Ðеверный формат запроÑа или его параметры. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "401": + description: Пользователь не ÑущеÑтвует или некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "403": + description: ÐедоÑтаточно прав Ð´Ð»Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð´ÐµÐ¹ÑтвиÑ. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "404": + description: Тендер не найден. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + + /tenders/{tenderId}/edit: + patch: + summary: Редактирование тендера + description: Изменение параметров ÑущеÑтвующего тендера. + operationId: editTender + parameters: + - name: tenderId + in: path + required: true + schema: + $ref: "#/components/schemas/tenderId" + - name: username + in: query + required: true + schema: + $ref: "#/components/schemas/username" + requestBody: + description: | + ПеречиÑление параметров и их новых значений Ð´Ð»Ñ Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ñ‚ÐµÐ½Ð´ÐµÑ€Ð°. + + ЕÑли значение не передано, оно оÑтанетÑÑ Ð±ÐµÐ· изменений. + required: true + content: + application/json: + schema: + type: object + properties: + name: + $ref: "#/components/schemas/tenderName" + description: + $ref: "#/components/schemas/tenderDescription" + serviceType: + $ref: "#/components/schemas/tenderServiceType" + responses: + "200": + description: Тендер уÑпешно изменен и возвращает обновленную информацию. + content: + application/json: + schema: + $ref: "#/components/schemas/tender" + "400": + description: Данные неправильно Ñформированы или не ÑоответÑтвуют требованиÑм. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "401": + description: Пользователь не ÑущеÑтвует или некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "403": + description: ÐедоÑтаточно прав Ð´Ð»Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð´ÐµÐ¹ÑтвиÑ. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "404": + description: Тендер не найден. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + + /tenders/{tenderId}/rollback/{version}: + put: + summary: Откат верÑии тендера + description: Откатить параметры тендера к указанной верÑии. Ðто ÑчитаетÑÑ Ð½Ð¾Ð²Ð¾Ð¹ правкой, поÑтому верÑÐ¸Ñ Ð¸Ð½ÐºÑ€ÐµÐ¼ÐµÐ½Ñ‚Ð¸Ñ€ÑƒÐµÑ‚ÑÑ. + operationId: rollbackTender + parameters: + - name: tenderId + in: path + required: true + schema: + $ref: "#/components/schemas/tenderId" + - name: version + in: path + required: true + schema: + type: integer + format: int32 + minimum: 1 + description: Ðомер верÑии, к которой нужно откатить тендер. + - name: username + in: query + required: true + schema: + $ref: "#/components/schemas/username" + responses: + "200": + description: Тендер уÑпешно откатан и верÑÐ¸Ñ Ð¸Ð½ÐºÑ€ÐµÐ¼ÐµÐ½Ñ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð°. + content: + application/json: + schema: + $ref: "#/components/schemas/tender" + "400": + description: Ðеверный формат запроÑа или его параметры. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "401": + description: Пользователь не ÑущеÑтвует или некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "403": + description: ÐедоÑтаточно прав Ð´Ð»Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð´ÐµÐ¹ÑтвиÑ. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "404": + description: Тендер или верÑÐ¸Ñ Ð½Ðµ найдены. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + + /bids/new: + post: + summary: Создание нового Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ + description: Создание Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ ÑущеÑтвующего тендера. + operationId: createBid + requestBody: + description: Данные нового предложениÑ. + required: true + content: + application/json: + schema: + type: object + properties: + name: + $ref: "#/components/schemas/bidName" + description: + $ref: "#/components/schemas/bidDescription" + tenderId: + $ref: "#/components/schemas/tenderId" + authorType: + $ref: "#/components/schemas/bidAuthorType" + authorId: + $ref: "#/components/schemas/bidAuthorId" + required: + - name + - description + - tenderId + - authorType + - authorId + responses: + "200": + description: Предложение уÑпешно Ñоздано. Сервер приÑваивает уникальный идентификатор и Ð²Ñ€ÐµÐ¼Ñ ÑозданиÑ. + content: + application/json: + schema: + $ref: "#/components/schemas/bid" + "401": + description: Пользователь не ÑущеÑтвует или некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "403": + description: ÐедоÑтаточно прав Ð´Ð»Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð´ÐµÐ¹ÑтвиÑ. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "404": + description: Тендер не найден. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + + /bids/my: + get: + summary: Получение ÑпиÑка ваших предложений + description: | + Получение ÑпиÑка предложений текущего пользователÑ. + + Ð”Ð»Ñ ÑƒÐ´Ð¾Ð±Ñтва иÑÐ¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ð½Ð¸Ñ Ð²ÐºÐ»ÑŽÑ‡ÐµÐ½Ð° поддержка пагинации. + operationId: getUserBids + parameters: + - $ref: "#/components/parameters/paginationLimit" + - $ref: "#/components/parameters/paginationOffset" + - name: username + in: query + schema: + $ref: "#/components/schemas/username" + responses: + "200": + description: СпиÑок предложений пользователÑ, отÑортированный по алфавиту. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/bid" + "401": + description: Пользователь не ÑущеÑтвует или некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + + /bids/{tenderId}/list: + get: + summary: Получение ÑпиÑка предложений Ð´Ð»Ñ Ñ‚ÐµÐ½Ð´ÐµÑ€Ð° + description: Получение предложений, ÑвÑзанных Ñ ÑƒÐºÐ°Ð·Ð°Ð½Ð½Ñ‹Ð¼ тендером. + operationId: getBidsForTender + parameters: + - name: tenderId + in: path + required: true + schema: + $ref: "#/components/schemas/tenderId" + - name: username + in: query + required: true + schema: + $ref: "#/components/schemas/username" + - $ref: "#/components/parameters/paginationLimit" + - $ref: "#/components/parameters/paginationOffset" + responses: + "200": + description: СпиÑок предложений, отÑортированный по алфавиту. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/bid" + "400": + description: Ðеверный формат запроÑа или его параметры. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "401": + description: Пользователь не ÑущеÑтвует или некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "403": + description: ÐедоÑтаточно прав Ð´Ð»Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð´ÐµÐ¹ÑтвиÑ. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "404": + description: Тендер или предложение не найдено. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + + /bids/{bidId}/status: + get: + summary: Получение текущего ÑтатуÑа Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ + description: Получить ÑÑ‚Ð°Ñ‚ÑƒÑ Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð¿Ð¾ его уникальному идентификатору. + operationId: getBidStatus + parameters: + - name: bidId + in: path + required: true + schema: + $ref: "#/components/schemas/bidId" + - name: username + in: query + required: true + schema: + $ref: "#/components/schemas/username" + responses: + "200": + description: Текущий ÑÑ‚Ð°Ñ‚ÑƒÑ Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ. + content: + application/json: + schema: + $ref: "#/components/schemas/bidStatus" + "401": + description: Пользователь не ÑущеÑтвует или некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "403": + description: ÐедоÑтаточно прав Ð´Ð»Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð´ÐµÐ¹ÑтвиÑ. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "404": + description: Предложение не найдено. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + put: + summary: Изменение ÑтатуÑа Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ + description: Изменить ÑÑ‚Ð°Ñ‚ÑƒÑ Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð¿Ð¾ его уникальному идентификатору. + operationId: updateBidStatus + parameters: + - name: bidId + in: path + required: true + schema: + $ref: "#/components/schemas/bidId" + - name: status + in: query + required: true + schema: + $ref: "#/components/schemas/bidStatus" + - name: username + in: query + required: true + schema: + $ref: "#/components/schemas/username" + responses: + "200": + description: Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ ÑƒÑпешно изменен. + content: + application/json: + schema: + $ref: "#/components/schemas/bid" + "400": + description: Ðеверный формат запроÑа или его параметры. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "401": + description: Пользователь не ÑущеÑтвует или некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "403": + description: ÐедоÑтаточно прав Ð´Ð»Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð´ÐµÐ¹ÑтвиÑ. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "404": + description: Предложение не найдено. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + + /bids/{bidId}/edit: + patch: + summary: Редактирование параметров Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ + description: Редактирование ÑущеÑтвующего предложениÑ. + operationId: editBid + parameters: + - name: bidId + in: path + required: true + schema: + $ref: "#/components/schemas/bidId" + - name: username + in: query + required: true + schema: + $ref: "#/components/schemas/username" + requestBody: + description: | + ПеречиÑление параметров и их новых значений Ð´Ð»Ñ Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ. + + ЕÑли значение не передано, оно оÑтанетÑÑ Ð±ÐµÐ· изменений. + required: true + content: + application/json: + schema: + type: object + properties: + name: + $ref: "#/components/schemas/bidName" + description: + $ref: "#/components/schemas/bidDescription" + responses: + "200": + description: Предложение уÑпешно изменено и возвращает обновленную информацию. + content: + application/json: + schema: + $ref: "#/components/schemas/bid" + "400": + description: Данные неправильно Ñформированы или не ÑоответÑтвуют требованиÑм. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "401": + description: Пользователь не ÑущеÑтвует или некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "403": + description: ÐедоÑтаточно прав Ð´Ð»Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð´ÐµÐ¹ÑтвиÑ. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "404": + description: Предложение не найдено. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + + /bids/{bidId}/submit_decision: + put: + summary: Отправка Ñ€ÐµÑˆÐµÐ½Ð¸Ñ Ð¿Ð¾ предложению + description: Отправить решение (одобрить или отклонить) по предложению. + operationId: submitBidDecision + parameters: + - name: bidId + in: path + required: true + schema: + $ref: "#/components/schemas/bidId" + - name: decision + in: query + required: true + schema: + $ref: "#/components/schemas/bidDecision" + - name: username + in: query + required: true + schema: + $ref: "#/components/schemas/username" + responses: + "200": + description: Решение по предложению уÑпешно отправлено. + content: + application/json: + schema: + $ref: "#/components/schemas/bid" + "400": + description: Решение не может быть отправлено. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "401": + description: Пользователь не ÑущеÑтвует или некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "403": + description: ÐедоÑтаточно прав Ð´Ð»Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð´ÐµÐ¹ÑтвиÑ. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "404": + description: Предложение не найдено. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + + /bids/{bidId}/feedback: + put: + summary: Отправка отзыва по предложению + description: Отправить отзыв по предложению. + operationId: submitBidFeedback + parameters: + - name: bidId + in: path + required: true + schema: + $ref: "#/components/schemas/bidId" + - name: bidFeedback + in: query + required: true + schema: + $ref: "#/components/schemas/bidFeedback" + - name: username + in: query + required: true + schema: + $ref: "#/components/schemas/username" + responses: + "200": + description: Отзыв по предложению уÑпешно отправлен. + content: + application/json: + schema: + $ref: "#/components/schemas/bid" + "400": + description: Отзыв не может быть отправлен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "401": + description: Пользователь не ÑущеÑтвует или некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "403": + description: ÐедоÑтаточно прав Ð´Ð»Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð´ÐµÐ¹ÑтвиÑ. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "404": + description: Предложение не найдено. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + + /bids/{bidId}/rollback/{version}: + put: + summary: Откат верÑии Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ + description: Откатить параметры Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ðº указанной верÑии. Ðто ÑчитаетÑÑ Ð½Ð¾Ð²Ð¾Ð¹ правкой, поÑтому верÑÐ¸Ñ Ð¸Ð½ÐºÑ€ÐµÐ¼ÐµÐ½Ñ‚Ð¸Ñ€ÑƒÐµÑ‚ÑÑ. + operationId: rollbackBid + parameters: + - name: bidId + in: path + required: true + schema: + $ref: "#/components/schemas/bidId" + - name: version + in: path + required: true + schema: + type: integer + format: int32 + minimum: 1 + description: Ðомер верÑии, к которой нужно откатить предложение. + - name: username + in: query + required: true + schema: + $ref: "#/components/schemas/username" + responses: + "200": + description: Предложение уÑпешно откатано и верÑÐ¸Ñ Ð¸Ð½ÐºÑ€ÐµÐ¼ÐµÐ½Ñ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð°. + content: + application/json: + schema: + $ref: "#/components/schemas/bid" + "400": + description: Ðеверный формат запроÑа или его параметры. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "401": + description: Пользователь не ÑущеÑтвует или некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "403": + description: ÐедоÑтаточно прав Ð´Ð»Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð´ÐµÐ¹ÑтвиÑ. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "404": + description: Предложение или верÑÐ¸Ñ Ð½Ðµ найдены. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + + /bids/{tenderId}/reviews: + get: + summary: ПроÑмотр отзывов на прошлые Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ + description: ОтветÑтвенный за организацию может поÑмотреть прошлые отзывы на Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð°Ð²Ñ‚Ð¾Ñ€Ð°, который Ñоздал предложение Ð´Ð»Ñ ÐµÐ³Ð¾ тендера. + operationId: getBidReviews + parameters: + - name: tenderId + in: path + required: true + schema: + $ref: "#/components/schemas/tenderId" + - name: authorUsername + in: query + required: true + schema: + $ref: "#/components/schemas/username" + description: Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð°Ð²Ñ‚Ð¾Ñ€Ð° предложений, отзывы на которые нужно проÑмотреть. + - name: requesterUsername + in: query + required: true + schema: + $ref: "#/components/schemas/username" + description: Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ, который запрашивает отзывы. + - $ref: "#/components/parameters/paginationLimit" + - $ref: "#/components/parameters/paginationOffset" + responses: + "200": + description: СпиÑок отзывов на Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ ÑƒÐºÐ°Ð·Ð°Ð½Ð½Ð¾Ð³Ð¾ автора. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/bidReview" + "400": + description: Ðеверный формат запроÑа или его параметры. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "401": + description: Пользователь не ÑущеÑтвует или некорректен. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "403": + description: ÐедоÑтаточно прав Ð´Ð»Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð´ÐµÐ¹ÑтвиÑ. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + "404": + description: Тендер или отзывы не найдены. + content: + application/json: + schema: + $ref: "#/components/schemas/errorResponse" + +components: + schemas: + username: + type: string + description: Уникальный slug пользователÑ. + example: test_user + tenderStatus: + type: string + description: Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Ñ‚ÐµÐ½Ð´ÐµÑ€ + enum: + - Created + - Published + - Closed + tenderServiceType: + type: string + description: Вид уÑлуги, к которой отноÑитьÑÑ Ñ‚ÐµÐ½Ð´ÐµÑ€ + enum: + - Construction + - Delivery + - Manufacture + tenderId: + type: string + description: Уникальный идентификатор тендера, приÑвоенный Ñервером. + example: 550e8400-e29b-41d4-a716-446655440000 + maxLength: 100 + tenderName: + type: string + description: Полное название тендера + maxLength: 100 + tenderDescription: + type: string + description: ОпиÑание тендера + maxLength: 500 + tenderVersion: + type: integer + description: Ðомер верÑии поÑел правок + format: int32 + minimum: 1 + default: 1 + organizationId: + type: string + description: Уникальный идентификатор организации, приÑвоенный Ñервером. + example: 550e8400-e29b-41d4-a716-446655440000 + maxLength: 100 + tender: + type: object + description: Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ тендере + properties: + id: + $ref: "#/components/schemas/tenderId" + name: + $ref: "#/components/schemas/tenderName" + description: + $ref: "#/components/schemas/tenderDescription" + serviceType: + $ref: "#/components/schemas/tenderServiceType" + status: + $ref: "#/components/schemas/tenderStatus" + organizationId: + $ref: "#/components/schemas/organizationId" + version: + $ref: "#/components/schemas/tenderVersion" + createdAt: + type: string + description: | + Ð¡ÐµÑ€Ð²ÐµÑ€Ð½Ð°Ñ Ð´Ð°Ñ‚Ð° и Ð²Ñ€ÐµÐ¼Ñ Ð² момент, когда пользователь отправил тендер на Ñоздание. + ПередаетÑÑ Ð² формате RFC3339. + example: 2006-01-02T15:04:05Z07:00 + + required: + - id + - name + - description + - serviceType + - status + - organizationId + - version + - createdAt + example: + id: 550e8400-e29b-41d4-a716-446655440000 + name: ДоÑтавка товары Казань - МоÑква + description: Ðужно доÑтавить оборудовоние Ð´Ð»Ñ Ð¾Ð»Ð¸Ð¼Ð¿Ð¸Ð°Ð´Ñ‹ по робототехники + status: Created + serviceType: Delivery + version: 1 + createdAt: 2006-01-02T15:04:05Z07:00 + bidStatus: + type: string + description: Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ + enum: + - Created + - Published + - Canceled + bidDecision: + type: string + description: Решение по предложению + enum: + - Approved + - Rejected + bidId: + type: string + description: Уникальный идентификатор предложениÑ, приÑвоенный Ñервером. + example: 550e8400-e29b-41d4-a716-446655440000 + maxLength: 100 + bidName: + type: string + description: Полное название Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ + maxLength: 100 + bidDescription: + type: string + description: ОпиÑание Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ + maxLength: 500 + bidFeedback: + type: string + description: Отзыв на предложение + maxLength: 1000 + bidAuthorType: + type: string + description: Тип автора + enum: + - Organization + - User + bidAuthorId: + type: string + description: Уникальный идентификатор автора предложениÑ, приÑвоенный Ñервером. + example: 550e8400-e29b-41d4-a716-446655440000 + maxLength: 100 + bidVersion: + type: integer + description: Ðомер верÑии поÑел правок + format: int32 + minimum: 1 + default: 1 + bidReviewId: + type: string + description: Уникальный идентификатор отзыва, приÑвоенный Ñервером. + example: 550e8400-e29b-41d4-a716-446655440000 + maxLength: 100 + bidReviewDescription: + type: string + description: ОпиÑание Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ + maxLength: 1000 + + bidReview: + type: object + description: Отзыв о предложении + properties: + id: + $ref: "#/components/schemas/bidReviewId" + description: + $ref: "#/components/schemas/bidReviewDescription" + createdAt: + type: string + description: | + Ð¡ÐµÑ€Ð²ÐµÑ€Ð½Ð°Ñ Ð´Ð°Ñ‚Ð° и Ð²Ñ€ÐµÐ¼Ñ Ð² момент, когда пользователь отправил отзыв на предложение. + ПередаетÑÑ Ð² формате RFC3339. + example: 2006-01-02T15:04:05Z07:00 + + required: + - id + - description + - createdAt + example: + id: 550e8400-e29b-41d4-a716-446655440000 + description: All gooood!!!! + createdAt: 2006-01-02T15:04:05Z07:00 + bid: + type: object + description: Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ предложении + properties: + id: + $ref: "#/components/schemas/bidId" + name: + $ref: "#/components/schemas/bidName" + description: + $ref: "#/components/schemas/bidDescription" + status: + $ref: "#/components/schemas/bidStatus" + tenderId: + $ref: "#/components/schemas/tenderId" + authorType: + $ref: "#/components/schemas/bidAuthorType" + authorId: + $ref: "#/components/schemas/bidAuthorId" + version: + $ref: "#/components/schemas/bidVersion" + createdAt: + type: string + description: | + Ð¡ÐµÑ€Ð²ÐµÑ€Ð½Ð°Ñ Ð´Ð°Ñ‚Ð° и Ð²Ñ€ÐµÐ¼Ñ Ð² момент, когда пользователь отправил предложение на Ñоздание. + ПередаетÑÑ Ð² формате RFC3339. + example: 2006-01-02T15:04:05Z07:00 + + required: + - id + - name + - description + - status + - tenderId + - createdAt + - authorType + - authorId + - version + example: + id: 550e8400-e29b-41d4-a716-446655440000 + name: ДоÑтавка товаров ÐлекÑей + status: Created + authorType: User + authorId: 61a485f0-e29b-41d4-a716-446655440000 + version: 1 + createdAt: 2006-01-02T15:04:05Z07:00 + + errorResponse: + type: object + description: ИÑпользуетÑÑ Ð´Ð»Ñ Ð²Ð¾Ð·Ð²Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ð¾ÑˆÐ¸Ð±ÐºÐ¸ пользователю + properties: + reason: + type: string + description: ОпиÑание ошибки в Ñвободной форме + minLength: 5 + required: + - reason + example: + reason: <объÑÑнение, почему Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð½Ðµ может быть обработан> + parameters: + paginationLimit: + in: query + name: limit + required: false + description: | + МакÑимальное чиÑло возвращаемых объектов. ИÑпользуетÑÑ Ð´Ð»Ñ Ð·Ð°Ð¿Ñ€Ð¾Ñов Ñ Ð¿Ð°Ð³Ð¸Ð½Ð°Ñ†Ð¸ÐµÐ¹. + + Сервер должен возвращать макÑимальное допуÑтимое чиÑло объектов. + schema: + type: integer + format: int32 + minimum: 0 + maximum: 50 + default: 5 + paginationOffset: + in: query + name: offset + required: false + description: | + Какое количеÑтво объектов должно быть пропущено Ñ Ð½Ð°Ñ‡Ð°Ð»Ð°. ИÑпользуетÑÑ Ð´Ð»Ñ Ð·Ð°Ð¿Ñ€Ð¾Ñов Ñ Ð¿Ð°Ð³Ð¸Ð½Ð°Ñ†Ð¸ÐµÐ¹. + schema: + type: integer + format: int32 + default: 0 + minimum: 0