├── .github └── ISSUE_TEMPLATE │ └── шаблон-баг-репорта.md ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── docker-compose-dev.sh ├── docker-compose-e2e.sh ├── docker-compose.mock.yml ├── docker-compose.yml ├── docker.properties ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── localenv.sh ├── niffler-auth ├── build.gradle └── src │ ├── main │ ├── java │ │ └── guru │ │ │ └── qa │ │ │ └── niffler │ │ │ ├── NifflerAuthApplication.java │ │ │ ├── config │ │ │ ├── NifflerAuthProducerConfiguration.java │ │ │ ├── NifflerAuthServiceConfig.java │ │ │ ├── SecurityConfig.java │ │ │ └── keys │ │ │ │ └── KeyManager.java │ │ │ ├── controller │ │ │ ├── ErrorAuthController.java │ │ │ ├── LoginController.java │ │ │ └── RegisterController.java │ │ │ ├── data │ │ │ ├── Authority.java │ │ │ ├── AuthorityEntity.java │ │ │ ├── UserEntity.java │ │ │ └── repository │ │ │ │ └── UserRepository.java │ │ │ ├── domain │ │ │ └── NifflerUserPrincipal.java │ │ │ ├── model │ │ │ ├── EqualPasswords.java │ │ │ ├── RegistrationModel.java │ │ │ └── UserJson.java │ │ │ └── service │ │ │ ├── EqualPasswordsValidator.java │ │ │ ├── NifflerUserDetailsService.java │ │ │ ├── PropertiesLogger.java │ │ │ ├── SpecificRequestDumperFilter.java │ │ │ ├── UserService.java │ │ │ └── cors │ │ │ ├── CookieCsrfFilter.java │ │ │ └── CorsCustomizer.java │ └── resources │ │ ├── application.yaml │ │ ├── db │ │ └── migration │ │ │ └── niffler-auth │ │ │ ├── V1__schema_init.sql │ │ │ └── V2__rename_tables.sql │ │ ├── logback-spring.xml │ │ ├── static │ │ ├── fonts │ │ │ ├── Inter-Regular.woff2 │ │ │ ├── YoungSerif-Regular.ttf │ │ │ ├── YoungSerif-Regular.woff │ │ │ └── YoungSerif-Regular.woff2 │ │ ├── images │ │ │ ├── background.jpg │ │ │ ├── coin.svg │ │ │ ├── eye-active.svg │ │ │ ├── eye.svg │ │ │ ├── favicon.ico │ │ │ ├── forest-back.jpeg │ │ │ ├── forest.svg │ │ │ └── niffler-logo.png │ │ └── styles │ │ │ └── styles.css │ │ └── templates │ │ ├── error.html │ │ ├── login.html │ │ └── register.html │ └── test │ └── java │ └── guru │ └── qa │ └── niffler │ └── service │ ├── EqualPasswordsValidatorTest.java │ ├── NifflerUserDetailsServiceTest.java │ └── cors │ ├── CookieCsrfFilterTest.java │ └── CorsCustomizerTest.java ├── niffler-currency ├── build.gradle └── src │ ├── main │ ├── java │ │ └── guru │ │ │ └── qa │ │ │ └── niffler │ │ │ ├── NifflerCurrencyApplication.java │ │ │ ├── data │ │ │ ├── CurrencyEntity.java │ │ │ ├── CurrencyValues.java │ │ │ └── repository │ │ │ │ └── CurrencyRepository.java │ │ │ └── service │ │ │ ├── GrpcCurrencyService.java │ │ │ ├── MigrationService.java │ │ │ └── PropertiesLogger.java │ └── resources │ │ ├── Dollar-cropped.svg │ │ ├── Euro-cropped.svg │ │ ├── Rub-cropped.svg │ │ ├── Tenge-cropped.svg │ │ ├── application.yaml │ │ └── db │ │ └── migration │ │ └── niffler-currency │ │ ├── V1__schema_init.sql │ │ └── V2__currency_symbols.sql │ └── test │ └── java │ └── guru │ └── qa │ └── niffler │ └── service │ └── GrpcCurrencyServiceTest.java ├── niffler-diagram.png ├── niffler-e-2-e-tests ├── .dockerignore ├── Dockerfile ├── build.gradle └── src │ └── test │ ├── graphql │ ├── currency.graphql │ └── stat.graphql │ ├── java │ └── guru │ │ └── qa │ │ └── niffler │ │ ├── api │ │ ├── AuthApi.java │ │ ├── GatewayApi.java │ │ ├── GatewayV2Api.java │ │ ├── GhApi.java │ │ ├── SpendApi.java │ │ ├── UserdataApi.java │ │ ├── UserdataSoapApi.java │ │ └── core │ │ │ ├── CodeInterceptor.java │ │ │ ├── RestClient.java │ │ │ ├── ThreadSafeCookieStore.java │ │ │ └── converter │ │ │ ├── SoapConverterFactory.java │ │ │ ├── SoapRequestConverter.java │ │ │ └── SoapResponseConverter.java │ │ ├── condition │ │ ├── Color.java │ │ └── StatConditions.java │ │ ├── config │ │ ├── Config.java │ │ ├── DockerConfig.java │ │ └── LocalConfig.java │ │ ├── data │ │ ├── dao │ │ │ ├── AuthAuthorityDao.java │ │ │ ├── AuthUserDao.java │ │ │ ├── CategoryDao.java │ │ │ ├── SpendDao.java │ │ │ ├── UserdataUserDao.java │ │ │ └── impl │ │ │ │ ├── AuthAuthorityDaoJdbc.java │ │ │ │ ├── AuthAuthorityDaoSpringJdbc.java │ │ │ │ ├── AuthUserDaoJdbc.java │ │ │ │ ├── AuthUserDaoSpringJdbc.java │ │ │ │ ├── CategoryDaoJdbc.java │ │ │ │ ├── CategoryDaoSpringJdbc.java │ │ │ │ ├── SpendDaoJdbc.java │ │ │ │ ├── SpendDaoSpringJdbc.java │ │ │ │ ├── UserdataUserDaoJdbc.java │ │ │ │ └── UserdataUserDaoSpringJdbc.java │ │ ├── entity │ │ │ ├── auth │ │ │ │ ├── AuthUserEntity.java │ │ │ │ ├── Authority.java │ │ │ │ └── AuthorityEntity.java │ │ │ ├── spend │ │ │ │ ├── CategoryEntity.java │ │ │ │ └── SpendEntity.java │ │ │ └── userdata │ │ │ │ ├── FriendShipId.java │ │ │ │ ├── FriendshipEntity.java │ │ │ │ ├── FriendshipStatus.java │ │ │ │ └── UserEntity.java │ │ ├── extractor │ │ │ ├── AuthUserEntityExtractor.java │ │ │ └── SpendEntityRowExtractor.java │ │ ├── jdbc │ │ │ ├── Connections.java │ │ │ ├── DataSources.java │ │ │ ├── JdbcConnectionHolder.java │ │ │ └── JdbcConnectionHolders.java │ │ ├── jpa │ │ │ ├── EntityManagers.java │ │ │ └── ThreadSafeEntityManager.java │ │ ├── logging │ │ │ ├── AllureAppender.java │ │ │ └── SqlAttachmentData.java │ │ ├── mapper │ │ │ ├── AuthUserEntityRowMapper.java │ │ │ ├── AuthorityEntityRowMapper.java │ │ │ ├── CategoryEntityRowMapper.java │ │ │ ├── SpendEntityRowMapper.java │ │ │ └── UserdataUserEntityRowMapper.java │ │ ├── repository │ │ │ ├── AuthUserRepository.java │ │ │ ├── SpendRepository.java │ │ │ ├── UserdataUserRepository.java │ │ │ └── impl │ │ │ │ ├── AuthUserRepositoryHibernate.java │ │ │ │ ├── AuthUserRepositoryJdbc.java │ │ │ │ ├── AuthUserRepositorySpringJdbc.java │ │ │ │ ├── SpendRepositoryHibernate.java │ │ │ │ ├── SpendRepositoryJdbc.java │ │ │ │ ├── SpendRepositorySpringJdbc.java │ │ │ │ ├── UserdataUserRepositoryHibernate.java │ │ │ │ ├── UserdataUserRepositoryJdbc.java │ │ │ │ └── UserdataUserRepositorySpringJdbc.java │ │ └── tpl │ │ │ ├── JdbcTransactionTemplate.java │ │ │ └── XaTransactionTemplate.java │ │ ├── jupiter │ │ ├── annotation │ │ │ ├── ApiLogin.java │ │ │ ├── Category.java │ │ │ ├── DisabledByIssue.java │ │ │ ├── ScreenShotTest.java │ │ │ ├── Spending.java │ │ │ ├── Token.java │ │ │ ├── User.java │ │ │ └── meta │ │ │ │ ├── GqlTest.java │ │ │ │ ├── GrpcTest.java │ │ │ │ ├── RestTest.java │ │ │ │ ├── SoapTest.java │ │ │ │ └── WebTest.java │ │ └── extension │ │ │ ├── AllureBackendLogsExtension.java │ │ │ ├── ApiLoginExtension.java │ │ │ ├── BrowserExtension.java │ │ │ ├── CategoryExtension.java │ │ │ ├── CookieJarExtension.java │ │ │ ├── DatabasesExtension.java │ │ │ ├── IssueExtension.java │ │ │ ├── ScreenShotTestExtension.java │ │ │ ├── SpendingExtension.java │ │ │ ├── SuiteExtension.java │ │ │ ├── TestMethodContextExtension.java │ │ │ ├── UserExtension.java │ │ │ ├── UsersClientExtension.java │ │ │ └── UsersQueueExtension.java │ │ ├── model │ │ ├── allure │ │ │ └── ScreenDif.java │ │ └── rest │ │ │ ├── CategoryJson.java │ │ │ ├── CurrencyJson.java │ │ │ ├── CurrencyValues.java │ │ │ ├── DataFilterValues.java │ │ │ ├── FriendJson.java │ │ │ ├── FriendshipStatus.java │ │ │ ├── SessionJson.java │ │ │ ├── SpendJson.java │ │ │ ├── StatisticByCategoryJson.java │ │ │ ├── StatisticJson.java │ │ │ ├── StatisticV2Json.java │ │ │ ├── SumByCategory.java │ │ │ ├── TestData.java │ │ │ ├── UserJson.java │ │ │ └── pageable │ │ │ └── RestResponsePage.java │ │ ├── page │ │ ├── BasePage.java │ │ ├── EditSpendingPage.java │ │ ├── FriendsPage.java │ │ ├── LoginPage.java │ │ ├── MainPage.java │ │ ├── PeoplePage.java │ │ ├── ProfilePage.java │ │ ├── RegisterPage.java │ │ └── component │ │ │ ├── BaseComponent.java │ │ │ ├── Calendar.java │ │ │ ├── Header.java │ │ │ ├── SearchField.java │ │ │ ├── SelectField.java │ │ │ ├── SpendingTable.java │ │ │ └── StatComponent.java │ │ ├── service │ │ ├── GhClient.java │ │ ├── SpendClient.java │ │ ├── UsersClient.java │ │ └── impl │ │ │ ├── AuthApiClient.java │ │ │ ├── GatewayApiClient.java │ │ │ ├── GatewayApiV2Client.java │ │ │ ├── GithubApiClient.java │ │ │ ├── SpendApiClient.java │ │ │ ├── SpendDbClient.java │ │ │ ├── UserdataSoapClient.java │ │ │ ├── UsersApiClient.java │ │ │ └── UsersDbClient.java │ │ ├── test │ │ ├── fake │ │ │ ├── JdbcTest.java │ │ │ └── OAuthTest.java │ │ ├── gql │ │ │ ├── BaseGraphQlTest.java │ │ │ ├── CurrenciesGraphQlTest.java │ │ │ └── StatGraphQlTest.java │ │ ├── grpc │ │ │ ├── BaseGrpcTest.java │ │ │ └── CurrencyGrpcTest.java │ │ ├── rest │ │ │ ├── FriendsTest.java │ │ │ └── FriendsV2Test.java │ │ ├── soap │ │ │ └── SoapUsersTest.java │ │ └── web │ │ │ ├── FriendsWebTest.java │ │ │ ├── LoginTest.java │ │ │ ├── ProfileTest.java │ │ │ ├── RegistrationTest.java │ │ │ └── SpendingWebTest.java │ │ └── utils │ │ ├── GrpcConsoleInterceptor.java │ │ ├── OAuthUtils.java │ │ ├── RandomDataUtils.java │ │ └── ScreenDiffResult.java │ ├── resources │ ├── META-INF │ │ ├── persistence.xml │ │ └── services │ │ │ └── org.junit.jupiter.api.extension.Extension │ ├── img │ │ ├── cat.jpeg │ │ ├── expected-stat.png │ │ └── renoire.jpeg │ ├── jndi.properties │ ├── junit-platform.properties │ ├── logback.xml │ ├── spy.properties │ ├── tpl │ │ └── sql-attachment.ftl │ └── xml │ │ └── currentUserRequest.xml │ └── schemas │ └── xjc │ └── userdata.wsdl ├── niffler-gateway ├── build.gradle ├── src │ ├── main │ │ ├── java │ │ │ └── guru │ │ │ │ └── qa │ │ │ │ └── niffler │ │ │ │ ├── NifflerGatewayApplication.java │ │ │ │ ├── config │ │ │ │ ├── NifflerGatewayServiceConfig.java │ │ │ │ ├── SecurityConfigLocal.java │ │ │ │ └── SecurityConfigMain.java │ │ │ │ ├── controller │ │ │ │ ├── CategoriesController.java │ │ │ │ ├── CurrencyController.java │ │ │ │ ├── FriendsController.java │ │ │ │ ├── InvitationsController.java │ │ │ │ ├── SessionController.java │ │ │ │ ├── SpendController.java │ │ │ │ ├── StatController.java │ │ │ │ ├── UserController.java │ │ │ │ ├── graphql │ │ │ │ │ ├── SessionQueryController.java │ │ │ │ │ ├── SpendMutationController.java │ │ │ │ │ ├── SpendQueryController.java │ │ │ │ │ ├── StatQueryController.java │ │ │ │ │ ├── UserMutationController.java │ │ │ │ │ └── UserQueryController.java │ │ │ │ └── v2 │ │ │ │ │ ├── FriendsV2Controller.java │ │ │ │ │ ├── SpendV2Controller.java │ │ │ │ │ ├── StatV2Controller.java │ │ │ │ │ └── UserV2Controller.java │ │ │ │ ├── ex │ │ │ │ ├── IllegalGqlFieldAccessException.java │ │ │ │ ├── InvalidUserJsonException.java │ │ │ │ ├── NoRestResponseException.java │ │ │ │ ├── NoSoapResponseException.java │ │ │ │ └── TooManySubQueriesException.java │ │ │ │ ├── model │ │ │ │ ├── CategoryJson.java │ │ │ │ ├── CurrencyJson.java │ │ │ │ ├── CurrencyValues.java │ │ │ │ ├── DataFilterValues.java │ │ │ │ ├── ErrorJson.java │ │ │ │ ├── FriendJson.java │ │ │ │ ├── FriendshipStatus.java │ │ │ │ ├── SessionJson.java │ │ │ │ ├── SpendJson.java │ │ │ │ ├── StatisticByCategoryJson.java │ │ │ │ ├── StatisticJson.java │ │ │ │ ├── StatisticV2Json.java │ │ │ │ ├── SumByCategory.java │ │ │ │ ├── UserJson.java │ │ │ │ ├── gql │ │ │ │ │ ├── CategoryGqlInput.java │ │ │ │ │ ├── FriendshipAction.java │ │ │ │ │ ├── FriendshipGqlInput.java │ │ │ │ │ ├── SpendFormGql.java │ │ │ │ │ ├── SpendGqlInput.java │ │ │ │ │ ├── UserGql.java │ │ │ │ │ └── UserGqlInput.java │ │ │ │ └── page │ │ │ │ │ └── RestPage.java │ │ │ │ └── service │ │ │ │ ├── GlobalExceptionHandler.java │ │ │ │ ├── GraphQlExceptionResolver.java │ │ │ │ ├── PropertiesLogger.java │ │ │ │ ├── StatisticAggregator.java │ │ │ │ ├── UserDataClient.java │ │ │ │ ├── api │ │ │ │ ├── GrpcCurrencyClient.java │ │ │ │ ├── RestSpendClient.java │ │ │ │ └── RestUserDataClient.java │ │ │ │ ├── cors │ │ │ │ └── CorsCustomizer.java │ │ │ │ ├── soap │ │ │ │ ├── SoapPageable.java │ │ │ │ └── SoapUserDataClient.java │ │ │ │ └── utils │ │ │ │ ├── GqlQueryPaginationAndSort.java │ │ │ │ └── HttpQueryPaginationAndSort.java │ │ ├── resources │ │ │ ├── application-test.yaml │ │ │ ├── application.yaml │ │ │ ├── graphql │ │ │ │ └── query.graphqls │ │ │ └── static │ │ │ │ └── favicon.ico │ │ └── schemas │ │ │ └── xjc │ │ │ └── userdata.wsdl │ └── test │ │ └── java │ │ └── guru │ │ └── qa │ │ └── niffler │ │ └── controller │ │ ├── CategoriesControllerTest.java │ │ └── CurrencyControllerTest.java └── userdata.wsdl ├── niffler-grpc-common ├── .dockerignore ├── build.gradle └── src │ └── main │ └── proto │ └── niffler-currency.proto ├── niffler-ng-client ├── .dockerignore ├── .env ├── .env.docker ├── .env.prod ├── .env.staging ├── .eslintrc.cjs ├── .gitignore ├── Dockerfile ├── README.md ├── index.html ├── nginx.conf ├── package-lock.json ├── package.json ├── public │ └── favicon.ico ├── src │ ├── App.css │ ├── App.tsx │ ├── api │ │ ├── apiClient.ts │ │ ├── authClient.ts │ │ └── authUtils.ts │ ├── assets │ │ ├── fonts │ │ │ ├── Inter-Regular.woff2 │ │ │ ├── YoungSerif-Regular.ttf │ │ │ ├── YoungSerif-Regular.woff │ │ │ └── YoungSerif-Regular.woff2 │ │ ├── icons │ │ │ ├── coin.svg │ │ │ ├── forest.svg │ │ │ ├── ic_add_friend.svg │ │ │ ├── ic_all.svg │ │ │ ├── ic_archive.svg │ │ │ ├── ic_cal.svg │ │ │ ├── ic_check.svg │ │ │ ├── ic_cross.svg │ │ │ ├── ic_delete.svg │ │ │ ├── ic_edit.svg │ │ │ ├── ic_friends.svg │ │ │ ├── ic_menu.svg │ │ │ ├── ic_plus.svg │ │ │ ├── ic_signout.svg │ │ │ ├── ic_upload.svg │ │ │ └── ic_user.svg │ │ └── images │ │ │ ├── niffler-with-a-coin.png │ │ │ └── niffler-with-a-coin2.png │ ├── components │ │ ├── AppContent │ │ │ └── index.tsx │ │ ├── Button │ │ │ └── index.tsx │ │ ├── CategoryItem │ │ │ └── index.tsx │ │ ├── CategorySection │ │ │ └── index.tsx │ │ ├── CategorySelect │ │ │ └── index.tsx │ │ ├── CurrencySelect │ │ │ └── index.tsx │ │ ├── Diagram │ │ │ └── index.tsx │ │ ├── EmptyUsersState │ │ │ └── index.tsx │ │ ├── Icon │ │ │ ├── CalendarIcon.tsx │ │ │ └── index.tsx │ │ ├── ImageUpload │ │ │ ├── index.tsx │ │ │ └── styles.css │ │ ├── Input │ │ │ ├── index.tsx │ │ │ └── style.css │ │ ├── Loader │ │ │ └── index.tsx │ │ ├── MenuAppBar │ │ │ ├── HeaderMenu │ │ │ │ └── index.tsx │ │ │ ├── MenuButton │ │ │ │ └── index.tsx │ │ │ ├── MobileHeaderMenu │ │ │ │ └── index.tsx │ │ │ ├── NewSpendingButton │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ └── styles.css │ │ ├── NewCategoryFrom │ │ │ └── index.tsx │ │ ├── PeopleTable │ │ │ ├── AcceptButton │ │ │ │ └── index.tsx │ │ │ ├── ActionButtons │ │ │ │ └── index.tsx │ │ │ ├── AddFriendButton │ │ │ │ └── index.tsx │ │ │ ├── AllTable │ │ │ │ └── index.tsx │ │ │ ├── DeclineButton │ │ │ │ └── index.tsx │ │ │ ├── FriendsTable │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── PrivateRoute │ │ │ └── index.tsx │ │ ├── ProfileForm │ │ │ ├── formValidate.ts │ │ │ └── index.tsx │ │ ├── SearchInput │ │ │ └── index.tsx │ │ ├── SpendingForm │ │ │ ├── formValidate.ts │ │ │ └── index.tsx │ │ ├── SpendingsTable │ │ │ ├── Toolbar │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── TabPanel │ │ │ └── index.tsx │ │ ├── Table │ │ │ ├── HeadCell │ │ │ │ └── index.tsx │ │ │ ├── Pagination │ │ │ │ └── index.tsx │ │ │ ├── TableHead │ │ │ │ └── index.tsx │ │ │ └── TableToolbar │ │ │ │ └── index.tsx │ │ └── Toggle │ │ │ └── index.tsx │ ├── const │ │ └── constants.ts │ ├── context │ │ ├── DialogContext.tsx │ │ ├── SessionContext.tsx │ │ └── SnackBarContext.tsx │ ├── hooks │ │ └── usePrevious.ts │ ├── index.css │ ├── main.tsx │ ├── pages │ │ ├── Authorized │ │ │ └── index.tsx │ │ ├── Main │ │ │ └── index.tsx │ │ ├── NotFoundPage │ │ │ └── index.tsx │ │ ├── PeoplePage │ │ │ └── index.tsx │ │ ├── ProfilePage │ │ │ └── index.tsx │ │ └── SpendingPage │ │ │ └── index.tsx │ ├── theme.tsx │ ├── types │ │ ├── Category.ts │ │ ├── CategoryStatistic.ts │ │ ├── Country.ts │ │ ├── Currency.ts │ │ ├── DoughnutOptions.ts │ │ ├── Error.ts │ │ ├── FilterPeriod.ts │ │ ├── FriendshipStatus.ts │ │ ├── IStringIndex.ts │ │ ├── Likes.ts │ │ ├── Order.ts │ │ ├── Pageable.ts │ │ ├── Photo.ts │ │ ├── RequestHandler.ts │ │ ├── Session.ts │ │ ├── Spending.ts │ │ ├── Statistic.ts │ │ ├── User.ts │ │ └── Void.ts │ ├── utils │ │ ├── arrays.ts │ │ ├── chart.ts │ │ ├── comparator.ts │ │ ├── dataConverter.ts │ │ ├── date.ts │ │ └── form.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── niffler-ng-gql-client ├── .dockerignore ├── .env ├── .env.docker ├── .env.prod ├── .env.staging ├── .eslintrc.cjs ├── .gitignore ├── Dockerfile ├── README.md ├── codegen.yml ├── index.html ├── nginx.conf ├── package-lock.json ├── package.json ├── public │ └── favicon.ico ├── src │ ├── App.css │ ├── App.tsx │ ├── api │ │ ├── authClient.ts │ │ ├── authUtils.ts │ │ ├── graphqlClient.ts │ │ ├── mutations │ │ │ ├── category.graphql │ │ │ ├── spend.graphql │ │ │ └── user.graphql │ │ └── queries │ │ │ ├── currency.graphql │ │ │ ├── session.graphql │ │ │ ├── spend.graphql │ │ │ ├── stat.graphql │ │ │ └── user.graphql │ ├── assets │ │ ├── fonts │ │ │ ├── Inter-Regular.woff2 │ │ │ ├── YoungSerif-Regular.ttf │ │ │ ├── YoungSerif-Regular.woff │ │ │ └── YoungSerif-Regular.woff2 │ │ ├── icons │ │ │ ├── coin.svg │ │ │ ├── forest.svg │ │ │ ├── ic_add_friend.svg │ │ │ ├── ic_all.svg │ │ │ ├── ic_archive.svg │ │ │ ├── ic_cal.svg │ │ │ ├── ic_check.svg │ │ │ ├── ic_cross.svg │ │ │ ├── ic_delete.svg │ │ │ ├── ic_edit.svg │ │ │ ├── ic_friends.svg │ │ │ ├── ic_menu.svg │ │ │ ├── ic_plus.svg │ │ │ ├── ic_signout.svg │ │ │ ├── ic_upload.svg │ │ │ └── ic_user.svg │ │ └── images │ │ │ ├── niffler-with-a-coin.png │ │ │ └── niffler-with-a-coin2.png │ ├── components │ │ ├── AppContent │ │ │ └── index.tsx │ │ ├── Button │ │ │ └── index.tsx │ │ ├── CategoryItem │ │ │ └── index.tsx │ │ ├── CategorySection │ │ │ └── index.tsx │ │ ├── CategorySelect │ │ │ └── index.tsx │ │ ├── CommonError │ │ │ └── index.tsx │ │ ├── CurrencySelect │ │ │ └── index.tsx │ │ ├── Diagram │ │ │ └── index.tsx │ │ ├── EmptyUsersState │ │ │ └── index.tsx │ │ ├── Icon │ │ │ ├── CalendarIcon.tsx │ │ │ └── index.tsx │ │ ├── ImageUpload │ │ │ ├── index.tsx │ │ │ └── styles.css │ │ ├── Input │ │ │ ├── index.tsx │ │ │ └── style.css │ │ ├── Loader │ │ │ └── index.tsx │ │ ├── MenuAppBar │ │ │ ├── HeaderMenu │ │ │ │ └── index.tsx │ │ │ ├── MenuButton │ │ │ │ └── index.tsx │ │ │ ├── MobileHeaderMenu │ │ │ │ └── index.tsx │ │ │ ├── NewSpendingButton │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ └── styles.css │ │ ├── NewCategoryFrom │ │ │ └── index.tsx │ │ ├── PeopleTable │ │ │ ├── AcceptButton │ │ │ │ └── index.tsx │ │ │ ├── ActionButtons │ │ │ │ └── index.tsx │ │ │ ├── AddFriendButton │ │ │ │ └── index.tsx │ │ │ ├── AllTable │ │ │ │ └── index.tsx │ │ │ ├── DeclineButton │ │ │ │ └── index.tsx │ │ │ ├── FriendsTable │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── PrivateRoute │ │ │ └── index.tsx │ │ ├── ProfileForm │ │ │ ├── formValidate.ts │ │ │ └── index.tsx │ │ ├── SearchInput │ │ │ └── index.tsx │ │ ├── SpendingForm │ │ │ ├── AddFormComponent │ │ │ │ └── index.tsx │ │ │ ├── EditFormComponent │ │ │ │ └── index.tsx │ │ │ ├── FormComponent │ │ │ │ └── index.tsx │ │ │ ├── formValidate.ts │ │ │ └── index.tsx │ │ ├── SpendingsTable │ │ │ ├── Toolbar │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── TabPanel │ │ │ └── index.tsx │ │ ├── Table │ │ │ ├── HeadCell │ │ │ │ └── index.tsx │ │ │ ├── Pagination │ │ │ │ └── index.tsx │ │ │ ├── TableHead │ │ │ │ └── index.tsx │ │ │ └── TableToolbar │ │ │ │ └── index.tsx │ │ └── Toggle │ │ │ └── index.tsx │ ├── const │ │ └── constants.ts │ ├── context │ │ ├── DialogContext.tsx │ │ └── SnackBarContext.tsx │ ├── generated │ │ └── graphql.tsx │ ├── hooks │ │ └── usePrevious.ts │ ├── index.css │ ├── main.tsx │ ├── pages │ │ ├── Authorized │ │ │ └── index.tsx │ │ ├── Main │ │ │ └── index.tsx │ │ ├── NotFoundPage │ │ │ └── index.tsx │ │ ├── PeoplePage │ │ │ └── index.tsx │ │ ├── ProfilePage │ │ │ └── index.tsx │ │ └── SpendingPage │ │ │ └── index.tsx │ ├── theme.tsx │ ├── types │ │ ├── Category.ts │ │ ├── CategoryStatistic.ts │ │ ├── Country.ts │ │ ├── Currency.ts │ │ ├── DoughnutOptions.ts │ │ ├── Error.ts │ │ ├── FilterPeriod.ts │ │ ├── FriendshipStatus.ts │ │ ├── IStringIndex.ts │ │ ├── Likes.ts │ │ ├── Order.ts │ │ ├── Photo.ts │ │ ├── RequestHandler.ts │ │ ├── Session.ts │ │ ├── Spending.ts │ │ ├── Statistic.ts │ │ ├── User.ts │ │ └── Void.ts │ ├── utils │ │ ├── arrays.ts │ │ ├── chart.ts │ │ ├── comparator.ts │ │ ├── dataConverter.ts │ │ ├── date.ts │ │ └── form.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── niffler-spend ├── build.gradle └── src │ ├── main │ ├── java │ │ └── guru │ │ │ └── qa │ │ │ └── niffler │ │ │ ├── NifflerSpendApplication.java │ │ │ ├── controller │ │ │ ├── CategoriesController.java │ │ │ ├── SpendController.java │ │ │ ├── StatController.java │ │ │ └── v2 │ │ │ │ ├── SpendV2Controller.java │ │ │ │ └── StatV2Controller.java │ │ │ ├── data │ │ │ ├── CategoryEntity.java │ │ │ ├── SpendEntity.java │ │ │ ├── projection │ │ │ │ ├── SumByCategory.java │ │ │ │ ├── SumByCategoryAggregate.java │ │ │ │ ├── SumByCategoryInUserCurrency.java │ │ │ │ └── SumByCategoryInfo.java │ │ │ └── repository │ │ │ │ ├── CategoryRepository.java │ │ │ │ └── SpendRepository.java │ │ │ ├── ex │ │ │ ├── CategoryNotFoundException.java │ │ │ ├── InvalidCategoryNameException.java │ │ │ ├── SpendNotFoundException.java │ │ │ └── TooManyCategoriesException.java │ │ │ ├── model │ │ │ ├── CategoryJson.java │ │ │ ├── CurrencyJson.java │ │ │ ├── CurrencyValues.java │ │ │ ├── DataFilterValues.java │ │ │ ├── ErrorJson.java │ │ │ ├── SpendJson.java │ │ │ ├── StatisticByCategoryJson.java │ │ │ ├── StatisticJson.java │ │ │ └── StatisticV2Json.java │ │ │ └── service │ │ │ ├── CategoryService.java │ │ │ ├── GlobalExceptionHandler.java │ │ │ ├── GrpcCurrencyClient.java │ │ │ ├── PropertiesLogger.java │ │ │ ├── SpendService.java │ │ │ └── StatService.java │ └── resources │ │ ├── application-test.yaml │ │ ├── application.yaml │ │ └── db │ │ └── migration │ │ └── niffler-spend │ │ ├── V1__schema_init.sql │ │ ├── V2__rename_tables.sql │ │ ├── V3__add_archived_field_to_category.sql │ │ └── V4__rename_category_field.sql │ └── test │ ├── java │ └── guru │ │ └── qa │ │ └── niffler │ │ ├── controller │ │ └── CategoriesControllerTest.java │ │ └── service │ │ ├── CategoryServiceTest.java │ │ └── StatServiceTest.java │ └── resources │ ├── categoriesListShouldBeReturnedForCurrentUser.sql │ └── categoryNameAndArchivedStatusShouldBeUpdated.sql ├── niffler-userdata ├── build.gradle └── src │ ├── main │ ├── java │ │ └── guru │ │ │ └── qa │ │ │ └── niffler │ │ │ ├── NifflerUserdataApplication.java │ │ │ ├── config │ │ │ ├── NifflerUserdataConsumerConfiguration.java │ │ │ └── NifflerUserdataServiceConfig.java │ │ │ ├── controller │ │ │ ├── FriendsController.java │ │ │ ├── InvitationsController.java │ │ │ ├── UserController.java │ │ │ └── v2 │ │ │ │ ├── FriendsV2Controller.java │ │ │ │ └── UserV2Controller.java │ │ │ ├── data │ │ │ ├── CurrencyValues.java │ │ │ ├── FriendShipId.java │ │ │ ├── FriendshipEntity.java │ │ │ ├── FriendshipStatus.java │ │ │ ├── UserEntity.java │ │ │ ├── projection │ │ │ │ └── UserWithStatus.java │ │ │ └── repository │ │ │ │ └── UserRepository.java │ │ │ ├── ex │ │ │ ├── NotFoundException.java │ │ │ └── SameUsernameException.java │ │ │ ├── model │ │ │ ├── ErrorJson.java │ │ │ ├── FriendshipStatus.java │ │ │ ├── IUserJson.java │ │ │ ├── UserJson.java │ │ │ └── UserJsonBulk.java │ │ │ ├── service │ │ │ ├── GlobalExceptionHandler.java │ │ │ ├── MigrationService.java │ │ │ ├── PropertiesLogger.java │ │ │ ├── SmallPhoto.java │ │ │ └── UserService.java │ │ │ └── soap │ │ │ ├── BaseEndpoint.java │ │ │ ├── FriendsEndpoint.java │ │ │ ├── InvitationsEndpoint.java │ │ │ ├── SpringPageable.java │ │ │ └── UserEndpoint.java │ └── resources │ │ ├── application-test.yaml │ │ ├── application.yaml │ │ ├── db │ │ └── migration │ │ │ └── niffler-userdata │ │ │ ├── V1__schema_init.sql │ │ │ ├── V2__rename_tables.sql │ │ │ ├── V3__friendship.sql │ │ │ ├── V4__small_avatar.sql │ │ │ └── V5__full_name.sql │ │ └── userdata.xsd │ └── test │ ├── java │ └── guru │ │ └── qa │ │ └── niffler │ │ ├── controller │ │ └── UserControllerTest.java │ │ └── service │ │ └── UserServiceTest.java │ └── resources │ └── currentUserShouldBeReturned.sql ├── postgres └── script │ └── init-database.sh ├── selenoid └── browsers.json ├── settings.gradle └── wiremock ├── grpc └── mappings │ ├── calculateRate.json │ └── getAllCurrencies.json └── rest └── mappings └── currentUser.json /.github/ISSUE_TEMPLATE/шаблон-баг-репорта.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Баг-репорт 3 | about: Описание 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Предусловие** 11 | Пример - "юзер авторизован"/"открыта главная страница сайта"/ "у юзера есть 3 спендинга в рублевой валюте за текущий 12 | месяц" 13 | 14 | **Шаги для воспроизведения бага** 15 | 16 | 1. 17 | 2. 18 | 3. 19 | 20 | ... 21 | 22 | **Ожидаемое поведение** 23 | Как система должна себя вести в соответствии с требованиями 24 | 25 | **Фактический результат** 26 | Как система себя ведет в действительности 27 | 28 | **Окружение** 29 | Если баг воспроизводится на определенных версиях операционных систем/браузеров/девайсов 30 | 31 | **Скриншоты/ Видео** 32 | Если есть возможность приложить визуальное подтверждение бага 33 | 34 | **Дополнительно** 35 | Место для дополнительной полезной информации о баге 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build 4 | **/build 5 | !gradle/wrapper/gradle-wrapper.jar 6 | !gradle/wrapper/gradle-wrapper.properties 7 | !**/src/main/**/build/ 8 | !**/src/test/**/build/ 9 | 10 | ### STS ### 11 | .apt_generated 12 | .classpath 13 | .factorypath 14 | .project 15 | .settings 16 | .springBeans 17 | .sts4-cache 18 | bin/ 19 | !**/src/main/**/bin/ 20 | !**/src/test/**/bin/ 21 | 22 | ### IntelliJ IDEA ### 23 | .idea 24 | *.iws 25 | *.iml 26 | *.ipr 27 | out/ 28 | !**/src/main/**/out/ 29 | !**/src/test/**/out/ 30 | 31 | ### NetBeans ### 32 | /nbproject/private/ 33 | /nbbuild/ 34 | /dist/ 35 | /nbdist/ 36 | /.nb-gradle/ 37 | 38 | ### VS Code ### 39 | .vscode/ 40 | 41 | ### gRPC generated files ### 42 | /niffler-grpc-common/src/generated/** 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) [2022-2024] [Dmitrii Tuchs] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /docker-compose-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source ./docker.properties 3 | export PROFILE=docker 4 | export PREFIX="${IMAGE_PREFIX}" 5 | 6 | docker compose down 7 | docker_containers=$(docker ps -a -q) 8 | docker_images=$(docker images --format '{{.Repository}}:{{.Tag}}' | grep 'niffler') 9 | 10 | if [ ! -z "$docker_containers" ]; then 11 | echo "### Stop containers: $docker_containers ###" 12 | docker stop $docker_containers 13 | docker rm $docker_containers 14 | fi 15 | 16 | if [ ! -z "$docker_images" ]; then 17 | echo "### Remove images: $docker_images ###" 18 | docker rmi $docker_images 19 | fi 20 | 21 | echo '### Java version ###' 22 | java --version 23 | bash ./gradlew clean 24 | if [ "$1" = "push" ]; then 25 | echo "### Build & push images ###" 26 | bash ./gradlew jib -x :niffler-e-2-e-tests:test 27 | docker compose push frontend.niffler.dc 28 | else 29 | echo "### Build images ###" 30 | bash ./gradlew jibDockerBuild -x :niffler-e-2-e-tests:test 31 | fi 32 | 33 | docker compose up -d 34 | docker ps -a 35 | -------------------------------------------------------------------------------- /docker-compose-e2e.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source ./docker.properties 3 | export COMPOSE_PROFILES=test 4 | export PROFILE=docker 5 | export PREFIX="${IMAGE_PREFIX}" 6 | 7 | export ALLURE_DOCKER_API=http://allure:5050/ 8 | export HEAD_COMMIT_MESSAGE="local build" 9 | export ARCH=$(uname -m) 10 | 11 | echo '### Java version ###' 12 | java --version 13 | 14 | docker compose down 15 | docker_containers=$(docker ps -a -q) 16 | docker_images=$(docker images --format '{{.Repository}}:{{.Tag}}' | grep 'niffler') 17 | 18 | if [ ! -z "$docker_containers" ]; then 19 | echo "### Stop containers: $docker_containers ###" 20 | docker stop $docker_containers 21 | docker rm $docker_containers 22 | fi 23 | 24 | if [ ! -z "$docker_images" ]; then 25 | echo "### Remove images: $docker_images ###" 26 | docker rmi $docker_images 27 | fi 28 | 29 | echo '### Java version ###' 30 | java --version 31 | bash ./gradlew clean 32 | bash ./gradlew jibDockerBuild -x :niffler-e-2-e-tests:test 33 | 34 | docker pull selenoid/vnc_chrome:127.0 35 | docker compose up -d 36 | docker ps -a 37 | -------------------------------------------------------------------------------- /docker.properties: -------------------------------------------------------------------------------- 1 | IMAGE_PREFIX=qaguru 2 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.daemon=false 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /localenv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker stop $(docker ps -a -q) 4 | docker rm $(docker ps -a -q) 5 | 6 | docker run --name niffler-all -p 5432:5432 -e POSTGRES_PASSWORD=secret -e CREATE_DATABASES=niffler-auth,niffler-currency,niffler-spend,niffler-userdata -e TZ=GMT+3 -e PGTZ=GMT+3 -v pgdata:/var/lib/postgresql/data -v ./postgres/script:/docker-entrypoint-initdb.d -d postgres:15.1 --max_prepared_transactions=100 7 | docker run --name=zookeeper -e ZOOKEEPER_CLIENT_PORT=2181 -p 2181:2181 -d confluentinc/cp-zookeeper:7.3.2 8 | docker run --name=kafka -e KAFKA_BROKER_ID=1 \ 9 | -e KAFKA_ZOOKEEPER_CONNECT=$(docker inspect zookeeper --format='{{ .NetworkSettings.IPAddress }}'):2181 \ 10 | -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092 \ 11 | -e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 \ 12 | -e KAFKA_TRANSACTION_STATE_LOG_MIN_ISR=1 \ 13 | -e KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1 \ 14 | -p 9092:9092 -d confluentinc/cp-kafka:7.3.2 15 | -------------------------------------------------------------------------------- /niffler-auth/src/main/java/guru/qa/niffler/NifflerAuthApplication.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler; 2 | 3 | import guru.qa.niffler.service.PropertiesLogger; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | @SpringBootApplication 8 | public class NifflerAuthApplication { 9 | 10 | public static void main(String[] args) { 11 | SpringApplication springApplication = new SpringApplication(NifflerAuthApplication.class); 12 | springApplication.addListeners(new PropertiesLogger()); 13 | springApplication.run(args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /niffler-auth/src/main/java/guru/qa/niffler/config/keys/KeyManager.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.config.keys; 2 | 3 | import com.nimbusds.jose.jwk.RSAKey; 4 | import jakarta.annotation.Nonnull; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.security.KeyPair; 8 | import java.security.KeyPairGenerator; 9 | import java.security.NoSuchAlgorithmException; 10 | import java.security.interfaces.RSAPrivateKey; 11 | import java.security.interfaces.RSAPublicKey; 12 | import java.util.UUID; 13 | 14 | @Component 15 | public class KeyManager { 16 | 17 | public @Nonnull 18 | RSAKey rsaKey() throws NoSuchAlgorithmException { 19 | KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); 20 | generator.initialize(2048); 21 | KeyPair keyPair = generator.generateKeyPair(); 22 | RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); 23 | RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); 24 | return new RSAKey.Builder(publicKey) 25 | .privateKey(privateKey) 26 | .keyID(UUID.randomUUID().toString()) 27 | .build(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /niffler-auth/src/main/java/guru/qa/niffler/data/Authority.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data; 2 | 3 | public enum Authority { 4 | read, write 5 | } 6 | -------------------------------------------------------------------------------- /niffler-auth/src/main/java/guru/qa/niffler/data/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data.repository; 2 | 3 | import guru.qa.niffler.data.UserEntity; 4 | import jakarta.annotation.Nonnull; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | import java.util.Optional; 8 | import java.util.UUID; 9 | 10 | public interface UserRepository extends JpaRepository { 11 | 12 | @Nonnull 13 | Optional findByUsername(@Nonnull String username); 14 | } 15 | -------------------------------------------------------------------------------- /niffler-auth/src/main/java/guru/qa/niffler/model/EqualPasswords.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | import guru.qa.niffler.service.EqualPasswordsValidator; 4 | import jakarta.validation.Constraint; 5 | import jakarta.validation.Payload; 6 | 7 | import java.lang.annotation.ElementType; 8 | import java.lang.annotation.Retention; 9 | import java.lang.annotation.RetentionPolicy; 10 | import java.lang.annotation.Target; 11 | 12 | @Target(ElementType.TYPE) 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Constraint(validatedBy = {EqualPasswordsValidator.class}) 15 | public @interface EqualPasswords { 16 | String message() default "Passwords should be equal"; 17 | 18 | Class[] groups() default {}; 19 | 20 | Class[] payload() default {}; 21 | } 22 | -------------------------------------------------------------------------------- /niffler-auth/src/main/java/guru/qa/niffler/model/RegistrationModel.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import jakarta.validation.constraints.Size; 5 | 6 | @EqualPasswords 7 | public record RegistrationModel( 8 | @NotBlank(message = "Username can not be blank") 9 | @Size(min = 3, max = 50, message = "Allowed username length should be from 3 to 50 characters") 10 | String username, 11 | @NotBlank(message = "Password can not be blank") 12 | @Size(min = 3, max = 12, message = "Allowed password length should be from 3 to 12 characters") 13 | String password, 14 | @NotBlank(message = "Password submit can not be blank") 15 | @Size(min = 3, max = 12, message = "Allowed password length should be from 3 to 12 characters") 16 | String passwordSubmit) { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /niffler-auth/src/main/java/guru/qa/niffler/model/UserJson.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public record UserJson( 6 | @JsonProperty("username") 7 | String username) { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /niffler-auth/src/main/java/guru/qa/niffler/service/EqualPasswordsValidator.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.service; 2 | 3 | import guru.qa.niffler.model.EqualPasswords; 4 | import guru.qa.niffler.model.RegistrationModel; 5 | import jakarta.validation.ConstraintValidator; 6 | import jakarta.validation.ConstraintValidatorContext; 7 | 8 | public class EqualPasswordsValidator implements ConstraintValidator { 9 | @Override 10 | public boolean isValid(RegistrationModel form, ConstraintValidatorContext context) { 11 | boolean isValid = form.password().equals(form.passwordSubmit()); 12 | if (!isValid) { 13 | context.disableDefaultConstraintViolation(); 14 | context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()) 15 | .addPropertyNode("password") 16 | .addConstraintViolation(); 17 | } 18 | return isValid; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /niffler-auth/src/main/resources/db/migration/niffler-auth/V1__schema_init.sql: -------------------------------------------------------------------------------- 1 | create extension if not exists "uuid-ossp"; 2 | 3 | create table if not exists "users" 4 | ( 5 | id UUID unique not null default uuid_generate_v1(), 6 | username varchar(50) unique not null, 7 | password varchar(255) not null, 8 | enabled boolean not null, 9 | account_non_expired boolean not null, 10 | account_non_locked boolean not null, 11 | credentials_non_expired boolean not null, 12 | primary key (id, username) 13 | ); 14 | 15 | alter table "users" 16 | owner to postgres; 17 | 18 | create table if not exists "authorities" 19 | ( 20 | id UUID unique not null default uuid_generate_v1() primary key, 21 | user_id UUID not null, 22 | authority varchar(50) not null, 23 | constraint fk_authorities_users foreign key (user_id) references "users" (id) 24 | ); 25 | 26 | alter table "authorities" 27 | owner to postgres; 28 | 29 | create unique index if not exists ix_auth_username on "authorities" (user_id, authority); -------------------------------------------------------------------------------- /niffler-auth/src/main/resources/db/migration/niffler-auth/V2__rename_tables.sql: -------------------------------------------------------------------------------- 1 | alter table "users" 2 | rename to "user"; 3 | alter table "authorities" 4 | rename to "authority"; -------------------------------------------------------------------------------- /niffler-auth/src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n 7 | 8 | 9 | 10 | 11 | ${LOG_PATH} 12 | 13 | ${LOG_PATH}.%d{yyyy-MM-dd}.gz 14 | 30 15 | 16 | 17 | %d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /niffler-auth/src/main/resources/static/fonts/Inter-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-auth/src/main/resources/static/fonts/Inter-Regular.woff2 -------------------------------------------------------------------------------- /niffler-auth/src/main/resources/static/fonts/YoungSerif-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-auth/src/main/resources/static/fonts/YoungSerif-Regular.ttf -------------------------------------------------------------------------------- /niffler-auth/src/main/resources/static/fonts/YoungSerif-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-auth/src/main/resources/static/fonts/YoungSerif-Regular.woff -------------------------------------------------------------------------------- /niffler-auth/src/main/resources/static/fonts/YoungSerif-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-auth/src/main/resources/static/fonts/YoungSerif-Regular.woff2 -------------------------------------------------------------------------------- /niffler-auth/src/main/resources/static/images/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-auth/src/main/resources/static/images/background.jpg -------------------------------------------------------------------------------- /niffler-auth/src/main/resources/static/images/eye-active.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /niffler-auth/src/main/resources/static/images/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /niffler-auth/src/main/resources/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-auth/src/main/resources/static/images/favicon.ico -------------------------------------------------------------------------------- /niffler-auth/src/main/resources/static/images/forest-back.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-auth/src/main/resources/static/images/forest-back.jpeg -------------------------------------------------------------------------------- /niffler-auth/src/main/resources/static/images/niffler-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-auth/src/main/resources/static/images/niffler-logo.png -------------------------------------------------------------------------------- /niffler-currency/src/main/java/guru/qa/niffler/NifflerCurrencyApplication.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler; 2 | 3 | import guru.qa.niffler.service.PropertiesLogger; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | @SpringBootApplication 8 | public class NifflerCurrencyApplication { 9 | 10 | public static void main(String[] args) { 11 | SpringApplication springApplication = new SpringApplication(NifflerCurrencyApplication.class); 12 | springApplication.addListeners(new PropertiesLogger()); 13 | springApplication.run(args); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /niffler-currency/src/main/java/guru/qa/niffler/data/CurrencyValues.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | 5 | @RequiredArgsConstructor 6 | public enum CurrencyValues { 7 | RUB("Rub-cropped.svg"), USD("Dollar-cropped.svg"), EUR("Euro-cropped.svg"), KZT("Tenge-cropped.svg"); 8 | 9 | public final String symbolResource; 10 | } 11 | -------------------------------------------------------------------------------- /niffler-currency/src/main/java/guru/qa/niffler/data/repository/CurrencyRepository.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data.repository; 2 | 3 | import guru.qa.niffler.data.CurrencyEntity; 4 | import guru.qa.niffler.data.CurrencyValues; 5 | import jakarta.annotation.Nonnull; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | 8 | import java.util.Optional; 9 | import java.util.UUID; 10 | 11 | public interface CurrencyRepository extends JpaRepository { 12 | 13 | @Nonnull 14 | Optional findByCurrency(@Nonnull CurrencyValues currency); 15 | } 16 | -------------------------------------------------------------------------------- /niffler-currency/src/main/resources/Dollar-cropped.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /niffler-currency/src/main/resources/Euro-cropped.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /niffler-currency/src/main/resources/Tenge-cropped.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /niffler-currency/src/main/resources/db/migration/niffler-currency/V1__schema_init.sql: -------------------------------------------------------------------------------- 1 | create extension if not exists "uuid-ossp"; 2 | 3 | create table if not exists "currency" 4 | ( 5 | id UUID unique not null default uuid_generate_v1() primary key, 6 | currency varchar(50) unique not null, 7 | currency_rate float not null 8 | ); 9 | 10 | alter table "currency" 11 | owner to postgres; 12 | 13 | delete 14 | from "currency"; 15 | insert into "currency"(currency, currency_rate) 16 | values ('RUB', 0.015); 17 | insert into "currency"(currency, currency_rate) 18 | values ('KZT', 0.0021); 19 | insert into "currency"(currency, currency_rate) 20 | values ('EUR', 1.08); 21 | insert into "currency"(currency, currency_rate) 22 | values ('USD', 1.0); 23 | -------------------------------------------------------------------------------- /niffler-currency/src/main/resources/db/migration/niffler-currency/V2__currency_symbols.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "currency" 2 | ADD symbol bytea; -------------------------------------------------------------------------------- /niffler-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-diagram.png -------------------------------------------------------------------------------- /niffler-e-2-e-tests/.dockerignore: -------------------------------------------------------------------------------- 1 | build/ -------------------------------------------------------------------------------- /niffler-e-2-e-tests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eclipse-temurin:21-jdk 2 | 3 | WORKDIR /niffler 4 | ENV TZ=Europe/Moscow 5 | COPY ./gradle ./gradle 6 | COPY ./niffler-e-2-e-tests ./niffler-e-2-e-tests 7 | COPY ./niffler-grpc-common ./niffler-grpc-common 8 | COPY ./gradlew ./ 9 | COPY ./build.gradle ./ 10 | COPY ./settings.gradle ./ 11 | COPY ./gradle.properties ./ 12 | 13 | CMD ./gradlew test -Dtest.env=docker -Duser.timezone=Europe/Moscow 14 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/graphql/currency.graphql: -------------------------------------------------------------------------------- 1 | query Currencies { 2 | currencies { 3 | currency 4 | } 5 | } -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/graphql/stat.graphql: -------------------------------------------------------------------------------- 1 | query Stat($filterPeriod: FilterPeriod, $filterCurrency: CurrencyValues, $statCurrency: CurrencyValues){ 2 | stat(filterPeriod: $filterPeriod, filterCurrency: $filterCurrency, statCurrency: $statCurrency) { 3 | total 4 | currency 5 | statByCategories { 6 | categoryName 7 | currency 8 | sum 9 | firstSpendDate 10 | lastSpendDate 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/GatewayV2Api.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.api; 2 | 3 | import guru.qa.niffler.model.rest.UserJson; 4 | import guru.qa.niffler.model.rest.pageable.RestResponsePage; 5 | import retrofit2.Call; 6 | import retrofit2.http.GET; 7 | import retrofit2.http.Header; 8 | import retrofit2.http.Query; 9 | 10 | import javax.annotation.Nullable; 11 | 12 | public interface GatewayV2Api { 13 | 14 | @GET("api/v2/friends/all") 15 | Call> allFriends(@Header("Authorization") String bearerToken, 16 | @Query("searchQuery") @Nullable String searchQuery, 17 | @Query("page") int page, 18 | @Query("sort") String sort); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/GhApi.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.api; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import retrofit2.Call; 5 | import retrofit2.http.GET; 6 | import retrofit2.http.Header; 7 | import retrofit2.http.Headers; 8 | import retrofit2.http.Path; 9 | 10 | public interface GhApi { 11 | 12 | @GET("repos/qa-guru/niffler/issues/{issue_number}") 13 | @Headers({ 14 | "Accept: application/vnd.github+json", 15 | "X-GitHub-Api-Version: 2022-11-28" 16 | }) 17 | Call issue(@Header("Authorization") String bearerToken, 18 | @Path("issue_number") String issueNumber); 19 | } 20 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/UserdataSoapApi.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.api; 2 | 3 | import guru.qa.jaxb.userdata.AllUsersRequest; 4 | import guru.qa.jaxb.userdata.CurrentUserRequest; 5 | import guru.qa.jaxb.userdata.UserResponse; 6 | import guru.qa.jaxb.userdata.UsersResponse; 7 | import retrofit2.Call; 8 | import retrofit2.http.Body; 9 | import retrofit2.http.Headers; 10 | import retrofit2.http.POST; 11 | 12 | public interface UserdataSoapApi { 13 | 14 | @Headers(value = { 15 | "Content-type: text/xml", 16 | "Accept-Charset: utf-8" 17 | }) 18 | @POST("ws") 19 | Call currentUser(@Body CurrentUserRequest currentUserRequest); 20 | 21 | @Headers(value = { 22 | "Content-type: text/xml", 23 | "Accept-Charset: utf-8" 24 | }) 25 | @POST("ws") 26 | Call allUsers(@Body AllUsersRequest allUsersRequest); 27 | 28 | 29 | } 30 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/api/core/CodeInterceptor.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.api.core; 2 | 3 | import guru.qa.niffler.jupiter.extension.ApiLoginExtension; 4 | import okhttp3.Interceptor; 5 | import okhttp3.Response; 6 | import org.apache.commons.lang3.StringUtils; 7 | 8 | import java.io.IOException; 9 | import java.util.Objects; 10 | 11 | public class CodeInterceptor implements Interceptor { 12 | @Override 13 | public Response intercept(Chain chain) throws IOException { 14 | final Response response = chain.proceed(chain.request()); 15 | if (response.isRedirect()) { 16 | String location = Objects.requireNonNull( 17 | response.header("Location") 18 | ); 19 | if (location.contains("code=")) { 20 | ApiLoginExtension.setCode( 21 | StringUtils.substringAfter( 22 | location, "code=" 23 | ) 24 | ); 25 | } 26 | } 27 | return response; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/condition/Color.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.condition; 2 | 3 | import lombok.AllArgsConstructor; 4 | 5 | @AllArgsConstructor 6 | public enum Color { 7 | yellow("rgba(255, 183, 3, 1)"), green("rgba(53, 173, 123, 1)"); 8 | 9 | public final String rgb; 10 | } 11 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/config/Config.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.config; 2 | 3 | import javax.annotation.Nonnull; 4 | 5 | public interface Config { 6 | 7 | static @Nonnull Config getInstance() { 8 | return "docker".equals(System.getProperty("test.env")) 9 | ? DockerConfig.INSTANCE 10 | : LocalConfig.INSTANCE; 11 | } 12 | 13 | @Nonnull 14 | String frontUrl(); 15 | 16 | @Nonnull 17 | String authUrl(); 18 | 19 | @Nonnull 20 | String authJdbcUrl(); 21 | 22 | @Nonnull 23 | String gatewayUrl(); 24 | 25 | @Nonnull 26 | String userdataUrl(); 27 | 28 | @Nonnull 29 | String userdataJdbcUrl(); 30 | 31 | @Nonnull 32 | String spendUrl(); 33 | 34 | @Nonnull 35 | String spendJdbcUrl(); 36 | 37 | @Nonnull 38 | String currencyJdbcUrl(); 39 | 40 | @Nonnull 41 | String currencyGrpcAddress(); 42 | 43 | default int currencyGrpcPort() { 44 | return 8092; 45 | } 46 | 47 | @Nonnull 48 | default String ghUrl() { 49 | return "https://api.github.com/"; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/data/dao/AuthAuthorityDao.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data.dao; 2 | 3 | import guru.qa.niffler.data.entity.auth.AuthorityEntity; 4 | 5 | import javax.annotation.Nonnull; 6 | import javax.annotation.ParametersAreNonnullByDefault; 7 | import java.util.List; 8 | import java.util.UUID; 9 | 10 | @ParametersAreNonnullByDefault 11 | public interface AuthAuthorityDao { 12 | 13 | void create(AuthorityEntity... authority); 14 | 15 | @Nonnull 16 | List findAll(); 17 | 18 | @Nonnull 19 | List findAllByUserId(UUID userId); 20 | } 21 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/data/dao/AuthUserDao.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data.dao; 2 | 3 | import guru.qa.niffler.data.entity.auth.AuthUserEntity; 4 | 5 | import javax.annotation.Nonnull; 6 | import javax.annotation.ParametersAreNonnullByDefault; 7 | import java.util.List; 8 | import java.util.Optional; 9 | import java.util.UUID; 10 | 11 | @ParametersAreNonnullByDefault 12 | public interface AuthUserDao { 13 | 14 | @Nonnull 15 | AuthUserEntity create(AuthUserEntity user); 16 | 17 | @Nonnull 18 | Optional findById(UUID id); 19 | 20 | @Nonnull 21 | Optional findByUsername(String username); 22 | 23 | @Nonnull 24 | List findAll(); 25 | } 26 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/data/dao/CategoryDao.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data.dao; 2 | 3 | import guru.qa.niffler.data.entity.spend.CategoryEntity; 4 | 5 | import javax.annotation.Nonnull; 6 | import javax.annotation.ParametersAreNonnullByDefault; 7 | import java.util.List; 8 | import java.util.Optional; 9 | import java.util.UUID; 10 | 11 | @ParametersAreNonnullByDefault 12 | public interface CategoryDao { 13 | 14 | @Nonnull 15 | CategoryEntity create(CategoryEntity category); 16 | 17 | @Nonnull 18 | Optional findById(UUID id); 19 | 20 | @Nonnull 21 | List findAll(); 22 | 23 | @Nonnull 24 | CategoryEntity update(CategoryEntity category); 25 | } 26 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/data/dao/SpendDao.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data.dao; 2 | 3 | import guru.qa.niffler.data.entity.spend.SpendEntity; 4 | 5 | import javax.annotation.Nonnull; 6 | import javax.annotation.ParametersAreNonnullByDefault; 7 | import java.util.List; 8 | import java.util.Optional; 9 | import java.util.UUID; 10 | 11 | @ParametersAreNonnullByDefault 12 | public interface SpendDao { 13 | 14 | @Nonnull 15 | SpendEntity create(SpendEntity spend); 16 | 17 | @Nonnull 18 | Optional findById(UUID id); 19 | 20 | @Nonnull 21 | List findAll(); 22 | 23 | @Nonnull 24 | SpendEntity update(SpendEntity spend); 25 | } 26 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/data/dao/UserdataUserDao.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data.dao; 2 | 3 | import guru.qa.niffler.data.entity.userdata.UserEntity; 4 | 5 | import javax.annotation.Nonnull; 6 | import javax.annotation.ParametersAreNonnullByDefault; 7 | import java.util.List; 8 | import java.util.Optional; 9 | import java.util.UUID; 10 | 11 | @ParametersAreNonnullByDefault 12 | public interface UserdataUserDao { 13 | 14 | @Nonnull 15 | UserEntity create(UserEntity user); 16 | 17 | @Nonnull 18 | UserEntity update(UserEntity user); 19 | 20 | @Nonnull 21 | Optional findById(UUID id); 22 | 23 | @Nonnull 24 | Optional findByUsername(String username); 25 | 26 | @Nonnull 27 | List findAll(); 28 | } 29 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/data/entity/auth/Authority.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data.entity.auth; 2 | 3 | public enum Authority { 4 | read, write 5 | } 6 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/data/entity/userdata/FriendShipId.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data.entity.userdata; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.io.Serializable; 7 | import java.util.Objects; 8 | import java.util.UUID; 9 | 10 | @Getter 11 | @Setter 12 | public class FriendShipId implements Serializable { 13 | 14 | private UUID requester; 15 | private UUID addressee; 16 | 17 | @Override 18 | public boolean equals(Object o) { 19 | if (this == o) return true; 20 | if (o == null || getClass() != o.getClass()) return false; 21 | FriendShipId friendsId = (FriendShipId) o; 22 | return Objects.equals(requester, friendsId.requester) && Objects.equals(addressee, friendsId.addressee); 23 | } 24 | 25 | @Override 26 | public int hashCode() { 27 | return Objects.hash(requester, addressee); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/data/entity/userdata/FriendshipStatus.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data.entity.userdata; 2 | 3 | public enum FriendshipStatus { 4 | PENDING, 5 | ACCEPTED 6 | } 7 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/data/jdbc/JdbcConnectionHolders.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data.jdbc; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.List; 5 | 6 | public class JdbcConnectionHolders implements AutoCloseable { 7 | 8 | private final List holders; 9 | 10 | public JdbcConnectionHolders(@Nonnull List holders) { 11 | this.holders = holders; 12 | } 13 | 14 | @Override 15 | public void close() { 16 | holders.forEach(JdbcConnectionHolder::close); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/data/logging/SqlAttachmentData.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data.logging; 2 | 3 | import io.qameta.allure.attachment.AttachmentData; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | public class SqlAttachmentData implements AttachmentData { 8 | 9 | private final String name; 10 | private final String sql; 11 | 12 | public SqlAttachmentData(String name, String sql) { 13 | this.name = name; 14 | this.sql = sql; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/data/mapper/AuthorityEntityRowMapper.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data.mapper; 2 | 3 | import guru.qa.niffler.data.entity.auth.AuthUserEntity; 4 | import guru.qa.niffler.data.entity.auth.Authority; 5 | import guru.qa.niffler.data.entity.auth.AuthorityEntity; 6 | import org.springframework.jdbc.core.RowMapper; 7 | 8 | import javax.annotation.Nonnull; 9 | import java.sql.ResultSet; 10 | import java.sql.SQLException; 11 | import java.util.UUID; 12 | 13 | public class AuthorityEntityRowMapper implements RowMapper { 14 | 15 | public static final AuthorityEntityRowMapper instance = new AuthorityEntityRowMapper(); 16 | 17 | private AuthorityEntityRowMapper() { 18 | } 19 | 20 | @Override 21 | @Nonnull 22 | public AuthorityEntity mapRow(ResultSet rs, int rowNum) throws SQLException { 23 | AuthorityEntity ae = new AuthorityEntity(); 24 | ae.setId(rs.getObject("id", UUID.class)); 25 | ae.setUser(new AuthUserEntity(rs.getObject("user_id", UUID.class))); 26 | ae.setAuthority(Authority.valueOf(rs.getString("authority"))); 27 | return ae; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/data/mapper/CategoryEntityRowMapper.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data.mapper; 2 | 3 | import guru.qa.niffler.data.entity.spend.CategoryEntity; 4 | import org.springframework.jdbc.core.RowMapper; 5 | 6 | import javax.annotation.Nonnull; 7 | import java.sql.ResultSet; 8 | import java.sql.SQLException; 9 | import java.util.UUID; 10 | 11 | public class CategoryEntityRowMapper implements RowMapper { 12 | 13 | public static final CategoryEntityRowMapper instance = new CategoryEntityRowMapper(); 14 | 15 | private CategoryEntityRowMapper() { 16 | } 17 | 18 | @Override 19 | @Nonnull 20 | public CategoryEntity mapRow(ResultSet rs, int rowNum) throws SQLException { 21 | CategoryEntity ce = new CategoryEntity(); 22 | ce.setId(rs.getObject("id", UUID.class)); 23 | ce.setUsername(rs.getString("username")); 24 | ce.setName(rs.getString("name")); 25 | ce.setArchived(rs.getBoolean("archived")); 26 | return ce; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/data/repository/AuthUserRepository.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data.repository; 2 | 3 | import guru.qa.niffler.data.entity.auth.AuthUserEntity; 4 | 5 | import javax.annotation.Nonnull; 6 | import javax.annotation.ParametersAreNonnullByDefault; 7 | import java.util.List; 8 | import java.util.Optional; 9 | import java.util.UUID; 10 | 11 | @ParametersAreNonnullByDefault 12 | public interface AuthUserRepository { 13 | 14 | @Nonnull 15 | AuthUserEntity create(AuthUserEntity user); 16 | 17 | @Nonnull 18 | Optional findById(UUID id); 19 | 20 | @Nonnull 21 | Optional findByUsername(String username); 22 | 23 | List all(); 24 | } 25 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/data/repository/UserdataUserRepository.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data.repository; 2 | 3 | import guru.qa.niffler.data.entity.userdata.UserEntity; 4 | 5 | import javax.annotation.Nonnull; 6 | import javax.annotation.ParametersAreNonnullByDefault; 7 | import java.util.Optional; 8 | import java.util.UUID; 9 | 10 | @ParametersAreNonnullByDefault 11 | public interface UserdataUserRepository { 12 | 13 | @Nonnull 14 | UserEntity create(UserEntity user); 15 | 16 | @Nonnull 17 | UserEntity update(UserEntity user); 18 | 19 | @Nonnull 20 | Optional findById(UUID id); 21 | 22 | @Nonnull 23 | Optional findByUsername(String username); 24 | 25 | void addFriendshipRequest(UserEntity requester, UserEntity addressee); 26 | 27 | void addFriend(UserEntity requester, UserEntity addressee); 28 | } 29 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/ApiLogin.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.jupiter.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface ApiLogin { 11 | String username() default ""; 12 | 13 | String password() default ""; 14 | } 15 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/Category.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.jupiter.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface Category { 11 | String name() default ""; 12 | 13 | boolean archived() default false; 14 | } 15 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/DisabledByIssue.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.jupiter.annotation; 2 | 3 | import guru.qa.niffler.jupiter.extension.IssueExtension; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | @Target({ElementType.TYPE, ElementType.METHOD}) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @ExtendWith(IssueExtension.class) 14 | public @interface DisabledByIssue { 15 | String value(); 16 | } 17 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/ScreenShotTest.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.jupiter.annotation; 2 | 3 | import guru.qa.niffler.jupiter.extension.ScreenShotTestExtension; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | 7 | import java.lang.annotation.ElementType; 8 | import java.lang.annotation.Retention; 9 | import java.lang.annotation.RetentionPolicy; 10 | import java.lang.annotation.Target; 11 | 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Target(ElementType.METHOD) 14 | @Test 15 | @ExtendWith(ScreenShotTestExtension.class) 16 | public @interface ScreenShotTest { 17 | String value(); 18 | } 19 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/Spending.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.jupiter.annotation; 2 | 3 | import guru.qa.niffler.model.rest.CurrencyValues; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Target(ElementType.METHOD) 12 | public @interface Spending { 13 | String category() default ""; 14 | 15 | String description(); 16 | 17 | double amount(); 18 | 19 | CurrencyValues currency() default CurrencyValues.RUB; 20 | } 21 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/Token.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.jupiter.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.PARAMETER) 10 | public @interface Token { 11 | } 12 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/User.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.jupiter.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface User { 11 | String username() default ""; 12 | 13 | Category[] categories() default {}; 14 | 15 | Spending[] spendings() default {}; 16 | 17 | int friends() default 0; 18 | 19 | int incomeInvitations() default 0; 20 | 21 | int outcomeInvitations() default 0; 22 | } 23 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/GqlTest.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.jupiter.annotation.meta; 2 | 3 | import guru.qa.niffler.jupiter.extension.CategoryExtension; 4 | import guru.qa.niffler.jupiter.extension.SpendingExtension; 5 | import guru.qa.niffler.jupiter.extension.UserExtension; 6 | import io.qameta.allure.junit5.AllureJunit5; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | 9 | import java.lang.annotation.ElementType; 10 | import java.lang.annotation.Retention; 11 | import java.lang.annotation.RetentionPolicy; 12 | import java.lang.annotation.Target; 13 | 14 | @Retention(RetentionPolicy.RUNTIME) 15 | @Target(ElementType.TYPE) 16 | @ExtendWith({ 17 | AllureJunit5.class, 18 | UserExtension.class, 19 | CategoryExtension.class, 20 | SpendingExtension.class, 21 | }) 22 | public @interface GqlTest { 23 | } 24 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/GrpcTest.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.jupiter.annotation.meta; 2 | 3 | import guru.qa.niffler.jupiter.extension.CategoryExtension; 4 | import guru.qa.niffler.jupiter.extension.SpendingExtension; 5 | import guru.qa.niffler.jupiter.extension.UserExtension; 6 | import io.qameta.allure.junit5.AllureJunit5; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | 9 | import java.lang.annotation.ElementType; 10 | import java.lang.annotation.Retention; 11 | import java.lang.annotation.RetentionPolicy; 12 | import java.lang.annotation.Target; 13 | 14 | @Retention(RetentionPolicy.RUNTIME) 15 | @Target(ElementType.TYPE) 16 | @ExtendWith({ 17 | AllureJunit5.class, 18 | UserExtension.class, 19 | CategoryExtension.class, 20 | SpendingExtension.class, 21 | }) 22 | public @interface GrpcTest { 23 | } 24 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/RestTest.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.jupiter.annotation.meta; 2 | 3 | import guru.qa.niffler.jupiter.extension.CategoryExtension; 4 | import guru.qa.niffler.jupiter.extension.SpendingExtension; 5 | import guru.qa.niffler.jupiter.extension.UserExtension; 6 | import io.qameta.allure.junit5.AllureJunit5; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | 9 | import java.lang.annotation.ElementType; 10 | import java.lang.annotation.Retention; 11 | import java.lang.annotation.RetentionPolicy; 12 | import java.lang.annotation.Target; 13 | 14 | @Retention(RetentionPolicy.RUNTIME) 15 | @Target(ElementType.TYPE) 16 | @ExtendWith({ 17 | AllureJunit5.class, 18 | UserExtension.class, 19 | CategoryExtension.class, 20 | SpendingExtension.class, 21 | }) 22 | public @interface RestTest { 23 | } 24 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/SoapTest.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.jupiter.annotation.meta; 2 | 3 | import guru.qa.niffler.jupiter.extension.CategoryExtension; 4 | import guru.qa.niffler.jupiter.extension.SpendingExtension; 5 | import guru.qa.niffler.jupiter.extension.UserExtension; 6 | import io.qameta.allure.junit5.AllureJunit5; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | 9 | import java.lang.annotation.ElementType; 10 | import java.lang.annotation.Retention; 11 | import java.lang.annotation.RetentionPolicy; 12 | import java.lang.annotation.Target; 13 | 14 | @Retention(RetentionPolicy.RUNTIME) 15 | @Target(ElementType.TYPE) 16 | @ExtendWith({ 17 | AllureJunit5.class, 18 | UserExtension.class, 19 | CategoryExtension.class, 20 | SpendingExtension.class, 21 | }) 22 | public @interface SoapTest { 23 | } 24 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/annotation/meta/WebTest.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.jupiter.annotation.meta; 2 | 3 | import guru.qa.niffler.jupiter.extension.ApiLoginExtension; 4 | import guru.qa.niffler.jupiter.extension.BrowserExtension; 5 | import guru.qa.niffler.jupiter.extension.CategoryExtension; 6 | import guru.qa.niffler.jupiter.extension.SpendingExtension; 7 | import guru.qa.niffler.jupiter.extension.UserExtension; 8 | import io.qameta.allure.junit5.AllureJunit5; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | 11 | import java.lang.annotation.ElementType; 12 | import java.lang.annotation.Retention; 13 | import java.lang.annotation.RetentionPolicy; 14 | import java.lang.annotation.Target; 15 | 16 | @Retention(RetentionPolicy.RUNTIME) 17 | @Target(ElementType.TYPE) 18 | @ExtendWith({ 19 | BrowserExtension.class, 20 | AllureJunit5.class, 21 | UserExtension.class, 22 | CategoryExtension.class, 23 | SpendingExtension.class, 24 | ApiLoginExtension.class 25 | }) 26 | public @interface WebTest { 27 | } 28 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/CookieJarExtension.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.jupiter.extension; 2 | 3 | import guru.qa.niffler.api.core.ThreadSafeCookieStore; 4 | import org.junit.jupiter.api.extension.AfterTestExecutionCallback; 5 | import org.junit.jupiter.api.extension.ExtensionContext; 6 | 7 | public class CookieJarExtension implements AfterTestExecutionCallback { 8 | @Override 9 | public void afterTestExecution(ExtensionContext context) throws Exception { 10 | ThreadSafeCookieStore.INSTANCE.removeAll(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/DatabasesExtension.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.jupiter.extension; 2 | 3 | import guru.qa.niffler.data.jdbc.Connections; 4 | import guru.qa.niffler.data.jpa.EntityManagers; 5 | 6 | public class DatabasesExtension implements SuiteExtension { 7 | @Override 8 | public void afterSuite() { 9 | Connections.closeAllConnections(); 10 | EntityManagers.closeAllEmfs(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/jupiter/extension/UsersClientExtension.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.jupiter.extension; 2 | 3 | import guru.qa.niffler.service.UsersClient; 4 | import guru.qa.niffler.service.impl.UsersDbClient; 5 | import org.junit.jupiter.api.extension.ExtensionContext; 6 | import org.junit.jupiter.api.extension.TestInstancePostProcessor; 7 | 8 | import java.lang.reflect.Field; 9 | 10 | public class UsersClientExtension implements TestInstancePostProcessor { 11 | @Override 12 | public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception { 13 | for (Field field : testInstance.getClass().getDeclaredFields()) { 14 | if (field.getType().isAssignableFrom(UsersClient.class)) { 15 | field.setAccessible(true); 16 | field.set(testInstance, new UsersDbClient()); 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/model/allure/ScreenDif.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model.allure; 2 | 3 | public record ScreenDif(String expected, String actual, String diff) { 4 | } 5 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/model/rest/CategoryJson.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model.rest; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import guru.qa.niffler.data.entity.spend.CategoryEntity; 5 | 6 | import javax.annotation.Nonnull; 7 | import java.util.UUID; 8 | 9 | public record CategoryJson( 10 | @JsonProperty("id") 11 | UUID id, 12 | @JsonProperty("name") 13 | String name, 14 | @JsonProperty("username") 15 | String username, 16 | @JsonProperty("archived") 17 | boolean archived) { 18 | 19 | public static @Nonnull CategoryJson fromEntity(@Nonnull CategoryEntity entity) { 20 | return new CategoryJson( 21 | entity.getId(), 22 | entity.getName(), 23 | entity.getUsername(), 24 | entity.isArchived() 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/model/rest/CurrencyJson.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model.rest; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public record CurrencyJson( 6 | @JsonProperty("currency") 7 | CurrencyValues currency, 8 | @JsonProperty("currencyRate") 9 | Double currencyRate) { 10 | } 11 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/model/rest/CurrencyValues.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model.rest; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | 5 | import javax.annotation.Nonnull; 6 | 7 | @RequiredArgsConstructor 8 | public enum CurrencyValues { 9 | RUB("₽"), USD("$"), EUR("€"), KZT("₸"); 10 | public final String symbol; 11 | 12 | public static @Nonnull CurrencyValues fromSymbol(@Nonnull String symbol) { 13 | for (CurrencyValues value : CurrencyValues.values()) { 14 | if (value.symbol.equals(symbol)) { 15 | return value; 16 | } 17 | } 18 | throw new IllegalArgumentException("Can`t find CurrencyValues by given symbol: " + symbol); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/model/rest/DataFilterValues.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model.rest; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | 5 | @RequiredArgsConstructor 6 | public enum DataFilterValues { 7 | TODAY("Today"), WEEK("last week"), MONTH("Last month"); 8 | public final String text; 9 | } 10 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/model/rest/FriendJson.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model.rest; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public record FriendJson( 6 | @JsonProperty("username") 7 | String username) { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/model/rest/FriendshipStatus.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model.rest; 2 | 3 | public enum FriendshipStatus { 4 | INVITE_SENT, INVITE_RECEIVED, FRIEND 5 | } 6 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/model/rest/SessionJson.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model.rest; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.util.Date; 6 | 7 | public record SessionJson(@JsonProperty("username") 8 | String username, 9 | @JsonProperty("issuedAt") 10 | Date issuedAt, 11 | @JsonProperty("expiresAt") 12 | Date expiresAt) { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/model/rest/StatisticByCategoryJson.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model.rest; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.util.List; 6 | 7 | public record StatisticByCategoryJson( 8 | @JsonProperty("category") 9 | String category, 10 | @JsonProperty("total") 11 | Double total, 12 | @JsonProperty("totalInUserDefaultCurrency") 13 | Double totalInUserDefaultCurrency, 14 | @JsonProperty("spends") 15 | List spends) { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/model/rest/StatisticJson.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model.rest; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.util.Date; 6 | import java.util.List; 7 | 8 | public record StatisticJson( 9 | @JsonProperty("dateFrom") 10 | Date dateFrom, 11 | @JsonProperty("dateTo") 12 | Date dateTo, 13 | @JsonProperty("currency") 14 | CurrencyValues currency, 15 | @JsonProperty("total") 16 | Double total, 17 | @JsonProperty("userDefaultCurrency") 18 | CurrencyValues userDefaultCurrency, 19 | @JsonProperty("totalInUserDefaultCurrency") 20 | Double totalInUserDefaultCurrency, 21 | @JsonProperty("categoryStatistics") 22 | List categoryStatistics) { 23 | 24 | } 25 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/model/rest/StatisticV2Json.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model.rest; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.util.List; 6 | 7 | public record StatisticV2Json( 8 | @JsonProperty("total") 9 | Double total, 10 | @JsonProperty("currency") 11 | CurrencyValues currency, 12 | @JsonProperty("statByCategories") 13 | List statByCategories) { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/model/rest/SumByCategory.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model.rest; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.util.Date; 6 | 7 | public record SumByCategory(@JsonProperty("categoryName") 8 | String categoryName, 9 | @JsonProperty("currency") 10 | CurrencyValues currency, 11 | @JsonProperty("sum") 12 | double sum, 13 | @JsonProperty("firstSpendDate") 14 | Date firstSpendDate, 15 | @JsonProperty("lastSpendDate") 16 | Date lastSpendDate) { 17 | } 18 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/page/component/BaseComponent.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.page.component; 2 | 3 | import com.codeborne.selenide.SelenideElement; 4 | 5 | import javax.annotation.Nonnull; 6 | import javax.annotation.ParametersAreNonnullByDefault; 7 | 8 | @ParametersAreNonnullByDefault 9 | public abstract class BaseComponent> { 10 | 11 | protected final SelenideElement self; 12 | 13 | public BaseComponent(SelenideElement self) { 14 | this.self = self; 15 | } 16 | 17 | @Nonnull 18 | public SelenideElement getSelf() { 19 | return self; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/page/component/SelectField.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.page.component; 2 | 3 | import com.codeborne.selenide.SelenideElement; 4 | import io.qameta.allure.Step; 5 | 6 | import static com.codeborne.selenide.Condition.text; 7 | import static com.codeborne.selenide.Selenide.$$; 8 | 9 | public class SelectField extends BaseComponent { 10 | 11 | public SelectField(SelenideElement self) { 12 | super(self); 13 | } 14 | 15 | private final SelenideElement input = self.$("input"); 16 | 17 | public void setValue(String value) { 18 | self.click(); 19 | $$("li[role='option']").find(text(value)).click(); 20 | } 21 | 22 | @Step("Check that selected value is equal to {value}") 23 | public void checkSelectValueIsEqualTo(String value) { 24 | self.shouldHave(text(value)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/service/GhClient.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.service; 2 | 3 | import javax.annotation.Nonnull; 4 | 5 | public interface GhClient { 6 | @Nonnull 7 | String issueState(String issueNumber); 8 | } 9 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/service/SpendClient.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.service; 2 | 3 | import guru.qa.niffler.model.rest.CategoryJson; 4 | import guru.qa.niffler.model.rest.SpendJson; 5 | 6 | import javax.annotation.Nonnull; 7 | import javax.annotation.ParametersAreNonnullByDefault; 8 | 9 | @ParametersAreNonnullByDefault 10 | public interface SpendClient { 11 | @Nonnull 12 | SpendJson createSpend(SpendJson spend); 13 | 14 | @Nonnull 15 | CategoryJson createCategory(CategoryJson category); 16 | 17 | @Nonnull 18 | CategoryJson updateCategory(CategoryJson category); 19 | 20 | void removeCategory(CategoryJson category); 21 | } 22 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/service/UsersClient.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.service; 2 | 3 | import guru.qa.niffler.model.rest.UserJson; 4 | 5 | import javax.annotation.Nonnull; 6 | import javax.annotation.ParametersAreNonnullByDefault; 7 | import java.util.List; 8 | 9 | @ParametersAreNonnullByDefault 10 | public interface UsersClient { 11 | 12 | List all(); 13 | 14 | @Nonnull 15 | UserJson createUser(String username, String password); 16 | 17 | void addIncomeInvitation(UserJson targetUser, int count); 18 | 19 | void addOutcomeInvitation(UserJson targetUser, int count); 20 | 21 | void addFriend(UserJson targetUser, int count); 22 | } 23 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/fake/OAuthTest.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.test.fake; 2 | 3 | import guru.qa.niffler.config.Config; 4 | import guru.qa.niffler.jupiter.annotation.ApiLogin; 5 | import guru.qa.niffler.jupiter.annotation.Token; 6 | import guru.qa.niffler.jupiter.extension.ApiLoginExtension; 7 | import guru.qa.niffler.service.impl.AuthApiClient; 8 | import org.junit.jupiter.api.Assertions; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.condition.DisabledIfSystemProperty; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | 13 | @ExtendWith(ApiLoginExtension.class) 14 | @DisabledIfSystemProperty(named = "test.env", matches = "docker") 15 | public class OAuthTest { 16 | 17 | private static final Config CFG = Config.getInstance(); 18 | private final AuthApiClient authApiClient = new AuthApiClient(); 19 | 20 | @Test 21 | @ApiLogin(username = "duck", password = "12345") 22 | void oauthTest(@Token String token) { 23 | Assertions.assertNotNull(token); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/grpc/BaseGrpcTest.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.test.grpc; 2 | 3 | import guru.qa.niffler.config.Config; 4 | import guru.qa.niffler.grpc.NifflerCurrencyServiceGrpc; 5 | import guru.qa.niffler.jupiter.annotation.meta.GrpcTest; 6 | import guru.qa.niffler.utils.GrpcConsoleInterceptor; 7 | import io.grpc.Channel; 8 | import io.grpc.ManagedChannelBuilder; 9 | 10 | @GrpcTest 11 | public class BaseGrpcTest { 12 | 13 | protected static final Config CFG = Config.getInstance(); 14 | 15 | protected static final Channel channel = ManagedChannelBuilder 16 | .forAddress(CFG.currencyGrpcAddress(), CFG.currencyGrpcPort()) 17 | .intercept(new GrpcConsoleInterceptor()) 18 | .usePlaintext() 19 | .build(); 20 | 21 | protected static final NifflerCurrencyServiceGrpc.NifflerCurrencyServiceBlockingStub blockingStub 22 | = NifflerCurrencyServiceGrpc.newBlockingStub(channel); 23 | } 24 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/grpc/CurrencyGrpcTest.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.test.grpc; 2 | 3 | import com.google.protobuf.Empty; 4 | import guru.qa.niffler.grpc.Currency; 5 | import guru.qa.niffler.grpc.CurrencyResponse; 6 | import org.junit.jupiter.api.Assertions; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.util.List; 10 | 11 | public class CurrencyGrpcTest extends BaseGrpcTest { 12 | 13 | @Test 14 | void allCurrenciesShouldReturned() { 15 | final CurrencyResponse response = blockingStub.getAllCurrencies(Empty.getDefaultInstance()); 16 | final List allCurrenciesList = response.getAllCurrenciesList(); 17 | Assertions.assertEquals(4, allCurrenciesList.size()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/soap/SoapUsersTest.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.test.soap; 2 | 3 | import guru.qa.jaxb.userdata.CurrentUserRequest; 4 | import guru.qa.jaxb.userdata.UserResponse; 5 | import guru.qa.niffler.jupiter.annotation.User; 6 | import guru.qa.niffler.jupiter.annotation.meta.SoapTest; 7 | import guru.qa.niffler.model.rest.UserJson; 8 | import guru.qa.niffler.service.impl.UserdataSoapClient; 9 | import org.junit.jupiter.api.Assertions; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import java.io.IOException; 13 | 14 | @SoapTest 15 | public class SoapUsersTest { 16 | 17 | private final UserdataSoapClient userdataSoapClient = new UserdataSoapClient(); 18 | 19 | @Test 20 | @User 21 | void currentUserTest(UserJson user) throws IOException { 22 | CurrentUserRequest request = new CurrentUserRequest(); 23 | request.setUsername(user.username()); 24 | UserResponse response = userdataSoapClient.currentUser(request); 25 | Assertions.assertEquals( 26 | user.username(), 27 | response.getUser().getUsername() 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/test/web/LoginTest.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.test.web; 2 | 3 | import com.codeborne.selenide.Selenide; 4 | import guru.qa.niffler.jupiter.annotation.User; 5 | import guru.qa.niffler.jupiter.annotation.meta.WebTest; 6 | import guru.qa.niffler.model.rest.UserJson; 7 | import guru.qa.niffler.page.LoginPage; 8 | import guru.qa.niffler.page.MainPage; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import static guru.qa.niffler.utils.RandomDataUtils.randomUsername; 12 | 13 | @WebTest 14 | public class LoginTest { 15 | 16 | @User 17 | @Test 18 | void mainPageShouldBeDisplayedAfterSuccessLogin(UserJson user) { 19 | Selenide.open(LoginPage.URL, LoginPage.class) 20 | .fillLoginPage(user.username(), user.testData().password()) 21 | .submit(new MainPage()) 22 | .checkThatPageLoaded(); 23 | } 24 | 25 | @Test 26 | void userShouldStayOnLoginPageAfterLoginWithBadCredentials() { 27 | Selenide.open(LoginPage.URL, LoginPage.class) 28 | .fillLoginPage(randomUsername(), "BAD") 29 | .submit(new LoginPage()) 30 | .checkError("Bad credentials"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/utils/OAuthUtils.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.utils; 2 | 3 | import lombok.SneakyThrows; 4 | 5 | import java.nio.charset.Charset; 6 | import java.security.MessageDigest; 7 | import java.security.SecureRandom; 8 | import java.util.Base64; 9 | 10 | public class OAuthUtils { 11 | 12 | private static SecureRandom secureRandom = new SecureRandom(); 13 | 14 | public static String generateCodeVerifier() { 15 | byte[] codeVerifier = new byte[32]; 16 | secureRandom.nextBytes(codeVerifier); 17 | return Base64.getUrlEncoder().withoutPadding().encodeToString(codeVerifier); 18 | } 19 | 20 | @SneakyThrows 21 | public static String generateCodeChallange(String codeVerifier) { 22 | byte[] bytes = codeVerifier.getBytes(Charset.forName("US-ASCII")); 23 | MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); 24 | messageDigest.update(bytes, 0, bytes.length); 25 | byte[] digest = messageDigest.digest(); 26 | return Base64.getUrlEncoder().withoutPadding().encodeToString(digest); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/utils/RandomDataUtils.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.utils; 2 | 3 | import com.github.javafaker.Faker; 4 | 5 | import javax.annotation.Nonnull; 6 | 7 | public class RandomDataUtils { 8 | 9 | private static final Faker faker = new Faker(); 10 | 11 | @Nonnull 12 | public static String randomUsername() { 13 | return faker.name().username(); 14 | } 15 | 16 | @Nonnull 17 | public static String randomName() { 18 | return faker.name().firstName(); 19 | } 20 | 21 | @Nonnull 22 | public static String randomSurname() { 23 | return faker.name().lastName(); 24 | } 25 | 26 | @Nonnull 27 | public static String randomCategoryName() { 28 | return faker.food().fruit(); 29 | } 30 | 31 | @Nonnull 32 | public static String randomSentence(int wordsCount) { 33 | return faker.lorem().sentence(wordsCount); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/java/guru/qa/niffler/utils/ScreenDiffResult.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.utils; 2 | 3 | import guru.qa.niffler.jupiter.extension.ScreenShotTestExtension; 4 | import ru.yandex.qatools.ashot.comparison.ImageDiff; 5 | import ru.yandex.qatools.ashot.comparison.ImageDiffer; 6 | 7 | import java.awt.image.BufferedImage; 8 | import java.util.function.BooleanSupplier; 9 | 10 | public class ScreenDiffResult implements BooleanSupplier { 11 | 12 | private final BufferedImage expected; 13 | private final BufferedImage actual; 14 | private final ImageDiff diff; 15 | private final boolean hasDif; 16 | 17 | public ScreenDiffResult(BufferedImage actual, BufferedImage expected) { 18 | this.actual = actual; 19 | this.expected = expected; 20 | this.diff = new ImageDiffer().makeDiff(expected, actual); 21 | this.hasDif = diff.hasDiff(); 22 | } 23 | 24 | @Override 25 | public boolean getAsBoolean() { 26 | if (hasDif) { 27 | ScreenShotTestExtension.setExpected(expected); 28 | ScreenShotTestExtension.setActual(actual); 29 | ScreenShotTestExtension.setDiff(diff.getMarkedImage()); 30 | } 31 | return hasDif; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension: -------------------------------------------------------------------------------- 1 | guru.qa.niffler.jupiter.extension.CookieJarExtension 2 | guru.qa.niffler.jupiter.extension.DatabasesExtension 3 | guru.qa.niffler.jupiter.extension.TestMethodContextExtension 4 | guru.qa.niffler.jupiter.extension.AllureBackendLogsExtension 5 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/resources/img/cat.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-e-2-e-tests/src/test/resources/img/cat.jpeg -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/resources/img/expected-stat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-e-2-e-tests/src/test/resources/img/expected-stat.png -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/resources/img/renoire.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-e-2-e-tests/src/test/resources/img/renoire.jpeg -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/resources/jndi.properties: -------------------------------------------------------------------------------- 1 | java.naming.factory.initial=org.osjava.sj.SimpleContextFactory 2 | org.osjava.sj.jndi.shared=true 3 | org.osjava.sj.jndi.ignoreClose=true 4 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/resources/junit-platform.properties: -------------------------------------------------------------------------------- 1 | junit.jupiter.execution.parallel.enabled=true 2 | junit.jupiter.execution.parallel.mode.default=concurrent 3 | junit.jupiter.execution.parallel.mode.classes.default=concurrent 4 | junit.jupiter.execution.parallel.config.strategy=fixed 5 | junit.jupiter.execution.parallel.config.fixed.parallelism=3 6 | junit.jupiter.execution.parallel.config.fixed.max-pool-size=3 7 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/resources/spy.properties: -------------------------------------------------------------------------------- 1 | driverlist=org.postgresql.Driver 2 | excludecategories=info,debug,result,resultset 3 | appender=guru.qa.niffler.data.logging.AllureAppender 4 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/resources/tpl/sql-attachment.ftl: -------------------------------------------------------------------------------- 1 | 2 | <#-- @ftlvariable name="data" type="guru.qa.niffler.data.logging.SqlAttachmentData" --> 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 |
SQL Query
18 |
19 |
${data.sql}
20 |
21 | 22 | -------------------------------------------------------------------------------- /niffler-e-2-e-tests/src/test/resources/xml/currentUserRequest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | pizzly 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/NifflerGatewayApplication.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler; 2 | 3 | import guru.qa.niffler.service.PropertiesLogger; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | @SpringBootApplication 8 | public class NifflerGatewayApplication { 9 | 10 | public static void main(String[] args) { 11 | SpringApplication springApplication = new SpringApplication(NifflerGatewayApplication.class); 12 | springApplication.addListeners(new PropertiesLogger()); 13 | springApplication.run(args); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/controller/SessionController.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.controller; 2 | 3 | 4 | import guru.qa.niffler.model.SessionJson; 5 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 6 | import org.springframework.security.oauth2.jwt.Jwt; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import java.util.Date; 12 | 13 | 14 | @RestController 15 | @RequestMapping("/api/session") 16 | public class SessionController { 17 | 18 | @GetMapping("/current") 19 | public SessionJson session(@AuthenticationPrincipal Jwt principal) { 20 | if (principal != null) { 21 | return new SessionJson( 22 | principal.getClaim("sub"), 23 | Date.from(principal.getIssuedAt()), 24 | Date.from(principal.getExpiresAt()) 25 | ); 26 | } else { 27 | return SessionJson.empty(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/controller/graphql/SessionQueryController.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.controller.graphql; 2 | 3 | 4 | import guru.qa.niffler.model.SessionJson; 5 | import org.springframework.graphql.data.method.annotation.QueryMapping; 6 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 7 | import org.springframework.security.oauth2.jwt.Jwt; 8 | import org.springframework.stereotype.Controller; 9 | 10 | import java.util.Date; 11 | 12 | 13 | @Controller 14 | public class SessionQueryController { 15 | 16 | @QueryMapping 17 | public SessionJson session(@AuthenticationPrincipal Jwt principal) { 18 | if (principal != null) { 19 | return new SessionJson( 20 | principal.getClaim("sub"), 21 | Date.from(principal.getIssuedAt()), 22 | Date.from(principal.getExpiresAt()) 23 | ); 24 | } else { 25 | return SessionJson.empty(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/ex/IllegalGqlFieldAccessException.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.ex; 2 | 3 | public class IllegalGqlFieldAccessException extends RuntimeException { 4 | public IllegalGqlFieldAccessException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/ex/InvalidUserJsonException.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.ex; 2 | 3 | public class InvalidUserJsonException extends RuntimeException { 4 | 5 | public InvalidUserJsonException() { 6 | } 7 | 8 | public InvalidUserJsonException(String message) { 9 | super(message); 10 | } 11 | 12 | public InvalidUserJsonException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public InvalidUserJsonException(Throwable cause) { 17 | super(cause); 18 | } 19 | 20 | public InvalidUserJsonException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 21 | super(message, cause, enableSuppression, writableStackTrace); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/ex/NoRestResponseException.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.ex; 2 | 3 | public class NoRestResponseException extends RuntimeException { 4 | public NoRestResponseException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/ex/NoSoapResponseException.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.ex; 2 | 3 | public class NoSoapResponseException extends RuntimeException { 4 | public NoSoapResponseException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/ex/TooManySubQueriesException.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.ex; 2 | 3 | public class TooManySubQueriesException extends RuntimeException { 4 | public TooManySubQueriesException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/model/CategoryJson.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import guru.qa.niffler.model.gql.CategoryGqlInput; 5 | import jakarta.annotation.Nonnull; 6 | import jakarta.validation.constraints.NotBlank; 7 | import jakarta.validation.constraints.Size; 8 | 9 | import java.util.UUID; 10 | 11 | public record CategoryJson( 12 | @JsonProperty("id") 13 | UUID id, 14 | @NotBlank(message = "Category can not be blank") 15 | @Size(min = 2, max = 50, message = "Allowed category length should be from 2 to 50 characters") 16 | @JsonProperty("name") 17 | String name, 18 | @JsonProperty("username") 19 | String username, 20 | @JsonProperty("archived") 21 | boolean archived) { 22 | 23 | public static CategoryJson fromCategoryInput(@Nonnull CategoryGqlInput input, @Nonnull String username) { 24 | return new CategoryJson( 25 | input.id(), 26 | input.name(), 27 | username, 28 | input.archived() 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/model/CurrencyJson.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import guru.qa.niffler.grpc.Currency; 5 | import jakarta.annotation.Nonnull; 6 | 7 | public record CurrencyJson( 8 | @JsonProperty("currency") 9 | CurrencyValues currency, 10 | @JsonProperty("currencyRate") 11 | Double currencyRate) { 12 | 13 | 14 | public static @Nonnull CurrencyJson fromGrpcMessage(@Nonnull Currency currencyMessage) { 15 | return new CurrencyJson( 16 | CurrencyValues.valueOf(currencyMessage.getCurrency().name()), 17 | currencyMessage.getCurrencyRate() 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/model/CurrencyValues.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | public enum CurrencyValues { 4 | RUB, USD, EUR, KZT 5 | } 6 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/model/DataFilterValues.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | public enum DataFilterValues { 4 | TODAY, WEEK, MONTH 5 | } 6 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/model/ErrorJson.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | import jakarta.annotation.Nonnull; 4 | 5 | public record ErrorJson(@Nonnull String type, 6 | @Nonnull String title, 7 | int status, 8 | @Nonnull String detail, 9 | @Nonnull String instance) { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/model/FriendJson.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.Size; 6 | 7 | public record FriendJson( 8 | @NotBlank(message = "Username can not be blank") 9 | @Size(max = 50, message = "Username can`t be longer than 50 characters") 10 | @JsonProperty("username") 11 | String username) { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/model/FriendshipStatus.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | public enum FriendshipStatus { 4 | INVITE_SENT, INVITE_RECEIVED, FRIEND 5 | } 6 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/model/SessionJson.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import jakarta.annotation.Nonnull; 5 | 6 | import java.util.Date; 7 | 8 | public record SessionJson(@JsonProperty("username") 9 | String username, 10 | @JsonProperty("issuedAt") 11 | Date issuedAt, 12 | @JsonProperty("expiresAt") 13 | Date expiresAt) { 14 | public static @Nonnull SessionJson empty() { 15 | return new SessionJson(null, null, null); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/model/StatisticByCategoryJson.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.util.List; 6 | 7 | public record StatisticByCategoryJson( 8 | @JsonProperty("category") 9 | String category, 10 | @JsonProperty("total") 11 | Double total, 12 | @JsonProperty("totalInUserDefaultCurrency") 13 | Double totalInUserDefaultCurrency, 14 | @JsonProperty("spends") 15 | List spends) { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/model/StatisticJson.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.util.Date; 6 | import java.util.List; 7 | 8 | public record StatisticJson( 9 | @JsonProperty("dateFrom") 10 | Date dateFrom, 11 | @JsonProperty("dateTo") 12 | Date dateTo, 13 | @JsonProperty("currency") 14 | CurrencyValues currency, 15 | @JsonProperty("total") 16 | Double total, 17 | @JsonProperty("userDefaultCurrency") 18 | CurrencyValues userDefaultCurrency, 19 | @JsonProperty("totalInUserDefaultCurrency") 20 | Double totalInUserDefaultCurrency, 21 | @JsonProperty("categoryStatistics") 22 | List categoryStatistics) { 23 | 24 | } 25 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/model/StatisticV2Json.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.util.List; 6 | 7 | public record StatisticV2Json( 8 | @JsonProperty("total") 9 | Double total, 10 | @JsonProperty("currency") 11 | CurrencyValues currency, 12 | @JsonProperty("statByCategories") 13 | List statByCategories) { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/model/SumByCategory.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.util.Date; 6 | 7 | public record SumByCategory(@JsonProperty("categoryName") 8 | String categoryName, 9 | @JsonProperty("currency") 10 | CurrencyValues currency, 11 | @JsonProperty("sum") 12 | double sum, 13 | @JsonProperty("firstSpendDate") 14 | Date firstSpendDate, 15 | @JsonProperty("lastSpendDate") 16 | Date lastSpendDate) { 17 | } 18 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/model/gql/CategoryGqlInput.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model.gql; 2 | 3 | import java.util.UUID; 4 | 5 | public record CategoryGqlInput( 6 | UUID id, 7 | String name, 8 | boolean archived 9 | ) { 10 | } 11 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/model/gql/FriendshipAction.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model.gql; 2 | 3 | public enum FriendshipAction { 4 | ADD, ACCEPT, REJECT, DELETE 5 | } 6 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/model/gql/FriendshipGqlInput.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model.gql; 2 | 3 | public record FriendshipGqlInput(String username, FriendshipAction action) { 4 | } 5 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/model/gql/SpendFormGql.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model.gql; 2 | 3 | import guru.qa.niffler.model.CategoryJson; 4 | import guru.qa.niffler.model.CurrencyJson; 5 | 6 | import java.util.List; 7 | 8 | public record SpendFormGql(List currencies, 9 | List categories) { 10 | } 11 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/model/gql/SpendGqlInput.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model.gql; 2 | 3 | import guru.qa.niffler.model.CurrencyValues; 4 | 5 | import java.util.Date; 6 | import java.util.UUID; 7 | 8 | public record SpendGqlInput( 9 | UUID id, 10 | Date spendDate, 11 | CategoryGqlInput category, 12 | CurrencyValues currency, 13 | Double amount, 14 | String description) { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/model/gql/UserGql.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model.gql; 2 | 3 | import guru.qa.niffler.model.CategoryJson; 4 | import guru.qa.niffler.model.CurrencyValues; 5 | import guru.qa.niffler.model.FriendshipStatus; 6 | import guru.qa.niffler.model.UserJson; 7 | import org.springframework.data.domain.Slice; 8 | 9 | import java.util.List; 10 | import java.util.UUID; 11 | 12 | public record UserGql( 13 | UUID id, 14 | String username, 15 | String fullname, 16 | CurrencyValues currency, 17 | String photo, 18 | String photoSmall, 19 | FriendshipStatus friendshipStatus, 20 | List categories, 21 | Slice friends, 22 | Slice allPeople) { 23 | 24 | public static UserGql fromUserJson(UserJson userJson) { 25 | return new UserGql( 26 | userJson.id(), 27 | userJson.username(), 28 | userJson.fullname(), 29 | userJson.currency(), 30 | userJson.photo(), 31 | userJson.photoSmall(), 32 | userJson.friendshipStatus(), 33 | null, 34 | null, 35 | null 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/model/gql/UserGqlInput.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model.gql; 2 | 3 | public record UserGqlInput( 4 | String fullname, 5 | String photo 6 | ) { 7 | } 8 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/java/guru/qa/niffler/service/utils/HttpQueryPaginationAndSort.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.service.utils; 2 | 3 | import org.springframework.data.domain.Pageable; 4 | import org.springframework.data.domain.Sort; 5 | 6 | import javax.annotation.Nonnull; 7 | 8 | public class HttpQueryPaginationAndSort { 9 | private final Pageable pageable; 10 | 11 | public HttpQueryPaginationAndSort(@Nonnull Pageable pageable) { 12 | this.pageable = pageable; 13 | } 14 | 15 | public @Nonnull String string() { 16 | StringBuilder query = new StringBuilder(); 17 | query.append("&page=") 18 | .append(pageable.getPageNumber()) 19 | .append("&size=") 20 | .append(pageable.getPageSize()); 21 | 22 | if (!pageable.getSort().isEmpty()) { 23 | for (Sort.Order order : pageable.getSort()) { 24 | query.append("&sort=") 25 | .append(order.getProperty()) 26 | .append(",") 27 | .append(order.getDirection().name()); 28 | } 29 | } 30 | return query.toString(); 31 | } 32 | 33 | @Override 34 | public String toString() { 35 | return string(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /niffler-gateway/src/main/resources/application-test.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | autoconfigure: 3 | exclude: 4 | - 'org.springframework.cloud.vault.config.VaultAutoConfiguration' 5 | - 'org.springframework.cloud.vault.config.VaultObservationAutoConfiguration' 6 | - 'org.springframework.cloud.vault.config.VaultReactiveAutoConfiguration' 7 | 8 | niffler-currency: 9 | base-uri: 'http://127.0.0.1:8091' 10 | niffler-userdata: 11 | base-uri: 'http://127.0.0.1:8089' 12 | niffler-spend: 13 | base-uri: 'http://127.0.0.1:8093' 14 | niffler-front: 15 | base-uri: 'http://127.0.0.1:3000' 16 | niffler-gateway: 17 | base-uri: 'http://127.0.0.1:8090' 18 | 19 | grpc: 20 | client: 21 | grpcCurrencyClient: 22 | address: 'static://localhost:8092' 23 | negotiationType: PLAINTEXT -------------------------------------------------------------------------------- /niffler-gateway/src/main/resources/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-gateway/src/main/resources/static/favicon.ico -------------------------------------------------------------------------------- /niffler-grpc-common/.dockerignore: -------------------------------------------------------------------------------- 1 | build/ -------------------------------------------------------------------------------- /niffler-grpc-common/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'idea' 3 | id 'com.google.protobuf' version '0.9.4' 4 | } 5 | 6 | group = 'guru.qa' 7 | version = '1.0.2' 8 | 9 | repositories { 10 | mavenCentral() 11 | } 12 | 13 | dependencies { 14 | implementation "io.grpc:grpc-protobuf:${project.ext.grpcVersion}" 15 | implementation "io.grpc:grpc-stub:${project.ext.grpcVersion}" 16 | implementation "com.google.protobuf:protobuf-java:${project.ext.protobufVersion}" 17 | compileOnly 'jakarta.annotation:jakarta.annotation-api:1.3.5' // Java 9+ compatibility - Do NOT update to 2.0.0 18 | } 19 | 20 | protobuf { 21 | protoc { 22 | artifact = "com.google.protobuf:protoc:${project.ext.protobufVersion}" 23 | } 24 | clean { 25 | delete generatedFilesBaseDir 26 | } 27 | plugins { 28 | grpc { 29 | artifact = "io.grpc:protoc-gen-grpc-java:${project.ext.grpcVersion}" 30 | } 31 | } 32 | generateProtoTasks { 33 | all()*.plugins { 34 | grpc {} 35 | } 36 | } 37 | } 38 | 39 | tasks.register('printVersion') { 40 | doLast { 41 | println project.version 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /niffler-grpc-common/src/main/proto/niffler-currency.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/protobuf/empty.proto"; 4 | 5 | package guru.qa.grpc.niffler; 6 | 7 | option java_multiple_files = true; 8 | option java_package = "guru.qa.niffler.grpc"; 9 | option java_outer_classname = "NifflerCurrencyProto"; 10 | 11 | service NifflerCurrencyService { 12 | rpc GetAllCurrencies (google.protobuf.Empty) returns (CurrencyResponse) {} 13 | rpc CalculateRate (CalculateRequest) returns (CalculateResponse) {} 14 | } 15 | 16 | message CurrencyResponse { 17 | repeated Currency allCurrencies = 1; 18 | } 19 | 20 | message Currency { 21 | CurrencyValues currency = 1; 22 | double currencyRate = 2; 23 | } 24 | 25 | message CalculateRequest { 26 | CurrencyValues spendCurrency = 1; 27 | CurrencyValues desiredCurrency = 2; 28 | double amount = 3; 29 | } 30 | 31 | message CalculateResponse { 32 | double calculatedAmount = 1; 33 | } 34 | 35 | enum CurrencyValues { 36 | UNSPECIFIED = 0; 37 | RUB = 1; 38 | USD = 2; 39 | EUR = 3; 40 | KZT = 4; 41 | } 42 | -------------------------------------------------------------------------------- /niffler-ng-client/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vite 3 | dist 4 | -------------------------------------------------------------------------------- /niffler-ng-client/.env: -------------------------------------------------------------------------------- 1 | VITE_AUTH_URL=http://127.0.0.1:9000 2 | VITE_API_URL=http://127.0.0.1:8090 3 | VITE_FRONT_HOST=127.0.0.1 4 | VITE_FRONT_URL=http://127.0.0.1:3000 5 | VITE_RESPONSE_TYPE=code 6 | VITE_CLIENT_ID=client 7 | -------------------------------------------------------------------------------- /niffler-ng-client/.env.docker: -------------------------------------------------------------------------------- 1 | VITE_AUTH_URL=http://auth.niffler.dc:9000 2 | VITE_API_URL=http://gateway.niffler.dc:8090 3 | VITE_FRONT_HOST=frontend.niffler.dc 4 | VITE_FRONT_URL=http://frontend.niffler.dc 5 | VITE_RESPONSE_TYPE=code 6 | VITE_CLIENT_ID=client 7 | -------------------------------------------------------------------------------- /niffler-ng-client/.env.prod: -------------------------------------------------------------------------------- 1 | VITE_AUTH_URL=https://auth.niffler.qa.guru 2 | VITE_API_URL=https://api.niffler.qa.guru 3 | VITE_FRONT_HOST=niffler.qa.guru 4 | VITE_FRONT_URL=https://niffler.qa.guru 5 | VITE_RESPONSE_TYPE=code 6 | VITE_CLIENT_ID=client 7 | -------------------------------------------------------------------------------- /niffler-ng-client/.env.staging: -------------------------------------------------------------------------------- 1 | VITE_AUTH_URL=https://auth.niffler-stage.qa.guru 2 | VITE_API_URL=https://api.niffler-stage.qa.guru 3 | VITE_FRONT_HOST=niffler-stage.qa.guru 4 | VITE_FRONT_URL=https://niffler-stage.qa.guru 5 | VITE_RESPONSE_TYPE=code 6 | VITE_CLIENT_ID=client 7 | -------------------------------------------------------------------------------- /niffler-ng-client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: {browser: true, es2020: true}, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | {allowConstantExport: true}, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /niffler-ng-client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | .vite 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /niffler-ng-client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22.6.0-alpine AS build 2 | ARG NPM_COMMAND 3 | WORKDIR /app 4 | COPY package.json ./ 5 | RUN npm install 6 | COPY . ./ 7 | RUN npm run ${NPM_COMMAND} 8 | 9 | # release step 10 | FROM nginx:1.27.1-alpine AS release 11 | LABEL maintainer="Irina Stiazhkina @irin_alexis" 12 | ENV TZ=Europe/Moscow 13 | COPY nginx.conf /etc/nginx/conf.d/default.conf 14 | COPY --from=build /app/dist /usr/share/nginx/html/ 15 | EXPOSE 80 16 | CMD ["nginx", "-g", "daemon off;"] 17 | -------------------------------------------------------------------------------- /niffler-ng-client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Niffler 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /niffler-ng-client/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | location / { 6 | root /usr/share/nginx/html; 7 | index index.html index.htm; 8 | try_files $uri /index.html; 9 | } 10 | 11 | error_page 500 502 503 504 /50x.html; 12 | location = /50x.html { 13 | root /usr/share/nginx/html; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /niffler-ng-client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-ng-client/public/favicon.ico -------------------------------------------------------------------------------- /niffler-ng-client/src/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-ng-client/src/App.css -------------------------------------------------------------------------------- /niffler-ng-client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import {AppContent} from "./components/AppContent"; 3 | import {SnackBarProvider} from "./context/SnackBarContext"; 4 | import {DialogProvider} from "./context/DialogContext.tsx"; 5 | 6 | 7 | const App = () => { 8 | 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ) 17 | } 18 | 19 | export default App; 20 | -------------------------------------------------------------------------------- /niffler-ng-client/src/assets/fonts/Inter-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-ng-client/src/assets/fonts/Inter-Regular.woff2 -------------------------------------------------------------------------------- /niffler-ng-client/src/assets/fonts/YoungSerif-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-ng-client/src/assets/fonts/YoungSerif-Regular.ttf -------------------------------------------------------------------------------- /niffler-ng-client/src/assets/fonts/YoungSerif-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-ng-client/src/assets/fonts/YoungSerif-Regular.woff -------------------------------------------------------------------------------- /niffler-ng-client/src/assets/fonts/YoungSerif-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-ng-client/src/assets/fonts/YoungSerif-Regular.woff2 -------------------------------------------------------------------------------- /niffler-ng-client/src/assets/icons/ic_add_friend.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /niffler-ng-client/src/assets/icons/ic_all.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /niffler-ng-client/src/assets/icons/ic_archive.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /niffler-ng-client/src/assets/icons/ic_cal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /niffler-ng-client/src/assets/icons/ic_check.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /niffler-ng-client/src/assets/icons/ic_cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /niffler-ng-client/src/assets/icons/ic_delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /niffler-ng-client/src/assets/icons/ic_edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /niffler-ng-client/src/assets/icons/ic_friends.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /niffler-ng-client/src/assets/icons/ic_menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /niffler-ng-client/src/assets/icons/ic_plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /niffler-ng-client/src/assets/icons/ic_signout.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /niffler-ng-client/src/assets/icons/ic_upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /niffler-ng-client/src/assets/icons/ic_user.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /niffler-ng-client/src/assets/images/niffler-with-a-coin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-ng-client/src/assets/images/niffler-with-a-coin.png -------------------------------------------------------------------------------- /niffler-ng-client/src/assets/images/niffler-with-a-coin2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-ng-client/src/assets/images/niffler-with-a-coin2.png -------------------------------------------------------------------------------- /niffler-ng-client/src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import MuiButton, {ButtonProps} from '@mui/material/Button'; 2 | import {styled} from '@mui/material/styles'; 3 | import {FC} from "react"; 4 | 5 | const Button = styled(MuiButton)(() => ({ 6 | padding: "12px 24px", 7 | borderRadius: "8px", 8 | textTransform: "none", 9 | weight: 600, 10 | fontSize: 16, 11 | lineHeight: 1.3, 12 | })); 13 | 14 | export const PrimaryButton: FC = (props) => { 15 | return 21 | : 30 | ); 31 | } -------------------------------------------------------------------------------- /niffler-ng-client/src/components/MenuAppBar/styles.css: -------------------------------------------------------------------------------- 1 | .link { 2 | text-decoration: none; 3 | display: flex; 4 | align-items: center; 5 | cursor: pointer; 6 | color: #000000; 7 | font: inherit; 8 | } 9 | 10 | .nav-link { 11 | width: 100%; 12 | } -------------------------------------------------------------------------------- /niffler-ng-client/src/components/TabPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import {Box} from "@mui/material"; 2 | import {FC} from "react"; 3 | 4 | interface TabPanelProps { 5 | children?: React.ReactNode; 6 | value: string; 7 | } 8 | 9 | export const TabPanel: FC = ({children, value}) => { 10 | 11 | return ( 12 |
17 | 18 | {children} 19 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /niffler-ng-client/src/components/Table/HeadCell/index.tsx: -------------------------------------------------------------------------------- 1 | export interface HeadCell { 2 | id: string; 3 | label: string; 4 | position: "right" | "left" | "center"; 5 | } -------------------------------------------------------------------------------- /niffler-ng-client/src/components/Table/TableToolbar/index.tsx: -------------------------------------------------------------------------------- 1 | import {Toolbar} from "@mui/material"; 2 | import {FC} from "react"; 3 | import {SearchInput} from "../../SearchInput"; 4 | 5 | interface TableToolbarProps { 6 | onSearchSubmit: (value: string) => void; 7 | } 8 | 9 | export const TableToolbar: FC = ({onSearchSubmit}) => { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /niffler-ng-client/src/components/Toggle/index.tsx: -------------------------------------------------------------------------------- 1 | import {ToggleButton, ToggleButtonGroup} from "@mui/material" 2 | import {FC} from "react"; 3 | 4 | interface ToggleInterface { 5 | withMyFriends: boolean, 6 | setWithMyFriends: (withMyFriends: boolean) => void; 7 | } 8 | 9 | export const Toggle: FC = ({withMyFriends, setWithMyFriends}) => { 10 | 11 | const handleChange = ( 12 | _event: React.MouseEvent, 13 | newFilter: "my" | "friends", 14 | ) => { 15 | setWithMyFriends(newFilter === "friends"); 16 | }; 17 | 18 | return ( 19 | 27 | Only my travels 28 | With friends 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /niffler-ng-client/src/const/constants.ts: -------------------------------------------------------------------------------- 1 | export const MAX_CATEGORIES_COUNT = 8; -------------------------------------------------------------------------------- /niffler-ng-client/src/context/SessionContext.tsx: -------------------------------------------------------------------------------- 1 | import {createContext, Dispatch, SetStateAction} from "react"; 2 | import {User} from "../types/User"; 3 | import {Statistic} from "../types/Statistic.ts"; 4 | 5 | interface SessionContextInterface { 6 | user: User; 7 | updateUser: Dispatch>; 8 | } 9 | 10 | export const USER_INITIAL_STATE: User = { 11 | id: "", 12 | username: "", 13 | fullname: "", 14 | photo: "", 15 | photoSmall: "", 16 | } 17 | 18 | export const STAT_INITIAL_STATE: Statistic = { 19 | total: 0.0, 20 | currency: "RUB", 21 | statByCategories: [], 22 | } 23 | 24 | const defaultState = { 25 | updateUser: () => { 26 | }, 27 | user: USER_INITIAL_STATE, 28 | statistic: STAT_INITIAL_STATE, 29 | }; 30 | 31 | export const SessionContext = createContext(defaultState); 32 | -------------------------------------------------------------------------------- /niffler-ng-client/src/hooks/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import {useEffect, useRef} from "react"; 2 | 3 | export function usePrevious(value: T) { 4 | const ref = useRef(); 5 | useEffect(() => { 6 | ref.current = value; 7 | }, [value]); 8 | return ref.current; 9 | } -------------------------------------------------------------------------------- /niffler-ng-client/src/index.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Young Serif"; 3 | src: url("./assets/fonts/YoungSerif-Regular.woff2") format("woff2"), 4 | url("./assets/fonts/YoungSerif-Regular.woff") format("woff"), 5 | url("./assets/fonts/YoungSerif-Regular.ttf") format("truetype"); 6 | font-weight: 400; 7 | font-style: normal; 8 | font-display: swap; 9 | } 10 | 11 | @font-face { 12 | font-family: "Inter"; 13 | src: url("./assets/fonts/Inter-Regular.woff2") format("woff2"); 14 | font-weight: 400; 15 | font-style: normal; 16 | font-display: swap; 17 | } 18 | 19 | * { 20 | font-family: Inter, sans-serif; 21 | margin: 0; 22 | padding: 0; 23 | box-sizing: border-box; 24 | } 25 | 26 | h1, h2, h3, h4, h5, h6 { 27 | font-family: "Young Serif", serif; 28 | } 29 | 30 | body { 31 | height: 100vh; 32 | font-family: Inter, sans-serif; 33 | } 34 | 35 | #root { 36 | height: 100%; 37 | } 38 | -------------------------------------------------------------------------------- /niffler-ng-client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | import App from './App.tsx'; 3 | import {CssBaseline, ThemeProvider} from '@mui/material'; 4 | import theme from './theme'; 5 | import './index.css'; 6 | import {StrictMode} from "react"; 7 | 8 | 9 | ReactDOM.createRoot(document.getElementById('root')!).render( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /niffler-ng-client/src/pages/NotFoundPage/index.tsx: -------------------------------------------------------------------------------- 1 | import {EmptyTableState} from "../../components/EmptyUsersState"; 2 | import {Container} from "@mui/material"; 3 | import {PrimaryButton} from "../../components/Button"; 4 | import {useNavigate} from "react-router-dom"; 5 | 6 | export const NotFoundPage = () => { 7 | const navigate = useNavigate(); 8 | return ( 9 | 10 | 11 | navigate("/main")} 14 | sx={{ 15 | display: "block", 16 | margin: "0 auto", 17 | }} 18 | > 19 | Go to homepage 20 | 21 | 22 | ); 23 | } -------------------------------------------------------------------------------- /niffler-ng-client/src/pages/ProfilePage/index.tsx: -------------------------------------------------------------------------------- 1 | import {Container} from "@mui/material" 2 | import {ProfileForm} from "../../components/ProfileForm" 3 | import {CategorySection} from "../../components/CategorySection"; 4 | 5 | export const ProfilePage = () => { 6 | 7 | return ( 8 | 9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /niffler-ng-client/src/pages/SpendingPage/index.tsx: -------------------------------------------------------------------------------- 1 | import {Container} from "@mui/material"; 2 | import {SpendingForm} from "../../components/SpendingForm"; 3 | import {useParams} from "react-router-dom"; 4 | 5 | export const SpendingPage = () => { 6 | const params = useParams(); 7 | return ( 8 | 9 | 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /niffler-ng-client/src/types/Category.ts: -------------------------------------------------------------------------------- 1 | export interface Category { 2 | id: string, 3 | name: string, 4 | username: string, 5 | archived: boolean, 6 | } 7 | -------------------------------------------------------------------------------- /niffler-ng-client/src/types/CategoryStatistic.ts: -------------------------------------------------------------------------------- 1 | export interface CategoryStatistic { 2 | categoryName: string, 3 | currency: string, 4 | sum: number, 5 | firstSpendDate: string, 6 | lastSpendDate: string, 7 | } 8 | -------------------------------------------------------------------------------- /niffler-ng-client/src/types/Country.ts: -------------------------------------------------------------------------------- 1 | export type Country = { 2 | code: string; 3 | flag: string; 4 | name: string; 5 | } 6 | -------------------------------------------------------------------------------- /niffler-ng-client/src/types/Currency.ts: -------------------------------------------------------------------------------- 1 | export interface Currency { 2 | currency: string, 3 | currencyRate?: number, 4 | } 5 | 6 | export type CurrencyValue = "RUB" | "KZT" | "USD" | "EUR" | "ALL"; 7 | 8 | export const convertCurrencyToData = (currency: Currency) => { 9 | switch (currency.currency) { 10 | case "RUB": 11 | case "KZT": 12 | case "EUR": 13 | case "USD": 14 | return currency.currency; 15 | default: 16 | return ""; 17 | } 18 | } 19 | 20 | export const getCurrencyIcon = (currency: CurrencyValue) => { 21 | switch (currency) { 22 | case "KZT": 23 | return "₸"; 24 | case "RUB": 25 | return "₽"; 26 | case "EUR": 27 | return "€"; 28 | case "USD": 29 | return "$"; 30 | case "ALL": 31 | return "⚖"; 32 | default: 33 | return ""; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /niffler-ng-client/src/types/DoughnutOptions.ts: -------------------------------------------------------------------------------- 1 | export interface DoughnutOptions { 2 | cutout: string; 3 | responsive: boolean; 4 | maintainAspectRatio: boolean; 5 | layout: Record, 6 | title: { 7 | display: boolean, 8 | text: string, 9 | currency: string, 10 | }, 11 | arc: { 12 | width: number, 13 | }, 14 | plugins: { 15 | legend: { 16 | display: boolean, 17 | }, 18 | htmlLegend: { 19 | containerID: string, 20 | }, 21 | }, 22 | tooltips: { 23 | enabled: boolean, 24 | }, 25 | events: [], 26 | } -------------------------------------------------------------------------------- /niffler-ng-client/src/types/Error.ts: -------------------------------------------------------------------------------- 1 | export class ApiError extends Error { 2 | public readonly detail: string; 3 | public readonly status: number; 4 | public readonly name = "ApiError"; 5 | 6 | constructor(detail: string, status: number) { 7 | super(detail); 8 | this.detail = detail; 9 | this.status = status; 10 | } 11 | } 12 | 13 | interface CommonError extends Error { 14 | message: string, 15 | name: "CommonError", 16 | } 17 | 18 | export function isCommonError(error: any): error is CommonError { 19 | return "message" in error; 20 | } 21 | 22 | export function isApiError(error: any): error is ApiError { 23 | return "detail" in error; 24 | } -------------------------------------------------------------------------------- /niffler-ng-client/src/types/FilterPeriod.ts: -------------------------------------------------------------------------------- 1 | export const filterPeriod = [ 2 | { 3 | value: "ALL", 4 | label: "All time", 5 | }, 6 | { 7 | value: "MONTH", 8 | label: "Last month", 9 | }, 10 | { 11 | value: "WEEK", 12 | label: "Last week", 13 | }, 14 | { 15 | value: "TODAY", 16 | label: "Today", 17 | }, 18 | ] as const; 19 | 20 | export type FilterPeriodValue = typeof filterPeriod[number]; 21 | 22 | export const convertValueToFilterPeriodValue = (value: string): FilterPeriodValue => { 23 | const res = filterPeriod.find(period => period.value === value); 24 | if (res) { 25 | return res 26 | } else { 27 | throw Error("Bad period value"); 28 | } 29 | } -------------------------------------------------------------------------------- /niffler-ng-client/src/types/FriendshipStatus.ts: -------------------------------------------------------------------------------- 1 | export type FriendshipStatus = "FRIEND" | "INVITE_SENT" | "INVITE_RECEIVED" | undefined; -------------------------------------------------------------------------------- /niffler-ng-client/src/types/IStringIndex.ts: -------------------------------------------------------------------------------- 1 | export interface IStringIndex extends Record { 2 | } 3 | -------------------------------------------------------------------------------- /niffler-ng-client/src/types/Likes.ts: -------------------------------------------------------------------------------- 1 | export type Likes = { 2 | total: number; 3 | likes: UserId[]; 4 | } 5 | 6 | type UserId = { 7 | user: string, 8 | } 9 | -------------------------------------------------------------------------------- /niffler-ng-client/src/types/Order.ts: -------------------------------------------------------------------------------- 1 | export type Order = 'asc' | 'desc'; 2 | -------------------------------------------------------------------------------- /niffler-ng-client/src/types/Pageable.ts: -------------------------------------------------------------------------------- 1 | interface Pageable { 2 | content: T[], 3 | number: number, 4 | empty: boolean, 5 | first: boolean, 6 | last: boolean, 7 | numberOfElements: number, 8 | } 9 | -------------------------------------------------------------------------------- /niffler-ng-client/src/types/Photo.ts: -------------------------------------------------------------------------------- 1 | import {Country} from "./Country"; 2 | import {Likes} from "./Likes"; 3 | 4 | export type Photo = { 5 | id: string; 6 | src: string; 7 | country: Country; 8 | description: string; 9 | likes: Likes; 10 | } 11 | -------------------------------------------------------------------------------- /niffler-ng-client/src/types/RequestHandler.ts: -------------------------------------------------------------------------------- 1 | export interface RequestHandler { 2 | onSuccess: (data: T) => void, 3 | onFailure: (e: Error) => void, 4 | } 5 | -------------------------------------------------------------------------------- /niffler-ng-client/src/types/Session.ts: -------------------------------------------------------------------------------- 1 | export interface Session { 2 | username: string | null, 3 | expiresAt: string | null, 4 | issuedAt: string | null, 5 | } 6 | -------------------------------------------------------------------------------- /niffler-ng-client/src/types/Spending.ts: -------------------------------------------------------------------------------- 1 | import {Category} from "./Category.ts"; 2 | 3 | export interface Spending { 4 | id: string, 5 | amount: number, 6 | category: Category, 7 | currency: string, 8 | description: string, 9 | spendDate: string, 10 | } 11 | 12 | export type TableSpending = Omit & { 13 | amount: string, 14 | }; 15 | -------------------------------------------------------------------------------- /niffler-ng-client/src/types/Statistic.ts: -------------------------------------------------------------------------------- 1 | import {CategoryStatistic} from "./CategoryStatistic.ts"; 2 | 3 | export interface Statistic { 4 | total: number, 5 | currency: string, 6 | statByCategories: CategoryStatistic[], 7 | } 8 | -------------------------------------------------------------------------------- /niffler-ng-client/src/types/User.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: string; 3 | username: string; 4 | fullname?: string; 5 | photo?: string; 6 | photoSmall?: string; 7 | friendshipStatus?: "FRIEND" | "INVITE_SENT" | "INVITE_RECEIVED"; 8 | } 9 | -------------------------------------------------------------------------------- /niffler-ng-client/src/types/Void.ts: -------------------------------------------------------------------------------- 1 | export interface Void { 2 | } 3 | -------------------------------------------------------------------------------- /niffler-ng-client/src/utils/arrays.ts: -------------------------------------------------------------------------------- 1 | export function stableSort(array: readonly T[], comparator: (a: T, b: T) => number) { 2 | const stabilizedThis = array.map((el, index) => [el, index] as [T, number]); 3 | stabilizedThis.sort((a, b) => { 4 | const order = comparator(a[0], b[0]); 5 | if (order !== 0) { 6 | return order; 7 | } 8 | return a[1] - b[1]; 9 | }); 10 | return stabilizedThis.map((el) => el[0]); 11 | } -------------------------------------------------------------------------------- /niffler-ng-client/src/utils/comparator.ts: -------------------------------------------------------------------------------- 1 | import {Order} from "../types/Order"; 2 | 3 | function descendingComparator(a: T, b: T, orderBy: keyof T) { 4 | if (b[orderBy] < a[orderBy]) { 5 | return -1; 6 | } 7 | if (b[orderBy] > a[orderBy]) { 8 | return 1; 9 | } 10 | return 0; 11 | } 12 | 13 | export function getComparator( 14 | order: Order, 15 | orderBy: Key, 16 | ): ( 17 | a: { [key in Key]: number | string }, 18 | b: { [key in Key]: number | string }, 19 | ) => number { 20 | return order === 'desc' 21 | ? (a, b) => descendingComparator(a, b, orderBy) 22 | : (a, b) => -descendingComparator(a, b, orderBy); 23 | } -------------------------------------------------------------------------------- /niffler-ng-client/src/utils/dataConverter.ts: -------------------------------------------------------------------------------- 1 | import {FilterPeriodValue} from "../types/FilterPeriod.ts"; 2 | 3 | export const convertFilterPeriod = (period: FilterPeriodValue) => { 4 | switch (period.value) { 5 | case "TODAY": 6 | case "WEEK": 7 | case "MONTH": 8 | return period.value; 9 | default: 10 | return ""; 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /niffler-ng-client/src/utils/date.ts: -------------------------------------------------------------------------------- 1 | export const convertDate = (toConvert: string) => { 2 | return new Date(toConvert).toLocaleDateString('en-US', 3 | {year: 'numeric', month: 'short', day: '2-digit'}); 4 | }; -------------------------------------------------------------------------------- /niffler-ng-client/src/utils/form.ts: -------------------------------------------------------------------------------- 1 | export const formHasErrors = (formValues: Record) => { 2 | const keys = Object.keys(formValues); 3 | return keys.some((key) => formValues[key].error === true); 4 | }; 5 | -------------------------------------------------------------------------------- /niffler-ng-client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// -------------------------------------------------------------------------------- /niffler-ng-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "ES2020", 7 | "DOM", 8 | "DOM.Iterable" 9 | ], 10 | "module": "ESNext", 11 | "skipLibCheck": true, 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true 24 | }, 25 | "include": [ 26 | "src" 27 | ], 28 | "references": [ 29 | { 30 | "path": "./tsconfig.node.json" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /niffler-ng-client/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": [ 10 | "vite.config.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /niffler-ng-client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig, loadEnv} from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import svgr from "vite-plugin-svgr"; 4 | 5 | 6 | export default defineConfig(({mode}) => { 7 | process.env = {...process.env, ...loadEnv(mode, process.cwd())}; 8 | 9 | return defineConfig({ 10 | plugins: [svgr(), react()], 11 | server: { 12 | host: process.env.VITE_FRONT_HOST, 13 | port: 3000, 14 | }, 15 | preview: { 16 | host: process.env.VITE_FRONT_HOST, 17 | port: 3000, 18 | strictPort: true, 19 | }, 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vite 3 | dist 4 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/.env: -------------------------------------------------------------------------------- 1 | VITE_AUTH_URL=http://127.0.0.1:9000 2 | VITE_API_URL=http://127.0.0.1:8090 3 | VITE_FRONT_HOST=127.0.0.1 4 | VITE_FRONT_URL=http://127.0.0.1:3000 5 | VITE_RESPONSE_TYPE=code 6 | VITE_CLIENT_ID=client 7 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/.env.docker: -------------------------------------------------------------------------------- 1 | VITE_AUTH_URL=http://auth.niffler.dc:9000 2 | VITE_API_URL=http://gateway.niffler.dc:8090 3 | VITE_FRONT_HOST=frontend.niffler.dc 4 | VITE_FRONT_URL=http://frontend.niffler.dc 5 | VITE_RESPONSE_TYPE=code 6 | VITE_CLIENT_ID=client 7 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/.env.prod: -------------------------------------------------------------------------------- 1 | VITE_AUTH_URL=https://auth.niffler.qa.guru 2 | VITE_API_URL=https://api.niffler.qa.guru 3 | VITE_FRONT_HOST=niffler.qa.guru 4 | VITE_FRONT_URL=https://niffler.qa.guru 5 | VITE_RESPONSE_TYPE=code 6 | VITE_CLIENT_ID=client 7 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/.env.staging: -------------------------------------------------------------------------------- 1 | VITE_AUTH_URL=https://auth.niffler-stage.qa.guru 2 | VITE_API_URL=https://api.niffler-stage.qa.guru 3 | VITE_FRONT_HOST=niffler-stage.qa.guru 4 | VITE_FRONT_URL=https://niffler-stage.qa.guru 5 | VITE_RESPONSE_TYPE=code 6 | VITE_CLIENT_ID=client 7 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: {browser: true, es2020: true}, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | {allowConstantExport: true}, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | .vite 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22.6.0-alpine AS build 2 | ARG NPM_COMMAND 3 | ARG VERSION 4 | WORKDIR /app 5 | COPY package.json ./ 6 | RUN npm install 7 | COPY . ./ 8 | RUN npm run ${NPM_COMMAND} 9 | 10 | # release step 11 | FROM nginx:1.27.1-alpine AS release 12 | LABEL maintainer="Irina Stiazhkina @irin_alexis" 13 | LABEL version=${VERSION} 14 | ENV TZ=Europe/Moscow 15 | COPY nginx.conf /etc/nginx/conf.d/default.conf 16 | COPY --from=build /app/dist /usr/share/nginx/html/ 17 | EXPOSE 80 18 | CMD ["nginx", "-g", "daemon off;"] 19 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/codegen.yml: -------------------------------------------------------------------------------- 1 | schema: "http://localhost:8090/graphql" 2 | documents: "src/**/*.graphql" 3 | generates: 4 | src/generated/graphql.tsx: 5 | plugins: 6 | - "typescript" 7 | - "typescript-operations" 8 | - "typescript-react-apollo" 9 | config: 10 | withHooks: true 11 | withHOC: false 12 | withComponent: false -------------------------------------------------------------------------------- /niffler-ng-gql-client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Niffler 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | location / { 6 | root /usr/share/nginx/html; 7 | index index.html index.htm; 8 | try_files $uri /index.html; 9 | } 10 | 11 | error_page 500 502 503 504 /50x.html; 12 | location = /50x.html { 13 | root /usr/share/nginx/html; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-ng-gql-client/public/favicon.ico -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-ng-gql-client/src/App.css -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import {AppContent} from "./components/AppContent"; 3 | import {SnackBarProvider} from "./context/SnackBarContext"; 4 | import {DialogProvider} from "./context/DialogContext.tsx"; 5 | 6 | 7 | const App = () => { 8 | 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ) 17 | } 18 | 19 | export default App; 20 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/api/graphqlClient.ts: -------------------------------------------------------------------------------- 1 | import {ApolloClient, createHttpLink, InMemoryCache} from "@apollo/client"; 2 | import {setContext} from "@apollo/client/link/context"; 3 | 4 | 5 | const BASE_URL = `${import.meta.env.VITE_API_URL}`; 6 | 7 | const apolloHttpLink = createHttpLink({ 8 | uri: `${BASE_URL}/graphql`, 9 | }) 10 | 11 | const headerLink = setContext((_request, previousContext) => ({ 12 | headers: { 13 | ...previousContext.headers, 14 | "Authorization": localStorage.getItem("id_token") ? `Bearer ${localStorage.getItem("id_token")}` : "", 15 | }, 16 | })); 17 | 18 | 19 | const client = new ApolloClient({ 20 | link: headerLink.concat(apolloHttpLink), 21 | cache: new InMemoryCache(), 22 | }); 23 | 24 | 25 | export default client; -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/api/mutations/category.graphql: -------------------------------------------------------------------------------- 1 | mutation UpdateCategory($input: CategoryInput!) { 2 | category(input: $input) { 3 | id 4 | name 5 | archived 6 | } 7 | } -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/api/mutations/spend.graphql: -------------------------------------------------------------------------------- 1 | mutation UpdateSpend($input: SpendInput!) { 2 | spend(input: $input) { 3 | id 4 | spendDate 5 | category { 6 | id 7 | name 8 | archived 9 | } 10 | currency 11 | amount 12 | description 13 | } 14 | } 15 | 16 | mutation DeleteSpends($ids: [ID!]!) { 17 | deleteSpend(ids: $ids) 18 | } -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/api/mutations/user.graphql: -------------------------------------------------------------------------------- 1 | mutation UpdateUser($input: UserInput!) { 2 | user(input: $input) { 3 | id 4 | username 5 | fullname 6 | photo 7 | } 8 | } 9 | 10 | mutation UpdateFriendshipStatus($input: FriendshipInput!) { 11 | friendship(input: $input) { 12 | id 13 | username 14 | fullname 15 | photo 16 | } 17 | } -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/api/queries/currency.graphql: -------------------------------------------------------------------------------- 1 | query Currencies { 2 | currencies { 3 | currency 4 | } 5 | } -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/api/queries/session.graphql: -------------------------------------------------------------------------------- 1 | query Session { 2 | session { 3 | username 4 | } 5 | } -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/api/queries/spend.graphql: -------------------------------------------------------------------------------- 1 | query Spends($page:Int!, $size:Int!, $sort: [String!], $searchQuery:String, $filterPeriod: FilterPeriod, $filterCurrency: CurrencyValues) { 2 | spends(page: $page, size: $size, sort: $sort, searchQuery: $searchQuery, filterPeriod: $filterPeriod, filterCurrency: $filterCurrency) { 3 | edges { 4 | node { 5 | id 6 | spendDate 7 | category { 8 | id 9 | name 10 | } 11 | currency 12 | amount 13 | description 14 | } 15 | } 16 | pageInfo { 17 | hasPreviousPage 18 | hasNextPage 19 | } 20 | } 21 | } 22 | 23 | query Spend($id: ID!) { 24 | spend(id: $id) { 25 | id 26 | spendDate 27 | category { 28 | name 29 | } 30 | currency 31 | amount 32 | description 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/api/queries/stat.graphql: -------------------------------------------------------------------------------- 1 | query Stat($filterPeriod: FilterPeriod, $filterCurrency: CurrencyValues, $statCurrency: CurrencyValues){ 2 | stat(filterPeriod: $filterPeriod, filterCurrency: $filterCurrency, statCurrency: $statCurrency) { 3 | total 4 | currency 5 | statByCategories { 6 | categoryName 7 | currency 8 | sum 9 | firstSpendDate 10 | lastSpendDate 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/assets/fonts/Inter-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-ng-gql-client/src/assets/fonts/Inter-Regular.woff2 -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/assets/fonts/YoungSerif-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-ng-gql-client/src/assets/fonts/YoungSerif-Regular.ttf -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/assets/fonts/YoungSerif-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-ng-gql-client/src/assets/fonts/YoungSerif-Regular.woff -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/assets/fonts/YoungSerif-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-ng-gql-client/src/assets/fonts/YoungSerif-Regular.woff2 -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/assets/icons/ic_add_friend.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/assets/icons/ic_all.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/assets/icons/ic_archive.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/assets/icons/ic_cal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/assets/icons/ic_check.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/assets/icons/ic_cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/assets/icons/ic_delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/assets/icons/ic_edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/assets/icons/ic_friends.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/assets/icons/ic_menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/assets/icons/ic_plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/assets/icons/ic_signout.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/assets/icons/ic_upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/assets/icons/ic_user.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/assets/images/niffler-with-a-coin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-ng-gql-client/src/assets/images/niffler-with-a-coin.png -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/assets/images/niffler-with-a-coin2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qa-guru/niffler-ng-6/84b576967fe7340cf7c52233e3a969d554b7b025/niffler-ng-gql-client/src/assets/images/niffler-with-a-coin2.png -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import MuiButton, {ButtonProps} from '@mui/material/Button'; 2 | import {styled} from '@mui/material/styles'; 3 | import {FC} from "react"; 4 | 5 | const Button = styled(MuiButton)(() => ({ 6 | padding: "12px 24px", 7 | borderRadius: "8px", 8 | textTransform: "none", 9 | weight: 600, 10 | fontSize: 16, 11 | lineHeight: 1.3, 12 | })); 13 | 14 | export const PrimaryButton: FC = (props) => { 15 | return 21 | : 30 | ); 31 | } -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/components/MenuAppBar/styles.css: -------------------------------------------------------------------------------- 1 | .link { 2 | text-decoration: none; 3 | display: flex; 4 | align-items: center; 5 | cursor: pointer; 6 | color: #000000; 7 | font: inherit; 8 | } 9 | 10 | .nav-link { 11 | width: 100%; 12 | } -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/components/SpendingForm/AddFormComponent/index.tsx: -------------------------------------------------------------------------------- 1 | import {FormComponent} from "../FormComponent"; 2 | import {SPENDING_INITIAL_STATE} from "../formValidate.ts"; 3 | 4 | 5 | export const AddFormComponent = () => { 6 | 7 | return ( 8 | 15 | ) 16 | } -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/components/SpendingForm/EditFormComponent/index.tsx: -------------------------------------------------------------------------------- 1 | import {FormComponent} from "../FormComponent"; 2 | import {useSpendQuery} from "../../../generated/graphql.tsx"; 3 | import {FC} from "react"; 4 | 5 | interface EditFormComponentInterface { 6 | id: string; 7 | } 8 | 9 | export const EditFormComponent: FC = ({id}) => { 10 | const {data} = useSpendQuery({ 11 | variables: { 12 | id 13 | } 14 | }); 15 | 16 | return ( 17 | data?.spend ? 18 | 25 | : 26 | <> 27 | ) 28 | } -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/components/SpendingForm/index.tsx: -------------------------------------------------------------------------------- 1 | import {FC} from "react"; 2 | import {EditFormComponent} from "./EditFormComponent"; 3 | import {AddFormComponent} from "./AddFormComponent"; 4 | 5 | 6 | interface SpendingFormInterface { 7 | id?: string, 8 | isEdit: boolean, 9 | } 10 | 11 | export const SpendingForm: FC = ({id, isEdit}) => { 12 | 13 | return ( 14 | isEdit && id ? 15 | 16 | : 17 | ) 18 | 19 | } 20 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/components/TabPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import {Box} from "@mui/material"; 2 | import {FC} from "react"; 3 | 4 | interface TabPanelProps { 5 | children?: React.ReactNode; 6 | value: string; 7 | } 8 | 9 | export const TabPanel: FC = ({children, value}) => { 10 | 11 | return ( 12 |
17 | 18 | {children} 19 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/components/Table/HeadCell/index.tsx: -------------------------------------------------------------------------------- 1 | export interface HeadCell { 2 | id: string; 3 | label: string; 4 | position: "right" | "left" | "center"; 5 | } -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/components/Table/TableToolbar/index.tsx: -------------------------------------------------------------------------------- 1 | import {Toolbar} from "@mui/material"; 2 | import {FC} from "react"; 3 | import {SearchInput} from "../../SearchInput"; 4 | 5 | interface TableToolbarProps { 6 | onSearchSubmit: (value: string) => void; 7 | } 8 | 9 | export const TableToolbar: FC = ({onSearchSubmit}) => { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/components/Toggle/index.tsx: -------------------------------------------------------------------------------- 1 | import {ToggleButton, ToggleButtonGroup} from "@mui/material" 2 | import {FC} from "react"; 3 | 4 | interface ToggleInterface { 5 | withMyFriends: boolean, 6 | setWithMyFriends: (withMyFriends: boolean) => void; 7 | } 8 | 9 | export const Toggle: FC = ({withMyFriends, setWithMyFriends}) => { 10 | 11 | const handleChange = ( 12 | _event: React.MouseEvent, 13 | newFilter: "my" | "friends", 14 | ) => { 15 | setWithMyFriends(newFilter === "friends"); 16 | }; 17 | 18 | return ( 19 | 27 | Only my travels 28 | With friends 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/const/constants.ts: -------------------------------------------------------------------------------- 1 | export const MAX_CATEGORIES_COUNT = 8; -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/hooks/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import {useEffect, useRef} from "react"; 2 | 3 | export function usePrevious(value: T) { 4 | const ref = useRef(); 5 | useEffect(() => { 6 | ref.current = value; 7 | }, [value]); 8 | return ref.current; 9 | } -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/index.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Young Serif"; 3 | src: url("./assets/fonts/YoungSerif-Regular.woff2") format("woff2"), 4 | url("./assets/fonts/YoungSerif-Regular.woff") format("woff"), 5 | url("./assets/fonts/YoungSerif-Regular.ttf") format("truetype"); 6 | font-weight: 400; 7 | font-style: normal; 8 | font-display: swap; 9 | } 10 | 11 | @font-face { 12 | font-family: "Inter"; 13 | src: url("./assets/fonts/Inter-Regular.woff2") format("woff2"); 14 | font-weight: 400; 15 | font-style: normal; 16 | font-display: swap; 17 | } 18 | 19 | * { 20 | font-family: Inter, sans-serif; 21 | margin: 0; 22 | padding: 0; 23 | box-sizing: border-box; 24 | } 25 | 26 | h1, h2, h3, h4, h5, h6 { 27 | font-family: "Young Serif", serif; 28 | } 29 | 30 | body { 31 | height: 100vh; 32 | font-family: Inter, sans-serif; 33 | } 34 | 35 | #root { 36 | height: 100%; 37 | } 38 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | import App from './App.tsx'; 3 | import {CssBaseline, ThemeProvider} from '@mui/material'; 4 | import theme from './theme'; 5 | import './index.css'; 6 | import {StrictMode} from "react"; 7 | import {ApolloProvider} from "@apollo/client"; 8 | import client from "./api/graphqlClient.ts"; 9 | 10 | 11 | ReactDOM.createRoot(document.getElementById('root')!).render( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/pages/NotFoundPage/index.tsx: -------------------------------------------------------------------------------- 1 | import {EmptyTableState} from "../../components/EmptyUsersState"; 2 | import {Container} from "@mui/material"; 3 | import {PrimaryButton} from "../../components/Button"; 4 | import {useNavigate} from "react-router-dom"; 5 | 6 | export const NotFoundPage = () => { 7 | const navigate = useNavigate(); 8 | return ( 9 | 10 | 11 | navigate("/main")} 14 | sx={{ 15 | display: "block", 16 | margin: "0 auto", 17 | }} 18 | > 19 | Go to homepage 20 | 21 | 22 | ); 23 | } -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/pages/SpendingPage/index.tsx: -------------------------------------------------------------------------------- 1 | import {Container} from "@mui/material"; 2 | import {SpendingForm} from "../../components/SpendingForm"; 3 | import {useParams} from "react-router-dom"; 4 | 5 | export const SpendingPage = () => { 6 | const params = useParams(); 7 | return ( 8 | 9 | 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/types/Category.ts: -------------------------------------------------------------------------------- 1 | import {Category as FullCategory, CurrentUserQuery} from "../generated/graphql.tsx"; 2 | 3 | export type Categories = CurrentUserQuery["user"]["categories"]; 4 | export type Category = Omit; 5 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/types/CategoryStatistic.ts: -------------------------------------------------------------------------------- 1 | export interface CategoryStatistic { 2 | categoryName: string, 3 | currency: string, 4 | sum: number, 5 | firstSpendDate: string, 6 | lastSpendDate: string, 7 | } 8 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/types/Country.ts: -------------------------------------------------------------------------------- 1 | export type Country = { 2 | code: string; 3 | flag: string; 4 | name: string; 5 | } 6 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/types/Currency.ts: -------------------------------------------------------------------------------- 1 | import {CurrencyValues} from "../generated/graphql.tsx"; 2 | 3 | export interface Currency { 4 | currency: string, 5 | currencyRate?: number, 6 | } 7 | 8 | export type CurrencyValue = "RUB" | "KZT" | "USD" | "EUR" | "ALL"; 9 | 10 | export const convertCurrencyToData = (currency: Currency) => { 11 | switch (currency.currency) { 12 | case "RUB": 13 | return CurrencyValues.Rub; 14 | case "KZT": 15 | return CurrencyValues.Kzt; 16 | case "EUR": 17 | return CurrencyValues.Eur; 18 | case "USD": 19 | return CurrencyValues.Usd 20 | default: 21 | return; 22 | } 23 | } 24 | 25 | export const getCurrencyIcon = (currency: CurrencyValue) => { 26 | switch (currency) { 27 | case "KZT": 28 | return "₸"; 29 | case "RUB": 30 | return "₽"; 31 | case "EUR": 32 | return "€"; 33 | case "USD": 34 | return "$"; 35 | case "ALL": 36 | return "⚖"; 37 | default: 38 | return ""; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/types/DoughnutOptions.ts: -------------------------------------------------------------------------------- 1 | export interface DoughnutOptions { 2 | cutout: string; 3 | responsive: boolean; 4 | maintainAspectRatio: boolean; 5 | layout: Record, 6 | title: { 7 | display: boolean, 8 | text: string, 9 | currency: string, 10 | }, 11 | arc: { 12 | width: number, 13 | }, 14 | plugins: { 15 | legend: { 16 | display: boolean, 17 | }, 18 | htmlLegend: { 19 | containerID: string, 20 | }, 21 | }, 22 | tooltips: { 23 | enabled: boolean, 24 | }, 25 | events: [], 26 | } -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/types/Error.ts: -------------------------------------------------------------------------------- 1 | export class ApiError extends Error { 2 | public readonly detail: string; 3 | public readonly status: number; 4 | public readonly name = "ApiError"; 5 | 6 | constructor(detail: string, status: number) { 7 | super(detail); 8 | this.detail = detail; 9 | this.status = status; 10 | } 11 | } 12 | 13 | interface CommonError extends Error { 14 | message: string, 15 | name: "CommonError", 16 | } 17 | 18 | export function isCommonError(error: any): error is CommonError { 19 | return "message" in error; 20 | } 21 | 22 | export function isApiError(error: any): error is ApiError { 23 | return "detail" in error; 24 | } -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/types/FilterPeriod.ts: -------------------------------------------------------------------------------- 1 | export const filterPeriod = [ 2 | { 3 | value: "ALL", 4 | label: "All time", 5 | }, 6 | { 7 | value: "MONTH", 8 | label: "Last month", 9 | }, 10 | { 11 | value: "WEEK", 12 | label: "Last week", 13 | }, 14 | { 15 | value: "TODAY", 16 | label: "Today", 17 | }, 18 | ] as const; 19 | 20 | export type FilterPeriodValue = typeof filterPeriod[number]; 21 | 22 | export const convertValueToFilterPeriodValue = (value: string): FilterPeriodValue => { 23 | const res = filterPeriod.find(period => period.value === value); 24 | if (res) { 25 | return res 26 | } else { 27 | throw Error("Bad period value"); 28 | } 29 | } -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/types/FriendshipStatus.ts: -------------------------------------------------------------------------------- 1 | export type FriendshipStatus = "FRIEND" | "INVITE_SENT" | "INVITE_RECEIVED" | undefined; -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/types/IStringIndex.ts: -------------------------------------------------------------------------------- 1 | export interface IStringIndex extends Record { 2 | } 3 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/types/Likes.ts: -------------------------------------------------------------------------------- 1 | export type Likes = { 2 | total: number; 3 | likes: UserId[]; 4 | } 5 | 6 | type UserId = { 7 | user: string, 8 | } 9 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/types/Order.ts: -------------------------------------------------------------------------------- 1 | export type Order = 'asc' | 'desc'; 2 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/types/Photo.ts: -------------------------------------------------------------------------------- 1 | import {Country} from "./Country"; 2 | import {Likes} from "./Likes"; 3 | 4 | export type Photo = { 5 | id: string; 6 | src: string; 7 | country: Country; 8 | description: string; 9 | likes: Likes; 10 | } 11 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/types/RequestHandler.ts: -------------------------------------------------------------------------------- 1 | export interface RequestHandler { 2 | onSuccess: (data: T) => void, 3 | onFailure: (e: Error) => void, 4 | } 5 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/types/Session.ts: -------------------------------------------------------------------------------- 1 | export interface Session { 2 | username: string | null, 3 | expiresAt: string | null, 4 | issuedAt: string | null, 5 | } 6 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/types/Spending.ts: -------------------------------------------------------------------------------- 1 | import {Spend} from "../generated/graphql.tsx"; 2 | import {Category} from "./Category.ts"; 3 | 4 | export interface Spending { 5 | id: string, 6 | amount: number, 7 | category: Category, 8 | currency: string, 9 | description: string, 10 | spendDate: string, 11 | } 12 | 13 | export type SpendingData = Omit & { 14 | id?: string, 15 | category: { 16 | name: string, 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/types/Statistic.ts: -------------------------------------------------------------------------------- 1 | import {CategoryStatistic} from "./CategoryStatistic.ts"; 2 | 3 | export interface Statistic { 4 | total: number, 5 | currency: string, 6 | statByCategories: CategoryStatistic[], 7 | } 8 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/types/User.ts: -------------------------------------------------------------------------------- 1 | import {CurrentUserQuery, FriendshipStatus} from "../generated/graphql.tsx"; 2 | 3 | export type User = CurrentUserQuery["user"]; 4 | 5 | 6 | export type TableUser = { 7 | id: string; 8 | username: string; 9 | photoSmall?: string | null; 10 | fullname?: string | null; 11 | friendshipStatus?: FriendshipStatus | null | undefined; 12 | } 13 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/types/Void.ts: -------------------------------------------------------------------------------- 1 | export interface Void { 2 | } 3 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/utils/arrays.ts: -------------------------------------------------------------------------------- 1 | export function stableSort(array: readonly T[], comparator: (a: T, b: T) => number) { 2 | const stabilizedThis = array.map((el, index) => [el, index] as [T, number]); 3 | stabilizedThis.sort((a, b) => { 4 | const order = comparator(a[0], b[0]); 5 | if (order !== 0) { 6 | return order; 7 | } 8 | return a[1] - b[1]; 9 | }); 10 | return stabilizedThis.map((el) => el[0]); 11 | } -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/utils/comparator.ts: -------------------------------------------------------------------------------- 1 | import {Order} from "../types/Order"; 2 | 3 | function descendingComparator(a: T, b: T, orderBy: keyof T) { 4 | if (b[orderBy] < a[orderBy]) { 5 | return -1; 6 | } 7 | if (b[orderBy] > a[orderBy]) { 8 | return 1; 9 | } 10 | return 0; 11 | } 12 | 13 | export function getComparator( 14 | order: Order, 15 | orderBy: Key, 16 | ): ( 17 | a: { [key in Key]: number | string }, 18 | b: { [key in Key]: number | string }, 19 | ) => number { 20 | return order === 'desc' 21 | ? (a, b) => descendingComparator(a, b, orderBy) 22 | : (a, b) => -descendingComparator(a, b, orderBy); 23 | } -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/utils/dataConverter.ts: -------------------------------------------------------------------------------- 1 | import {FilterPeriodValue} from "../types/FilterPeriod.ts"; 2 | import {FilterPeriod} from "../generated/graphql.tsx"; 3 | 4 | export const convertFilterPeriod = (period: FilterPeriodValue) => { 5 | switch (period.value) { 6 | case "TODAY": 7 | return FilterPeriod.Today 8 | case "WEEK": 9 | return FilterPeriod.Week 10 | case "MONTH": 11 | return FilterPeriod.Month; 12 | default: 13 | return; 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/utils/date.ts: -------------------------------------------------------------------------------- 1 | export const convertDate = (toConvert: string) => { 2 | return new Date(toConvert).toLocaleDateString('en-US', 3 | {year: 'numeric', month: 'short', day: '2-digit'}); 4 | }; -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/utils/form.ts: -------------------------------------------------------------------------------- 1 | export const formHasErrors = (formValues: Record) => { 2 | const keys = Object.keys(formValues); 3 | return keys.some((key) => formValues[key].error === true); 4 | }; 5 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// -------------------------------------------------------------------------------- /niffler-ng-gql-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "ES2020", 7 | "DOM", 8 | "DOM.Iterable" 9 | ], 10 | "module": "ESNext", 11 | "skipLibCheck": true, 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true 24 | }, 25 | "include": [ 26 | "src" 27 | ], 28 | "references": [ 29 | { 30 | "path": "./tsconfig.node.json" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": [ 10 | "vite.config.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /niffler-ng-gql-client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig, loadEnv} from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import svgr from "vite-plugin-svgr"; 4 | 5 | 6 | export default defineConfig(({mode}) => { 7 | process.env = {...process.env, ...loadEnv(mode, process.cwd())}; 8 | 9 | return defineConfig({ 10 | plugins: [svgr(), react()], 11 | server: { 12 | host: process.env.VITE_FRONT_HOST, 13 | port: 3000, 14 | }, 15 | preview: { 16 | host: process.env.VITE_FRONT_HOST, 17 | port: 3000, 18 | strictPort: true, 19 | }, 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /niffler-spend/src/main/java/guru/qa/niffler/NifflerSpendApplication.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler; 2 | 3 | import guru.qa.niffler.service.PropertiesLogger; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | @SpringBootApplication 8 | public class NifflerSpendApplication { 9 | 10 | public static void main(String[] args) { 11 | SpringApplication springApplication = new SpringApplication(NifflerSpendApplication.class); 12 | springApplication.addListeners(new PropertiesLogger()); 13 | springApplication.run(args); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /niffler-spend/src/main/java/guru/qa/niffler/data/projection/SumByCategory.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data.projection; 2 | 3 | import guru.qa.niffler.model.CurrencyValues; 4 | 5 | import java.util.Date; 6 | 7 | public record SumByCategory(String categoryName, 8 | CurrencyValues currency, 9 | double sum, 10 | Date firstSpendDate, 11 | Date lastSpendDate) implements SumByCategoryInfo { 12 | } 13 | -------------------------------------------------------------------------------- /niffler-spend/src/main/java/guru/qa/niffler/data/projection/SumByCategoryInfo.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data.projection; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import guru.qa.niffler.model.CurrencyValues; 5 | 6 | import java.util.Date; 7 | 8 | public interface SumByCategoryInfo { 9 | @JsonProperty("categoryName") 10 | String categoryName(); 11 | 12 | @JsonProperty("currency") 13 | CurrencyValues currency(); 14 | 15 | @JsonProperty("sum") 16 | double sum(); 17 | 18 | @JsonProperty("firstSpendDate") 19 | Date firstSpendDate(); 20 | 21 | @JsonProperty("lastSpendDate") 22 | Date lastSpendDate(); 23 | } 24 | -------------------------------------------------------------------------------- /niffler-spend/src/main/java/guru/qa/niffler/data/repository/CategoryRepository.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data.repository; 2 | 3 | import guru.qa.niffler.data.CategoryEntity; 4 | import jakarta.annotation.Nonnull; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.data.jpa.repository.Query; 7 | 8 | import java.util.List; 9 | import java.util.Optional; 10 | import java.util.UUID; 11 | 12 | public interface CategoryRepository extends JpaRepository { 13 | 14 | @Nonnull 15 | Optional findByUsernameAndName(@Nonnull String username, @Nonnull String category); 16 | 17 | @Nonnull 18 | Optional findByUsernameAndId(@Nonnull String username, @Nonnull UUID id); 19 | 20 | @Nonnull 21 | @Query("SELECT c FROM CategoryEntity c WHERE c.username = :username ORDER BY c.archived ASC, c.name ASC") 22 | List findAllByUsernameOrderByName(@Nonnull String username); 23 | 24 | long countByUsernameAndArchived(@Nonnull String username, boolean archived); 25 | } 26 | -------------------------------------------------------------------------------- /niffler-spend/src/main/java/guru/qa/niffler/ex/CategoryNotFoundException.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.ex; 2 | 3 | public class CategoryNotFoundException extends RuntimeException { 4 | public CategoryNotFoundException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /niffler-spend/src/main/java/guru/qa/niffler/ex/InvalidCategoryNameException.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.ex; 2 | 3 | public class InvalidCategoryNameException extends RuntimeException { 4 | public InvalidCategoryNameException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /niffler-spend/src/main/java/guru/qa/niffler/ex/SpendNotFoundException.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.ex; 2 | 3 | public class SpendNotFoundException extends RuntimeException { 4 | public SpendNotFoundException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /niffler-spend/src/main/java/guru/qa/niffler/ex/TooManyCategoriesException.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.ex; 2 | 3 | public class TooManyCategoriesException extends RuntimeException { 4 | public TooManyCategoriesException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /niffler-spend/src/main/java/guru/qa/niffler/model/CategoryJson.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import guru.qa.niffler.data.CategoryEntity; 5 | import jakarta.annotation.Nonnull; 6 | 7 | import java.util.UUID; 8 | 9 | public record CategoryJson( 10 | @JsonProperty("id") 11 | UUID id, 12 | @JsonProperty("name") 13 | String name, 14 | @JsonProperty("username") 15 | String username, 16 | @JsonProperty("archived") 17 | boolean archived) { 18 | 19 | public static @Nonnull CategoryJson fromEntity(@Nonnull CategoryEntity entity) { 20 | return new CategoryJson( 21 | entity.getId(), 22 | entity.getName(), 23 | entity.getUsername(), 24 | entity.isArchived() 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /niffler-spend/src/main/java/guru/qa/niffler/model/CurrencyJson.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public record CurrencyJson( 6 | @JsonProperty("currency") 7 | CurrencyValues currency, 8 | @JsonProperty("currencyRate") 9 | Double currencyRate) { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /niffler-spend/src/main/java/guru/qa/niffler/model/CurrencyValues.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | public enum CurrencyValues { 4 | RUB, USD, EUR, KZT 5 | } 6 | -------------------------------------------------------------------------------- /niffler-spend/src/main/java/guru/qa/niffler/model/DataFilterValues.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | public enum DataFilterValues { 4 | TODAY, WEEK, MONTH, ALL 5 | } 6 | -------------------------------------------------------------------------------- /niffler-spend/src/main/java/guru/qa/niffler/model/ErrorJson.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | import jakarta.annotation.Nonnull; 4 | 5 | public record ErrorJson(@Nonnull String type, 6 | @Nonnull String title, 7 | int status, 8 | @Nonnull String detail, 9 | @Nonnull String instance) { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /niffler-spend/src/main/java/guru/qa/niffler/model/StatisticByCategoryJson.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.util.List; 6 | 7 | public record StatisticByCategoryJson( 8 | @JsonProperty("category") 9 | String category, 10 | @JsonProperty("total") 11 | Double total, 12 | @JsonProperty("totalInUserDefaultCurrency") 13 | Double totalInUserDefaultCurrency, 14 | @JsonProperty("spends") 15 | List spends) { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /niffler-spend/src/main/java/guru/qa/niffler/model/StatisticJson.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.util.Date; 6 | import java.util.List; 7 | 8 | public record StatisticJson( 9 | @JsonProperty("dateFrom") 10 | Date dateFrom, 11 | @JsonProperty("dateTo") 12 | Date dateTo, 13 | @JsonProperty("currency") 14 | CurrencyValues currency, 15 | @JsonProperty("total") 16 | Double total, 17 | @JsonProperty("userDefaultCurrency") 18 | CurrencyValues userDefaultCurrency, 19 | @JsonProperty("totalInUserDefaultCurrency") 20 | Double totalInUserDefaultCurrency, 21 | @JsonProperty("categoryStatistics") 22 | List categoryStatistics) { 23 | 24 | } 25 | -------------------------------------------------------------------------------- /niffler-spend/src/main/java/guru/qa/niffler/model/StatisticV2Json.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import guru.qa.niffler.data.projection.SumByCategoryInfo; 5 | 6 | import java.util.List; 7 | 8 | public record StatisticV2Json( 9 | @JsonProperty("total") 10 | Double total, 11 | @JsonProperty("currency") 12 | CurrencyValues currency, 13 | @JsonProperty("statByCategories") 14 | List statByCategories) { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /niffler-spend/src/main/resources/application-test.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | autoconfigure: 3 | exclude: 4 | - 'org.springframework.cloud.vault.config.VaultAutoConfiguration' 5 | - 'org.springframework.cloud.vault.config.VaultObservationAutoConfiguration' 6 | - 'org.springframework.cloud.vault.config.VaultReactiveAutoConfiguration' 7 | - 'org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration' 8 | 9 | datasource: 10 | driverClassName: org.h2.Driver 11 | url: jdbc:h2:mem:niffler-spend;DB_CLOSE_DELAY=-1;MODE=PostgreSQL;DB_CLOSE_ON_EXIT=FALSE 12 | username: sa 13 | password: 14 | jpa: 15 | generate-ddl: true 16 | database-platform: org.hibernate.dialect.H2Dialect 17 | hibernate: 18 | ddl-auto: create 19 | 20 | niffler-currency: 21 | base-uri: 'http://127.0.0.1:8091' 22 | niffler-userdata: 23 | base-uri: 'http://127.0.0.1:8089' 24 | -------------------------------------------------------------------------------- /niffler-spend/src/main/resources/db/migration/niffler-spend/V2__rename_tables.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "categories" 2 | RENAME TO "category"; 3 | ALTER TABLE "spends" 4 | RENAME TO "spend"; -------------------------------------------------------------------------------- /niffler-spend/src/main/resources/db/migration/niffler-spend/V3__add_archived_field_to_category.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "category" 2 | ADD archived boolean NOT NULL DEFAULT false; -------------------------------------------------------------------------------- /niffler-spend/src/main/resources/db/migration/niffler-spend/V4__rename_category_field.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "category" 2 | RENAME COLUMN category TO name; -------------------------------------------------------------------------------- /niffler-spend/src/test/resources/categoriesListShouldBeReturnedForCurrentUser.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO public.category (id, name, username, archived) 2 | VALUES ('68bcb6da-f690-41df-bef4-18c98de97d1a', 'Веселье', 'duck', false); 3 | INSERT INTO public.category (id, name, username, archived) 4 | VALUES ('357ed0ec-8018-11ef-86dd-0242ac110002', 'Магазины', 'duck', true); 5 | -------------------------------------------------------------------------------- /niffler-spend/src/test/resources/categoryNameAndArchivedStatusShouldBeUpdated.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO public.category (id, name, username, archived) 2 | VALUES ('06fe9c06-ae67-406e-a711-ba729e3d4775', 'Обучение', 'duck', false); -------------------------------------------------------------------------------- /niffler-userdata/src/main/java/guru/qa/niffler/NifflerUserdataApplication.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler; 2 | 3 | import guru.qa.niffler.service.PropertiesLogger; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | @SpringBootApplication 8 | public class NifflerUserdataApplication { 9 | 10 | public static void main(String[] args) { 11 | SpringApplication springApplication = new SpringApplication(NifflerUserdataApplication.class); 12 | springApplication.addListeners(new PropertiesLogger()); 13 | springApplication.run(args); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /niffler-userdata/src/main/java/guru/qa/niffler/data/CurrencyValues.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data; 2 | 3 | public enum CurrencyValues { 4 | RUB, USD, EUR, KZT 5 | } 6 | -------------------------------------------------------------------------------- /niffler-userdata/src/main/java/guru/qa/niffler/data/FriendShipId.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.io.Serializable; 7 | import java.util.Objects; 8 | import java.util.UUID; 9 | 10 | @Getter 11 | @Setter 12 | public class FriendShipId implements Serializable { 13 | 14 | private UUID requester; 15 | private UUID addressee; 16 | 17 | @Override 18 | public boolean equals(Object o) { 19 | if (this == o) return true; 20 | if (o == null || getClass() != o.getClass()) return false; 21 | FriendShipId friendsId = (FriendShipId) o; 22 | return Objects.equals(requester, friendsId.requester) && Objects.equals(addressee, friendsId.addressee); 23 | } 24 | 25 | @Override 26 | public int hashCode() { 27 | return Objects.hash(requester, addressee); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /niffler-userdata/src/main/java/guru/qa/niffler/data/FriendshipStatus.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data; 2 | 3 | public enum FriendshipStatus { 4 | PENDING, 5 | ACCEPTED 6 | } 7 | -------------------------------------------------------------------------------- /niffler-userdata/src/main/java/guru/qa/niffler/data/projection/UserWithStatus.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.data.projection; 2 | 3 | import guru.qa.niffler.data.CurrencyValues; 4 | import guru.qa.niffler.data.FriendshipStatus; 5 | 6 | import java.util.UUID; 7 | 8 | public record UserWithStatus( 9 | UUID id, 10 | String username, 11 | CurrencyValues currency, 12 | String fullname, 13 | byte[] photoSmall, 14 | FriendshipStatus status 15 | ) { 16 | } 17 | -------------------------------------------------------------------------------- /niffler-userdata/src/main/java/guru/qa/niffler/ex/NotFoundException.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.ex; 2 | 3 | public class NotFoundException extends RuntimeException { 4 | public NotFoundException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /niffler-userdata/src/main/java/guru/qa/niffler/ex/SameUsernameException.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.ex; 2 | 3 | public class SameUsernameException extends RuntimeException { 4 | public SameUsernameException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /niffler-userdata/src/main/java/guru/qa/niffler/model/ErrorJson.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | import jakarta.annotation.Nonnull; 4 | 5 | public record ErrorJson(@Nonnull String type, 6 | @Nonnull String title, 7 | int status, 8 | @Nonnull String detail, 9 | @Nonnull String instance) { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /niffler-userdata/src/main/java/guru/qa/niffler/model/FriendshipStatus.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | public enum FriendshipStatus { 4 | INVITE_SENT, INVITE_RECEIVED, FRIEND 5 | } 6 | -------------------------------------------------------------------------------- /niffler-userdata/src/main/java/guru/qa/niffler/model/IUserJson.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.model; 2 | 3 | import guru.qa.niffler.data.CurrencyValues; 4 | 5 | import java.util.UUID; 6 | 7 | public interface IUserJson { 8 | UUID id(); 9 | 10 | String username(); 11 | 12 | String firstname(); 13 | 14 | String surname(); 15 | 16 | CurrencyValues currency(); 17 | 18 | String photo(); 19 | 20 | String photoSmall(); 21 | 22 | FriendshipStatus friendshipStatus(); 23 | } 24 | -------------------------------------------------------------------------------- /niffler-userdata/src/main/java/guru/qa/niffler/soap/SpringPageable.java: -------------------------------------------------------------------------------- 1 | package guru.qa.niffler.soap; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import jaxb.userdata.PageInfo; 5 | import org.springframework.data.domain.PageRequest; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.data.domain.Sort; 8 | 9 | public class SpringPageable { 10 | private final PageInfo pageInfo; 11 | 12 | public SpringPageable(PageInfo pageInfo) { 13 | this.pageInfo = pageInfo; 14 | } 15 | 16 | public @Nonnull Pageable pageable() { 17 | return PageRequest.of( 18 | pageInfo.getPage(), pageInfo.getSize(), sortFromRequest() 19 | ); 20 | } 21 | 22 | private @Nonnull Sort sortFromRequest() { 23 | return Sort.by(pageInfo.getSort().stream() 24 | .map(st -> new Sort.Order( 25 | Sort.Direction.fromString( 26 | st.getDirection().name() 27 | ), 28 | st.getProperty() 29 | ) 30 | ).toList()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /niffler-userdata/src/main/resources/application-test.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | autoconfigure: 3 | exclude: 4 | - 'org.springframework.cloud.vault.config.VaultAutoConfiguration' 5 | - 'org.springframework.cloud.vault.config.VaultObservationAutoConfiguration' 6 | - 'org.springframework.cloud.vault.config.VaultReactiveAutoConfiguration' 7 | - 'org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration' 8 | 9 | datasource: 10 | driverClassName: org.h2.Driver 11 | url: jdbc:h2:mem:niffler-userdata;DB_CLOSE_DELAY=-1;MODE=PostgreSQL;DB_CLOSE_ON_EXIT=FALSE 12 | username: sa 13 | password: 14 | jpa: 15 | generate-ddl: true 16 | database-platform: org.hibernate.dialect.H2Dialect 17 | hibernate: 18 | ddl-auto: create 19 | 20 | niffler-userdata: 21 | base-uri: 'http://127.0.0.1:8089' 22 | -------------------------------------------------------------------------------- /niffler-userdata/src/main/resources/db/migration/niffler-userdata/V1__schema_init.sql: -------------------------------------------------------------------------------- 1 | -- create database "niffler-userdata" with owner postgres; 2 | 3 | create extension if not exists "uuid-ossp"; 4 | 5 | create table if not exists "users" 6 | ( 7 | id UUID unique not null default uuid_generate_v1(), 8 | username varchar(50) unique not null, 9 | currency varchar(3) not null, 10 | firstname varchar(255), 11 | surname varchar(255), 12 | photo bytea, 13 | primary key (id) 14 | ); 15 | 16 | alter table "users" 17 | owner to postgres; 18 | 19 | create table if not exists "friends" 20 | ( 21 | user_id UUID not null, 22 | friend_id UUID not null, 23 | pending boolean not null default true, 24 | primary key (user_id, friend_id), 25 | constraint fk_user_id foreign key (user_id) references "users" (id), 26 | constraint fk_friend_id foreign key (friend_id) references "users" (id) 27 | ); 28 | 29 | alter table "friends" 30 | owner to postgres; -------------------------------------------------------------------------------- /niffler-userdata/src/main/resources/db/migration/niffler-userdata/V2__rename_tables.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "users" 2 | RENAME TO "user"; 3 | ALTER TABLE "friends" 4 | RENAME TO "friendship"; -------------------------------------------------------------------------------- /niffler-userdata/src/main/resources/db/migration/niffler-userdata/V3__friendship.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "friendship" 2 | ADD created_date date NOT NULL DEFAULT CURRENT_DATE; 3 | 4 | ALTER TABLE "friendship" 5 | ALTER COLUMN pending TYPE varchar(50) USING ( 6 | CASE pending 7 | WHEN TRUE THEN 'PENDING'::varchar(50) 8 | WHEN FALSE THEN 'ACCEPTED'::varchar(50) 9 | END 10 | ); 11 | 12 | ALTER TABLE "friendship" 13 | RENAME COLUMN pending TO status; 14 | 15 | ALTER TABLE "friendship" 16 | RENAME COLUMN user_id TO requester_id; 17 | 18 | ALTER TABLE "friendship" 19 | RENAME COLUMN friend_id TO addressee_id; 20 | 21 | ALTER TABLE "friendship" 22 | ADD CONSTRAINT friend_are_distinct_ck CHECK (requester_id <> addressee_id); 23 | -------------------------------------------------------------------------------- /niffler-userdata/src/main/resources/db/migration/niffler-userdata/V4__small_avatar.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "user" 2 | ADD photo_small bytea; -------------------------------------------------------------------------------- /niffler-userdata/src/main/resources/db/migration/niffler-userdata/V5__full_name.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "user" 2 | ADD full_name varchar(100); -------------------------------------------------------------------------------- /postgres/script/init-database.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -u 5 | 6 | function create_databases() { 7 | echo " Creating database '$1'" 8 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL 9 | SELECT 'CREATE DATABASE "$1"' 10 | WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '"$1"')\gexec 11 | EOSQL 12 | } 13 | 14 | if [ -n "$CREATE_DATABASES" ]; then 15 | echo "Multiple database creation requested: $CREATE_DATABASES" 16 | for db in $(echo $CREATE_DATABASES | tr ',' ' '); do 17 | create_databases $db 18 | done 19 | echo "Multiple databases created" 20 | fi 21 | -------------------------------------------------------------------------------- /selenoid/browsers.json: -------------------------------------------------------------------------------- 1 | { 2 | "chrome": { 3 | "default": "127.0", 4 | "versions": { 5 | "125.0": { 6 | "image": "selenoid/vnc_chrome:125.0", 7 | "port": "4444", 8 | "env": [ 9 | "TZ=Europe/Moscow" 10 | ] 11 | }, 12 | "127.0": { 13 | "image": "selenoid/vnc_chrome:127.0", 14 | "port": "4444", 15 | "env": [ 16 | "TZ=Europe/Moscow" 17 | ] 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'niffler-ng' 2 | include 'niffler-gateway' 3 | include 'niffler-auth' 4 | include 'niffler-currency' 5 | include 'niffler-userdata' 6 | include 'niffler-spend' 7 | include 'niffler-grpc-common' 8 | include 'niffler-e-2-e-tests' 9 | -------------------------------------------------------------------------------- /wiremock/grpc/mappings/getAllCurrencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "POST", 4 | "url": "/NifflerCurrencyService/getAllCurrencies" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "jsonBody": { 9 | "allCurrencies": [ 10 | { 11 | "currency": "RUB", 12 | "currencyRate": 0.015 13 | }, 14 | { 15 | "currency": "KZT", 16 | "currencyRate": 0.0021 17 | }, 18 | { 19 | "currency": "EUR", 20 | "currencyRate": 1.08 21 | }, 22 | { 23 | "currency": "USD", 24 | "currencyRate": 1.0 25 | } 26 | ] 27 | }, 28 | "headers": { 29 | "Content-Type": "application/json" 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /wiremock/rest/mappings/currentUser.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "url": "/currentUser?username=dima" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "jsonBody": { 9 | "id": "{{randomValue type='UUID'}}", 10 | "username": "{{request.query.username}}", 11 | "firstname": "null", 12 | "surname": null, 13 | "fullname": "Dmitrii Tuchs", 14 | "currency": "RUB", 15 | "photo": null 16 | }, 17 | "headers": { 18 | "Content-Type": "application/json" 19 | } 20 | } 21 | } --------------------------------------------------------------------------------