├── .github └── workflows │ ├── cd.yml │ ├── ci.yml │ └── deploy.yml ├── Dockerfile ├── Procfile ├── README.md ├── docker-compose.yml ├── pom.xml ├── screenshots ├── flow.png └── flow.puml ├── src ├── lombok.config ├── main │ ├── java │ │ └── com │ │ │ └── whiskels │ │ │ └── notifier │ │ │ ├── App.java │ │ │ ├── infrastructure │ │ │ ├── admin │ │ │ │ └── telegram │ │ │ │ │ ├── Bot.java │ │ │ │ │ ├── BotMessage.java │ │ │ │ │ ├── Command.java │ │ │ │ │ ├── CommandHandler.java │ │ │ │ │ ├── DocumentBotMessage.java │ │ │ │ │ ├── MessageProcessor.java │ │ │ │ │ ├── TextBotMessage.java │ │ │ │ │ ├── handler │ │ │ │ │ ├── DefaultHandler.java │ │ │ │ │ ├── ExceptionEventHandler.java │ │ │ │ │ ├── log │ │ │ │ │ │ ├── config │ │ │ │ │ │ │ └── LogHandlerConfiguration.java │ │ │ │ │ │ ├── domain │ │ │ │ │ │ │ └── PaperTrailEventLog.java │ │ │ │ │ │ └── service │ │ │ │ │ │ │ ├── LogHandler.java │ │ │ │ │ │ │ ├── LogService.java │ │ │ │ │ │ │ └── PapertrailClient.java │ │ │ │ │ ├── reload │ │ │ │ │ │ └── DataReloadHandler.java │ │ │ │ │ └── retry │ │ │ │ │ │ └── RetryHandler.java │ │ │ │ │ └── util │ │ │ │ │ └── TelegramUtil.java │ │ │ ├── config │ │ │ │ ├── clock │ │ │ │ │ └── ClockConfiguration.java │ │ │ │ └── feign │ │ │ │ │ ├── FeignProxyConfig.java │ │ │ │ │ ├── FixieProxyPropertiesProvider.java │ │ │ │ │ └── ProxyPropertiesProvider.java │ │ │ ├── domain │ │ │ │ └── AbstractBaseEntity.java │ │ │ ├── googlesheets │ │ │ │ ├── GoogleCredentialConfigurationProperties.java │ │ │ │ ├── GoogleCredentialProvider.java │ │ │ │ └── GoogleSheetsReader.java │ │ │ ├── mock │ │ │ │ └── MockUtil.java │ │ │ ├── report │ │ │ │ ├── ReportExecutor.java │ │ │ │ ├── slack │ │ │ │ │ ├── SlackClient.java │ │ │ │ │ ├── SlackPayloadMapper.java │ │ │ │ │ ├── SlackReportExecutor.java │ │ │ │ │ ├── builder │ │ │ │ │ │ ├── AccessoryBlock.java │ │ │ │ │ │ └── SlackPayloadBuilder.java │ │ │ │ │ └── config │ │ │ │ │ │ └── SlackConfiguration.java │ │ │ │ └── webhook │ │ │ │ │ ├── FeignWebhookSink.java │ │ │ │ │ ├── FeignWebhookSinkConfig.java │ │ │ │ │ ├── FeignWebhookSinkDto.java │ │ │ │ │ └── WebhookSinkExecutor.java │ │ │ └── repository │ │ │ │ └── AbstractRepository.java │ │ │ ├── reporting │ │ │ ├── ReportService.java │ │ │ ├── ReportType.java │ │ │ ├── ScheduledReportTaskSubmitter.java │ │ │ ├── _ReportConfig.java │ │ │ ├── domain │ │ │ │ ├── AbstractTimeStampedEntity.java │ │ │ │ ├── AbstractTimeStampedEntityListener.java │ │ │ │ └── HasBirthday.java │ │ │ ├── exception │ │ │ │ └── ExceptionEvent.java │ │ │ └── service │ │ │ │ ├── DataFetchService.java │ │ │ │ ├── GenericReportService.java │ │ │ │ ├── Report.java │ │ │ │ ├── ReportData.java │ │ │ │ ├── ReportMessageConverter.java │ │ │ │ ├── ReportServiceImpl.java │ │ │ │ ├── SingleReportMessageConverter.java │ │ │ │ ├── audit │ │ │ │ ├── AuditDataFetchResult.java │ │ │ │ ├── AuditDataFetchResultAspect.java │ │ │ │ ├── LoadAudit.java │ │ │ │ └── LoadAuditRepository.java │ │ │ │ ├── cleaner │ │ │ │ └── DatabaseCleaner.java │ │ │ │ ├── customer │ │ │ │ ├── birthday │ │ │ │ │ ├── config │ │ │ │ │ │ ├── CustomerBirthdayInfoFetchConfig.java │ │ │ │ │ │ ├── CustomerBirthdayInfoReportConfig.java │ │ │ │ │ │ └── CustomerBirthdaySpreadsheetProperties.java │ │ │ │ │ ├── convert │ │ │ │ │ │ ├── CustomerBirthdayInfoReportMessageConverter.java │ │ │ │ │ │ ├── ReportContext.java │ │ │ │ │ │ └── context │ │ │ │ │ │ │ ├── BeforeEventReportContext.java │ │ │ │ │ │ │ ├── DailyReportContext.java │ │ │ │ │ │ │ ├── MonthMiddleReportContext.java │ │ │ │ │ │ │ └── MonthStartReportContext.java │ │ │ │ │ ├── domain │ │ │ │ │ │ └── CustomerBirthdayInfo.java │ │ │ │ │ ├── fetch │ │ │ │ │ │ └── CustomerBirthdayInfoFetchService.java │ │ │ │ │ └── mock │ │ │ │ │ │ └── CustomerBirthdayInfoFetchServiceMock.java │ │ │ │ ├── debt │ │ │ │ │ ├── config │ │ │ │ │ │ ├── CustomerDebtFetchConfig.java │ │ │ │ │ │ └── CustomerDebtReportConfig.java │ │ │ │ │ ├── convert │ │ │ │ │ │ ├── CustomerDebtDto.java │ │ │ │ │ │ └── CustomerDebtReportMessageConverter.java │ │ │ │ │ ├── domain │ │ │ │ │ │ ├── CurrencyRate.java │ │ │ │ │ │ └── CustomerDebt.java │ │ │ │ │ ├── fetch │ │ │ │ │ │ ├── CurrencyRateDataFetchService.java │ │ │ │ │ │ ├── CurrencyRateFeignClient.java │ │ │ │ │ │ ├── CustomerDebtData.java │ │ │ │ │ │ ├── CustomerDebtDebtDataFetchService.java │ │ │ │ │ │ └── CustomerDebtFeignClient.java │ │ │ │ │ └── mock │ │ │ │ │ │ └── CustomerDebtFetchServiceMock.java │ │ │ │ └── payment │ │ │ │ │ ├── config │ │ │ │ │ ├── CustomerPaymentFetchConfig.java │ │ │ │ │ ├── CustomerPaymentReportConfig.java │ │ │ │ │ └── CustomerPaymentReportPicProperties.java │ │ │ │ │ ├── domain │ │ │ │ │ ├── CustomerPaymentDto.java │ │ │ │ │ └── FinancialOperation.java │ │ │ │ │ ├── fetch │ │ │ │ │ ├── FinOperationDataFetchService.java │ │ │ │ │ ├── FinOperationFeignClient.java │ │ │ │ │ ├── FinOperationReloadScheduler.java │ │ │ │ │ ├── FinOperationRepository.java │ │ │ │ │ └── PaymentReportDataFetchService.java │ │ │ │ │ ├── messaging │ │ │ │ │ └── PaymentReportMessageConverter.java │ │ │ │ │ └── mock │ │ │ │ │ └── CustomerPaymentDtoFetchMock.java │ │ │ │ └── employee │ │ │ │ ├── config │ │ │ │ ├── EmployeeEventFetchConfig.java │ │ │ │ └── EmployeeEventReportConfig.java │ │ │ │ ├── convert │ │ │ │ ├── EmployeeDto.java │ │ │ │ ├── EmployeeEventReportMessageConverter.java │ │ │ │ ├── ReportContext.java │ │ │ │ └── context │ │ │ │ │ ├── BeforeEventReportContext.java │ │ │ │ │ ├── DailyReportContext.java │ │ │ │ │ ├── MonthMiddleReportContext.java │ │ │ │ │ └── MonthStartReportContext.java │ │ │ │ ├── domain │ │ │ │ ├── BirthdayDeserializer.java │ │ │ │ └── Employee.java │ │ │ │ ├── fetch │ │ │ │ ├── EmployeeDataFetchService.java │ │ │ │ └── EmployeeFeignClient.java │ │ │ │ └── mock │ │ │ │ └── EmployeeFetchMock.java │ │ │ └── utilities │ │ │ ├── DateTimeUtil.java │ │ │ ├── Util.java │ │ │ ├── collections │ │ │ └── StreamUtil.java │ │ │ └── formatters │ │ │ ├── DateTimeFormatter.java │ │ │ └── StringFormatter.java │ └── resources │ │ ├── application-mock.yaml │ │ ├── application-prod.yaml │ │ ├── application-telegram.yaml │ │ ├── application.yaml │ │ ├── db │ │ └── migration │ │ │ ├── V001__initialize.sql │ │ │ ├── V002__create_table_receivable.sql │ │ │ ├── V003__alter_receivable.sql │ │ │ ├── V004__alter_receivable.sql │ │ │ ├── V005__create_load_audit.sql │ │ │ ├── V006__alter_load_audit.sql │ │ │ ├── V007__alter_receivable.sql │ │ │ ├── V008__alter_users.sql │ │ │ ├── V009__alter_financial_operation.sql │ │ │ ├── V010__alter_load_audit.sql │ │ │ ├── V011__drop_tables.sql │ │ │ ├── V012__drop_date_columns.sql │ │ │ └── V013__drop_table_user_roles.sql │ │ └── mocks │ │ ├── customer.json │ │ ├── debt.json │ │ ├── employee.json │ │ └── payment.json └── test │ ├── java │ └── com │ │ └── whiskels │ │ └── notifier │ │ ├── DisabledDataSourceConfiguration.java │ │ ├── JsonUtils.java │ │ ├── MockedClockConfiguration.java │ │ ├── SharedPostgresContainer.java │ │ ├── TestUtil.java │ │ ├── infrastructure │ │ ├── admin │ │ │ └── telegram │ │ │ │ ├── BotTest.java │ │ │ │ ├── MessageProcessorTest.java │ │ │ │ ├── TextBotMessageTest.java │ │ │ │ ├── handler │ │ │ │ ├── DefaultHandlerTest.java │ │ │ │ ├── ExceptionEventHandlerTest.java │ │ │ │ ├── log │ │ │ │ │ └── service │ │ │ │ │ │ ├── LogHandlerTest.java │ │ │ │ │ │ ├── LogServiceTest.java │ │ │ │ │ │ └── PapertrailClientTest.java │ │ │ │ ├── reload │ │ │ │ │ └── DataReloadHandlerTest.java │ │ │ │ └── retry │ │ │ │ │ └── RetryHandlerTest.java │ │ │ │ └── util │ │ │ │ └── TelegramUtilTest.java │ │ ├── config │ │ │ └── feign │ │ │ │ └── FixieProxyPropertiesProviderTest.java │ │ └── slack │ │ │ ├── SlackClientTest.java │ │ │ └── builder │ │ │ └── SlackPayloadBuilderTest.java │ │ ├── reporting │ │ ├── GenericReportServiceTest.java │ │ ├── ReportPropertiesTest.java │ │ ├── ReportServiceTest.java │ │ ├── ReportTest.java │ │ ├── ScheduledReportTaskSubmitterTest.java │ │ ├── WireMockTestConfig.java │ │ ├── domain │ │ │ ├── AbstractTimeStampedEntityListenerTest.java │ │ │ └── HasBirthdayTest.java │ │ ├── exception │ │ │ └── ExceptionEventTest.java │ │ ├── executor │ │ │ └── SlackReportExecutorTest.java │ │ └── service │ │ │ ├── audit │ │ │ ├── AuditDataFetchResultAspectTest.java │ │ │ └── LoadAuditTest.java │ │ │ ├── cleaner │ │ │ └── DatabaseCleanerTest.java │ │ │ ├── customer │ │ │ ├── birthday │ │ │ │ ├── convert │ │ │ │ │ ├── CustomerBirthdayInfoReportMessageConverterTest.java │ │ │ │ │ └── context │ │ │ │ │ │ ├── BeforeEventReportDataTest.java │ │ │ │ │ │ ├── DailyReportDataTest.java │ │ │ │ │ │ ├── MonthMiddleReportDataTest.java │ │ │ │ │ │ └── MonthStartReportDataTest.java │ │ │ │ ├── fetch │ │ │ │ │ └── CustomerBirthdayInfoFetchServiceTest.java │ │ │ │ └── mock │ │ │ │ │ └── CustomerBirthdayInfoFetchServiceMockTest.java │ │ │ ├── debt │ │ │ │ ├── convert │ │ │ │ │ ├── CustomerDebtDtoTest.java │ │ │ │ │ └── CustomerDebtReportMessageConverterTest.java │ │ │ │ ├── fetch │ │ │ │ │ ├── CurrencyRateDataFetchServiceTest.java │ │ │ │ │ └── CustomerDebtDebtDataFetchServiceTest.java │ │ │ │ └── mock │ │ │ │ │ └── CustomerDebtFetchServiceMockTest.java │ │ │ └── payment │ │ │ │ ├── fetch │ │ │ │ ├── FinOperationDataFetchServiceTest.java │ │ │ │ ├── FinOperationFeignClientTest.java │ │ │ │ ├── FinOperationReloadSchedulerTest.java │ │ │ │ ├── FinOperationRepositoryTest.java │ │ │ │ └── PaymentReportDataFetchServiceTest.java │ │ │ │ ├── messaging │ │ │ │ └── PaymentReportMessageConverterTest.java │ │ │ │ └── mock │ │ │ │ └── CustomerPaymentDtoFetchMockTest.java │ │ │ └── employee │ │ │ ├── convert │ │ │ ├── EmployeeDtoTest.java │ │ │ ├── EmployeeEventReportMessageConverterTest.java │ │ │ └── context │ │ │ │ ├── BeforeEventReportDataTest.java │ │ │ │ ├── DailyReportDataTest.java │ │ │ │ ├── MonthMiddleReportDataTest.java │ │ │ │ └── MonthStartReportDataTest.java │ │ │ ├── domain │ │ │ ├── BirthdayDeserializerTest.java │ │ │ └── EmployeeTest.java │ │ │ ├── fetch │ │ │ ├── EmployeeDataFetchServiceTest.java │ │ │ └── EmployeeFeignClientTest.java │ │ │ └── mock │ │ │ └── EmployeeFetchMockTest.java │ │ └── utilities │ │ ├── DateTimeUtilTest.java │ │ ├── UtilTest.java │ │ ├── collections │ │ └── StreamUtilTest.java │ │ └── formatters │ │ ├── DateTimeFormatterTest.java │ │ └── StringFormatterTest.java │ └── resources │ ├── application-test-containers.yaml │ └── json │ └── telegram │ └── update │ ├── callbackquery │ └── token.json │ └── message │ └── help.json └── system.properties /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | workflow_run: 5 | workflows: [ "CI" ] 6 | branches: [ master ] 7 | types: 8 | - completed 9 | 10 | jobs: 11 | deploy: 12 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 13 | uses: ./.github/workflows/deploy.yml 14 | with: 15 | branch: ${{ github.event.inputs.branch }} 16 | secrets: inherit 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '*' 7 | push: 8 | branches: 9 | - 'master' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up JDK 17 | uses: actions/setup-java@v4 18 | with: 19 | java-version: '21' 20 | distribution: 'adopt' 21 | cache: maven 22 | - name: Test Compile 23 | run: mvn -B clean test-compile 24 | - name: Run Tests 25 | run: mvn -B clean test 26 | - name: Test Coverage 27 | uses: codecov/codecov-action@v4 28 | with: 29 | token: ${{ secrets.CODECOV_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Heroku 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | branch: 7 | description: "Branch to deploy" 8 | required: true 9 | type: string 10 | workflow_dispatch: 11 | inputs: 12 | branch: 13 | description: "Branch to deploy" 14 | required: true 15 | type: string 16 | default: master 17 | 18 | jobs: 19 | deploy: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | ref: ${{ github.event.inputs.branch }} 25 | - name: Set up JDK 26 | uses: actions/setup-java@v4 27 | with: 28 | java-version: '21' 29 | distribution: 'adopt' 30 | cache: maven 31 | - name: Deploy 32 | env: 33 | HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} 34 | run: mvn -B clean heroku:deploy 35 | scale: 36 | needs: deploy 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Sleep for 30 seconds 40 | uses: whatnick/wait-action@master 41 | with: 42 | time: '30s' 43 | - name: Scale worker 44 | env: 45 | HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} 46 | HEROKU_APP_NAME: ${{ secrets.HEROKU_APP_NAME }} 47 | run: heroku ps:scale worker=1 -a $HEROKU_APP_NAME 48 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:21-jdk-slim as build 2 | LABEL maintainer="whiskels" 3 | ARG JAR_FILE=target/*.jar 4 | COPY ${JAR_FILE} NotifierBot.jar 5 | 6 | CMD java $JAVA_OPTS --enable-preview -jar /NotifierBot.jar 7 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: sh target/bin/NotifierBot -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | bot: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | container_name: NotifierBot 9 | depends_on: 10 | - db 11 | environment: 12 | - SPRING_PROFILES_ACTIVE=mock,telegram 13 | - WEBHOOK_URL 14 | - TELEGRAM_BOT_ADMIN 15 | - TELEGRAM_BOT_TOKEN 16 | - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/postgres 17 | - SPRING_DATASOURCE_USERNAME=postgres 18 | - SPRING_DATASOURCE_PASSWORD=postgres 19 | 20 | db: 21 | image: 'postgres:16.2' 22 | container_name: db 23 | ports: 24 | - "5432:5432" 25 | environment: 26 | - POSTGRES_USER=postgres 27 | - POSTGRES_PASSWORD=postgres -------------------------------------------------------------------------------- /screenshots/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whiskels/NotifierBot/456b9b7496ae5f77dce8d2dfaa83c47aa7983e43/screenshots/flow.png -------------------------------------------------------------------------------- /screenshots/flow.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam actorStyle awesome 3 | 4 | participant "Cron Jobs" as CJ 5 | participant "Task Scheduler" as TS 6 | participant "Report Service" as RS 7 | participant "Generic Report Service" as GRP 8 | participant "Data Fetch Service" as DFS 9 | participant "Message Conversion Service" as MCS 10 | participant "Payload Executor" as MS 11 | boundary "Slack" as SL 12 | actor "Admin" as ADMIN 13 | participant "CommandHandler" as CH 14 | participant "LogService" as LS 15 | 16 | ' ---- Scheduled Reporting Flow ---- 17 | == Scheduled Reporting flow == 18 | 19 | CJ -> TS: Scheduled by 20 | TS -> RS: Requests report 21 | 22 | group Report Processing 23 | RS -> GRP: Propagates report context 24 | 25 | GRP -> DFS: Requests report data 26 | alt DataFetchService Exception ' Occurs if breaking changed occured in API, proxy failed etc. 27 | DFS --> RS: Exception Occurred 28 | RS --> MS: Passes exception message 29 | MS --> ADMIN: Sends exception message 30 | end 31 | 32 | DFS -> GRP: Returns report data 33 | GRP -> MCS: Calls 34 | MCS -> GRP: Returns iterable messages 35 | GRP -> RS: Returns iterable messages 36 | 37 | RS -> MS: Passes iterable messages 38 | MS -> SL: Sends to slack 39 | 40 | alt PayloadExecutor Exception ' Can occur if Slack payload delivery failed 41 | SL --> MS: Returns exception 42 | MS --> ADMIN: Sends exception message 43 | end 44 | end 45 | 46 | ' ---- Admin Intervention Flow ---- 47 | == Admin Intervention == 48 | 49 | group Retry report 50 | ADMIN -> CH: Requests report retry 51 | CH -> RS: Retry report 52 | ref over RS: Report Processing 53 | end 54 | 55 | group Request for POD logs 56 | ADMIN -> CH: Requests logs 57 | CH -> LS: Requests logs 58 | LS -> CH: Returns logs 59 | CH -> ADMIN: Sends logs message 60 | end 61 | @enduml -------------------------------------------------------------------------------- /src/lombok.config: -------------------------------------------------------------------------------- 1 | config.stopBubbling = true 2 | lombok.addLombokGeneratedAnnotation = true -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/App.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 6 | import org.springframework.cloud.openfeign.EnableFeignClients; 7 | import org.springframework.context.annotation.Profile; 8 | import org.springframework.scheduling.annotation.EnableScheduling; 9 | 10 | @Profile("!test") 11 | @SpringBootApplication 12 | @EnableFeignClients 13 | @EnableScheduling 14 | @EnableConfigurationProperties 15 | public class App { 16 | public static void main(String[] args) { 17 | SpringApplication.run(App.class, args); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/admin/telegram/Bot.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.admin.telegram; 2 | 3 | import lombok.Getter; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Profile; 7 | import org.springframework.stereotype.Service; 8 | import org.telegram.telegrambots.bots.TelegramLongPollingBot; 9 | import org.telegram.telegrambots.meta.api.objects.Update; 10 | 11 | import static java.util.Objects.nonNull; 12 | 13 | @Slf4j 14 | @Service 15 | @Profile("telegram") 16 | public class Bot extends TelegramLongPollingBot { 17 | @Getter 18 | private final String botUsername; 19 | @Getter 20 | private final String botToken; 21 | private final MessageProcessor messageProcessor; 22 | 23 | public Bot(@Value("${telegram.bot.name:NotifierAdmin}") String botUsername, 24 | @Value("${telegram.bot.token}") String botToken, 25 | MessageProcessor messageProcessor) { 26 | this.botUsername = botUsername; 27 | this.botToken = botToken; 28 | this.messageProcessor = messageProcessor; 29 | } 30 | 31 | @Override 32 | public void onUpdateReceived(Update update) { 33 | var message = messageProcessor.onUpdateReceived(update); 34 | if (nonNull(message)) { 35 | message.send(this); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/admin/telegram/BotMessage.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.admin.telegram; 2 | 3 | public interface BotMessage { 4 | void send(Bot bot); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/admin/telegram/Command.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.admin.telegram; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public enum Command { 7 | RETRY_REPORT(10, "\uD83D\uDD01 Retry reports"), 8 | RELOAD_DATA(15, "\uD83D\uDCC8 Reload data"), 9 | GET_LOGS(20, "\uD83D\uDD79 Fetch logs"), 10 | 11 | DEFAULT; 12 | 13 | private final String description; 14 | private final Integer order; 15 | 16 | Command() { 17 | this.description = ""; 18 | this.order = Integer.MAX_VALUE; 19 | } 20 | 21 | Command(Integer order, String description) { 22 | this.description = description; 23 | this.order = order; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/admin/telegram/CommandHandler.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.admin.telegram; 2 | 3 | public interface CommandHandler { 4 | BotMessage handle(final String chatId, final String message); 5 | 6 | Command getCommand(); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/admin/telegram/DocumentBotMessage.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.admin.telegram; 2 | 3 | import lombok.SneakyThrows; 4 | import lombok.Value; 5 | import org.telegram.telegrambots.meta.api.methods.send.SendDocument; 6 | 7 | @Value(staticConstructor = "of") 8 | public class DocumentBotMessage implements BotMessage { 9 | SendDocument message; 10 | 11 | @Override 12 | @SneakyThrows 13 | public void send(Bot bot) { 14 | bot.execute(message); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/admin/telegram/TextBotMessage.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.admin.telegram; 2 | 3 | import lombok.SneakyThrows; 4 | import lombok.Value; 5 | import org.telegram.telegrambots.meta.api.methods.send.SendMessage; 6 | 7 | @Value(staticConstructor = "of") 8 | public class TextBotMessage implements BotMessage { 9 | SendMessage message; 10 | 11 | @Override 12 | @SneakyThrows 13 | public void send(Bot bot) { 14 | bot.execute(message); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/admin/telegram/handler/DefaultHandler.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.admin.telegram.handler; 2 | 3 | import com.whiskels.notifier.infrastructure.admin.telegram.BotMessage; 4 | import com.whiskels.notifier.infrastructure.admin.telegram.Command; 5 | import com.whiskels.notifier.infrastructure.admin.telegram.CommandHandler; 6 | import com.whiskels.notifier.infrastructure.admin.telegram.TextBotMessage; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.context.annotation.Primary; 9 | import org.springframework.stereotype.Service; 10 | import org.telegram.telegrambots.meta.api.methods.send.SendMessage; 11 | import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | import static com.whiskels.notifier.infrastructure.admin.telegram.Command.DEFAULT; 17 | import static com.whiskels.notifier.infrastructure.admin.telegram.util.TelegramUtil.button; 18 | import static com.whiskels.notifier.infrastructure.admin.telegram.util.TelegramUtil.createMarkup; 19 | 20 | @Primary 21 | @Service 22 | @RequiredArgsConstructor 23 | class DefaultHandler implements CommandHandler { 24 | private final List handlers; 25 | 26 | @Override 27 | public BotMessage handle(String userId, String message) { 28 | var sendMessage = SendMessage.builder() 29 | .chatId(userId) 30 | .text(""" 31 | \uD83D\uDC4B Welcome to admin module 32 | Here is what you can do 33 | """) 34 | .replyMarkup(createMarkup(getKeyBoard())) 35 | .build(); 36 | 37 | return TextBotMessage.of(sendMessage); 38 | } 39 | 40 | private List> getKeyBoard() { 41 | List> keyboard = new ArrayList<>(); 42 | handlers.forEach(handler -> { 43 | var command = handler.getCommand(); 44 | keyboard.add(List.of(button(command.getDescription(), command.name()))); 45 | } 46 | ); 47 | return keyboard; 48 | } 49 | 50 | @Override 51 | public Command getCommand() { 52 | return DEFAULT; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/admin/telegram/handler/log/config/LogHandlerConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.admin.telegram.handler.log.config; 2 | 3 | import com.whiskels.notifier.infrastructure.admin.telegram.handler.log.service.LogHandler; 4 | import com.whiskels.notifier.infrastructure.admin.telegram.handler.log.service.LogService; 5 | import com.whiskels.notifier.infrastructure.admin.telegram.handler.log.service.PapertrailClient; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.Profile; 10 | 11 | @Configuration(proxyBeanMethods = false) 12 | @ConditionalOnProperty(PapertrailClient.PAPERTRAIL_API_TOKEN) 13 | @Profile("telegram") 14 | class LogHandlerConfiguration { 15 | @Bean 16 | LogService logService(final PapertrailClient papertrailClient) { 17 | return new LogService(papertrailClient); 18 | } 19 | 20 | @Bean 21 | LogHandler logHandler(final LogService logService) { 22 | return new LogHandler(logService); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/admin/telegram/handler/log/domain/PaperTrailEventLog.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.admin.telegram.handler.log.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import lombok.Data; 5 | 6 | import java.util.List; 7 | 8 | @Data 9 | @JsonIgnoreProperties(ignoreUnknown = true) 10 | public class PaperTrailEventLog { 11 | private List events; 12 | 13 | @Data 14 | @JsonIgnoreProperties(ignoreUnknown = true) 15 | public static class Event { 16 | private String message; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/admin/telegram/handler/log/service/LogHandler.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.admin.telegram.handler.log.service; 2 | 3 | import com.whiskels.notifier.infrastructure.admin.telegram.BotMessage; 4 | import com.whiskels.notifier.infrastructure.admin.telegram.Command; 5 | import com.whiskels.notifier.infrastructure.admin.telegram.CommandHandler; 6 | import com.whiskels.notifier.infrastructure.admin.telegram.DocumentBotMessage; 7 | import lombok.RequiredArgsConstructor; 8 | import org.telegram.telegrambots.meta.api.methods.send.SendDocument; 9 | import org.telegram.telegrambots.meta.api.objects.InputFile; 10 | 11 | import java.io.ByteArrayInputStream; 12 | 13 | import static com.whiskels.notifier.infrastructure.admin.telegram.Command.GET_LOGS; 14 | 15 | @RequiredArgsConstructor 16 | public class LogHandler implements CommandHandler { 17 | private static final String MESSAGE = "Application logs"; 18 | private static final String FILE_NAME = "logs.txt"; 19 | private final LogService logService; 20 | 21 | @Override 22 | public BotMessage handle(final String userId, final String message) { 23 | var document = SendDocument.builder() 24 | .chatId(userId) 25 | .caption(MESSAGE) 26 | .document(getFile()) 27 | .build(); 28 | return DocumentBotMessage.of(document); 29 | } 30 | 31 | private InputFile getFile() { 32 | ByteArrayInputStream bais = new ByteArrayInputStream(logService.getLogsAsByteArray()); 33 | return new InputFile(bais, FILE_NAME); 34 | } 35 | 36 | @Override 37 | public Command getCommand() { 38 | return GET_LOGS; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/admin/telegram/handler/log/service/LogService.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.admin.telegram.handler.log.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.SneakyThrows; 5 | 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.OutputStreamWriter; 8 | import java.io.Writer; 9 | import java.nio.charset.StandardCharsets; 10 | 11 | 12 | @RequiredArgsConstructor 13 | public class LogService { 14 | private final PapertrailClient papertrailClient; 15 | 16 | @SneakyThrows 17 | public byte[] getLogsAsByteArray() { 18 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 19 | Writer writer = new OutputStreamWriter(baos, StandardCharsets.UTF_8); 20 | writer.write(String.join("\n", papertrailClient.getLogs())); 21 | writer.flush(); 22 | return baos.toByteArray(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/admin/telegram/handler/log/service/PapertrailClient.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.admin.telegram.handler.log.service; 2 | 3 | import com.whiskels.notifier.infrastructure.admin.telegram.handler.log.domain.PaperTrailEventLog; 4 | import com.whiskels.notifier.utilities.collections.StreamUtil; 5 | import feign.RequestInterceptor; 6 | import feign.RequestTemplate; 7 | import feign.Retryer; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 10 | import org.springframework.cloud.openfeign.FeignClient; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RequestMethod; 14 | 15 | import java.util.Collections; 16 | import java.util.List; 17 | import java.util.Optional; 18 | 19 | @ConditionalOnProperty(PapertrailClient.PAPERTRAIL_API_TOKEN) 20 | @FeignClient(name = "papertrailClient", 21 | url = "${papertrail.url:https://papertrailapp.com/api/v1/events/search.json}", 22 | configuration = PapertrailClient.PapertrailClientConfig.class) 23 | public interface PapertrailClient { 24 | String PAPERTRAIL_HEADER = "X-Papertrail-Token"; 25 | String PAPERTRAIL_API_TOKEN = "PAPERTRAIL_API_TOKEN"; 26 | String PAPERTRAIL_API_TOKEN_SPEL = "${" + PAPERTRAIL_API_TOKEN + "}"; 27 | 28 | @RequestMapping(method = RequestMethod.GET) 29 | PaperTrailEventLog getLog(); 30 | 31 | default List getLogs() { 32 | return Optional.ofNullable(getLog()) 33 | .map(PaperTrailEventLog::getEvents) 34 | .map(events -> StreamUtil.map(events, PaperTrailEventLog.Event::getMessage)) 35 | .orElse(Collections.emptyList()); 36 | } 37 | 38 | record AuthRequestInterceptor(String token) implements RequestInterceptor { 39 | @Override 40 | public void apply(RequestTemplate template) { 41 | template.header(PAPERTRAIL_HEADER, token); 42 | } 43 | } 44 | 45 | class PapertrailClientConfig { 46 | @Bean 47 | AuthRequestInterceptor interceptor(@Value(PAPERTRAIL_API_TOKEN_SPEL) final String token) { 48 | return new AuthRequestInterceptor(token); 49 | } 50 | 51 | @Bean 52 | Retryer retryer( 53 | @Value("${papertrail.retry.period-millis:5000}") final Long periodMillis, 54 | @Value("${papertrail.retry.max-period-millis:15000}") final Long maxPeriodMillis, 55 | @Value("${papertrail.retry.max-attempts:3}") final int maxAttempts 56 | ) { 57 | return new Retryer.Default(periodMillis, maxPeriodMillis, maxAttempts); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/admin/telegram/util/TelegramUtil.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.admin.telegram.util; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup; 6 | import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton; 7 | 8 | import java.util.List; 9 | 10 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 11 | public class TelegramUtil { 12 | public static String extractArguments(String message) { 13 | return message.substring(message.indexOf(" ") + 1); 14 | } 15 | 16 | public static InlineKeyboardMarkup createMarkup(List> keyboard) { 17 | var markup = new InlineKeyboardMarkup(); 18 | markup.setKeyboard(keyboard); 19 | return markup; 20 | } 21 | 22 | public static InlineKeyboardButton button(String text, String callbackData) { 23 | var button = new InlineKeyboardButton(); 24 | button.setText(text); 25 | button.setCallbackData(callbackData); 26 | return button; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/config/clock/ClockConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.config.clock; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.context.annotation.Profile; 7 | 8 | import java.time.Clock; 9 | import java.time.Instant; 10 | import java.time.ZoneId; 11 | 12 | @Configuration 13 | @Profile("!test") 14 | class ClockConfiguration { 15 | @Bean 16 | @Profile("!mock") 17 | Clock defaultClock(@Value("${common.timezone}") String timeZone) { 18 | return Clock.system(ZoneId.of(timeZone)); 19 | } 20 | 21 | @Bean 22 | @Profile("mock") 23 | Clock mockedClock() { 24 | return Clock.fixed(Instant.parse("2025-01-01T10:15:30Z"), ZoneId.of("UTC")); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/config/feign/FeignProxyConfig.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.config.feign; 2 | 3 | import feign.Client; 4 | import okhttp3.Authenticator; 5 | import okhttp3.ConnectionPool; 6 | import okhttp3.Credentials; 7 | import okhttp3.OkHttpClient; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.http.HttpHeaders; 12 | 13 | import java.net.InetSocketAddress; 14 | import java.net.Proxy; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | public class FeignProxyConfig { 18 | @Value("${feign.proxy.max-idle-connections:5}") 19 | private int maxIdleConnections; 20 | @Value("${feign.proxy.keep-alive-duration-minutes:5}") 21 | private int keepAliveDurationMinutes; 22 | 23 | @Bean 24 | @ConditionalOnBean(ProxyPropertiesProvider.class) 25 | public Client proxiedFeignClient(final ProxyPropertiesProvider propertiesProvider) { 26 | return new feign.okhttp.OkHttpClient( 27 | proxiedHttpClient(propertiesProvider, maxIdleConnections, keepAliveDurationMinutes) 28 | ); 29 | } 30 | 31 | private static OkHttpClient proxiedHttpClient(final ProxyPropertiesProvider propertiesProvider, 32 | final int maxIdleConnections, 33 | final int keepAliveDurationMinutes 34 | ) { 35 | Authenticator proxyAuthenticator = (_, response) -> { 36 | String credential = Credentials.basic(propertiesProvider.getUser(), propertiesProvider.getPassword()); 37 | return response.request().newBuilder() 38 | .header(HttpHeaders.PROXY_AUTHORIZATION, credential) 39 | .build(); 40 | }; 41 | 42 | return new OkHttpClient.Builder() 43 | .proxy(createProxy(propertiesProvider)) 44 | .proxyAuthenticator(proxyAuthenticator) 45 | .connectionPool(new ConnectionPool(maxIdleConnections, keepAliveDurationMinutes, TimeUnit.MINUTES)) 46 | .build(); 47 | } 48 | 49 | private static Proxy createProxy(ProxyPropertiesProvider propertiesProvider) { 50 | return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(propertiesProvider.getHost(), propertiesProvider.getPort())); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/config/feign/FixieProxyPropertiesProvider.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.config.feign; 2 | 3 | import lombok.Getter; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 5 | import org.springframework.core.env.Environment; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.Objects; 9 | 10 | @Getter 11 | @Component 12 | @ConditionalOnProperty(FixieProxyPropertiesProvider.FIXIE_ENV_VAR) 13 | class FixieProxyPropertiesProvider implements ProxyPropertiesProvider { 14 | static final String FIXIE_ENV_VAR = "FIXIE_URL"; 15 | 16 | private final String user; 17 | private final String password; 18 | private final String host; 19 | private final int port; 20 | 21 | public FixieProxyPropertiesProvider(final Environment environment) { 22 | final String fixieUrl = environment.getProperty(FIXIE_ENV_VAR); 23 | Objects.requireNonNull(fixieUrl); 24 | final String[] fixieValues = fixieUrl.split("[/(:\\/@)/]+"); 25 | this.user = fixieValues[1]; 26 | this.password = fixieValues[2]; 27 | this.host = fixieValues[3]; 28 | this.port = Integer.parseInt(fixieValues[4]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/config/feign/ProxyPropertiesProvider.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.config.feign; 2 | 3 | 4 | interface ProxyPropertiesProvider { 5 | String getUser(); 6 | 7 | String getPassword(); 8 | 9 | String getHost(); 10 | 11 | int getPort(); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/domain/AbstractBaseEntity.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.domain; 2 | 3 | import javax.persistence.Access; 4 | import javax.persistence.AccessType; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.GenerationType; 7 | import javax.persistence.Id; 8 | import javax.persistence.MappedSuperclass; 9 | import javax.persistence.SequenceGenerator; 10 | import lombok.AccessLevel; 11 | import lombok.EqualsAndHashCode; 12 | import lombok.Getter; 13 | import lombok.NoArgsConstructor; 14 | import lombok.Setter; 15 | 16 | 17 | @MappedSuperclass 18 | @Access(AccessType.FIELD) 19 | @Getter 20 | @Setter 21 | @EqualsAndHashCode 22 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 23 | public abstract class AbstractBaseEntity { 24 | public static final int START_SEQ = 100000; 25 | 26 | @Id 27 | @SequenceGenerator(name = "global_seq", sequenceName = "global_seq", allocationSize = 1, initialValue = START_SEQ) 28 | @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "global_seq") 29 | protected Integer id; 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/googlesheets/GoogleCredentialConfigurationProperties.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.googlesheets; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | 7 | @Getter 8 | @Setter 9 | @ConfigurationProperties(prefix = GoogleCredentialProvider.GOOGLE_CREDENTIALS_PREFIX) 10 | class GoogleCredentialConfigurationProperties { 11 | private String appName; 12 | private String credentials; 13 | private String email; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/googlesheets/GoogleSheetsReader.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.googlesheets; 2 | 3 | import com.google.api.services.sheets.v4.Sheets; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.context.annotation.Lazy; 7 | import org.springframework.stereotype.Component; 8 | 9 | import javax.annotation.Nonnull; 10 | import java.io.IOException; 11 | import java.util.List; 12 | 13 | import static com.whiskels.notifier.infrastructure.googlesheets.GoogleCredentialProvider.JSON_FACTORY; 14 | 15 | @Slf4j 16 | @Lazy 17 | @Component 18 | @RequiredArgsConstructor 19 | public class GoogleSheetsReader { 20 | private final GoogleCredentialProvider credentialProvider; 21 | 22 | @Nonnull 23 | public List> read(@Nonnull final String spreadsheetId, @Nonnull final String range) { 24 | try { 25 | return getSheets() 26 | .spreadsheets() 27 | .values() 28 | .get(spreadsheetId, range) 29 | .execute() 30 | .getValues(); 31 | } catch (IOException e) { 32 | throw new RuntimeException(e.getMessage()); 33 | } 34 | } 35 | 36 | private Sheets getSheets() { 37 | return new Sheets.Builder(credentialProvider.getHttpTransport(), JSON_FACTORY, credentialProvider.getCredential()) 38 | .setApplicationName(credentialProvider.getProperties().getAppName()) 39 | .build(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/mock/MockUtil.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.mock; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 6 | import lombok.NoArgsConstructor; 7 | import lombok.SneakyThrows; 8 | 9 | import java.io.InputStream; 10 | 11 | import static lombok.AccessLevel.PRIVATE; 12 | 13 | @NoArgsConstructor(access = PRIVATE) 14 | public class MockUtil { 15 | private static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new JavaTimeModule()); 16 | 17 | @SneakyThrows 18 | public static T read(String path, TypeReference ref) { 19 | return MAPPER.readValue(getResourceFileAsInputStream(path), ref); 20 | } 21 | 22 | private static InputStream getResourceFileAsInputStream(String fileName) { 23 | ClassLoader classLoader = MockUtil.class.getClassLoader(); 24 | return classLoader.getResourceAsStream(fileName); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/report/ReportExecutor.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.report; 2 | 3 | import com.whiskels.notifier.reporting.ReportType; 4 | import com.whiskels.notifier.reporting.service.Report; 5 | 6 | public interface ReportExecutor { 7 | void send(ReportType type, Report report); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/report/slack/SlackClient.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.report.slack; 2 | 3 | import com.slack.api.Slack; 4 | import com.slack.api.webhook.Payload; 5 | import lombok.AllArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.io.IOException; 11 | 12 | @Slf4j 13 | @Component 14 | @AllArgsConstructor 15 | public class SlackClient { 16 | private final Slack slack; 17 | 18 | public void send(String webhook, Payload payload) throws IOException { 19 | var response = slack.send(webhook, payload); 20 | if (response.getCode() != HttpStatus.OK.value()) { 21 | throw new RuntimeException(STR."Error on slack call: \{response.getCode()}: \{response.getMessage()}"); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/report/slack/SlackPayloadMapper.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.report.slack; 2 | 3 | import com.slack.api.webhook.Payload; 4 | import com.whiskels.notifier.infrastructure.report.slack.builder.SlackPayloadBuilder; 5 | import com.whiskels.notifier.reporting.service.Report; 6 | import org.springframework.stereotype.Service; 7 | 8 | import static java.util.Objects.nonNull; 9 | 10 | @Service 11 | public class SlackPayloadMapper { 12 | public Payload map(final Report report) { 13 | var builder = SlackPayloadBuilder.builder() 14 | .header(report.getHeader()); 15 | if (report.isNotifyChannel()) { 16 | builder.notifyChannel(); 17 | } 18 | 19 | if (nonNull(report.getBanner())) { 20 | builder.header(report.getBanner()); 21 | } 22 | 23 | report.getBody().forEach(block -> { 24 | if (nonNull(block.mediaContentUrl())) { 25 | builder.block(block.text(), block.mediaContentUrl()); 26 | } else { 27 | builder.block(block.text()); 28 | } 29 | }); 30 | 31 | return builder.build(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/report/slack/SlackReportExecutor.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.report.slack; 2 | 3 | import com.whiskels.notifier.infrastructure.report.ReportExecutor; 4 | import com.whiskels.notifier.reporting.ReportType; 5 | import com.whiskels.notifier.reporting.service.Report; 6 | import lombok.AllArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | import java.io.IOException; 10 | import java.util.Map; 11 | 12 | import static org.springframework.util.CollectionUtils.isEmpty; 13 | 14 | @Slf4j 15 | @AllArgsConstructor 16 | public class SlackReportExecutor implements ReportExecutor { 17 | private final SlackClient slackClient; 18 | private final SlackPayloadMapper slackPayloadMapper; 19 | private final Map webhookMappings; 20 | 21 | @Override 22 | public void send(ReportType type, Report report) { 23 | if (isEmpty(webhookMappings) || !webhookMappings.containsKey(type)) { 24 | log.error("No webhook mapping for type {}", type); 25 | return; 26 | } 27 | try { 28 | slackClient.send(webhookMappings.get(type), slackPayloadMapper.map(report)); 29 | } catch (IOException e) { 30 | throw new IllegalArgumentException(e); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/report/slack/builder/AccessoryBlock.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.report.slack.builder; 2 | 3 | import com.slack.api.model.block.element.BlockElement; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | 7 | @Data 8 | @EqualsAndHashCode(callSuper = true) 9 | public class AccessoryBlock extends BlockElement { 10 | private final String type = "image"; 11 | private final String image_url; 12 | private final String alt_text = "Funny pic"; 13 | 14 | public AccessoryBlock(String image_url) { 15 | this.image_url = image_url; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/report/slack/config/SlackConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.report.slack.config; 2 | 3 | import com.slack.api.Slack; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | @Configuration(proxyBeanMethods = false) 8 | class SlackConfiguration { 9 | @Bean 10 | Slack slack() { 11 | return Slack.getInstance(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/report/webhook/FeignWebhookSink.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.report.webhook; 2 | 3 | import feign.Headers; 4 | import feign.RequestLine; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.RequestBody; 7 | 8 | import java.net.URI; 9 | 10 | public interface FeignWebhookSink { 11 | @RequestLine("POST") 12 | @Headers("Content-Type: application/json") 13 | ResponseEntity send(URI url, @RequestBody FeignWebhookSinkDto dto); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/report/webhook/FeignWebhookSinkConfig.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.report.webhook; 2 | 3 | import feign.Feign; 4 | import feign.Target; 5 | import feign.codec.Decoder; 6 | import feign.codec.Encoder; 7 | import org.springframework.cloud.openfeign.FeignClientsConfiguration; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.context.annotation.Import; 11 | 12 | @Configuration 13 | @Import(FeignClientsConfiguration.class) 14 | class FeignWebhookSinkConfig { 15 | @Bean 16 | FeignWebhookSink feignWebhookSink(Encoder encoder, Decoder decoder) { 17 | return Feign.builder() 18 | .encoder(encoder) 19 | .decoder(decoder) 20 | .target(Target.EmptyTarget.create(FeignWebhookSink.class)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/report/webhook/FeignWebhookSinkDto.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.report.webhook; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class FeignWebhookSinkDto { 7 | String message; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/report/webhook/WebhookSinkExecutor.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.report.webhook; 2 | 3 | import com.whiskels.notifier.infrastructure.report.ReportExecutor; 4 | import com.whiskels.notifier.reporting.ReportType; 5 | import com.whiskels.notifier.reporting.service.Report; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.util.CollectionUtils; 9 | 10 | import java.net.URI; 11 | import java.util.Map; 12 | 13 | import static com.whiskels.notifier.utilities.formatters.StringFormatter.COLLECTOR_NEW_LINE; 14 | 15 | @Slf4j 16 | @RequiredArgsConstructor 17 | public class WebhookSinkExecutor implements ReportExecutor { 18 | private final FeignWebhookSink feignWebhookSink; 19 | private final Map webhookMappings; 20 | 21 | @Override 22 | public void send(final ReportType type, final Report report) { 23 | if (CollectionUtils.isEmpty(webhookMappings) || !webhookMappings.containsKey(type)) { 24 | log.error("No webhook mapping for type {}", type); 25 | return; 26 | } 27 | try { 28 | final var dto = new FeignWebhookSinkDto(); 29 | var reportBody = report.getBody().stream() 30 | .map(Report.ReportBodyBlock::text) 31 | .collect(COLLECTOR_NEW_LINE); 32 | dto.setMessage(STR."\{report.getHeader()}\n\n\{reportBody}"); 33 | final var response = feignWebhookSink.send(new URI(webhookMappings.get(type)), dto); 34 | 35 | if (!response.getStatusCode().is2xxSuccessful()) { 36 | log.error("Webhook sink {} failed: {}", type, response); 37 | } 38 | } catch (Exception e) { 39 | throw new RuntimeException(e); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/infrastructure/repository/AbstractRepository.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.repository; 2 | 3 | import com.whiskels.notifier.reporting.domain.AbstractTimeStampedEntity; 4 | import org.springframework.data.jpa.repository.Modifying; 5 | import org.springframework.data.jpa.repository.Query; 6 | import org.springframework.data.repository.NoRepositoryBean; 7 | import org.springframework.data.repository.Repository; 8 | import org.springframework.data.repository.query.Param; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | import java.time.LocalDateTime; 12 | import java.util.List; 13 | 14 | @NoRepositoryBean 15 | public interface AbstractRepository extends Repository { 16 | void save(T audit); 17 | 18 | List saveAll(Iterable var1); 19 | 20 | @Transactional 21 | @Modifying 22 | @Query("delete from #{#entityName} a where a.loadDateTime < :date") 23 | int deleteByDateBefore(@Param("date") LocalDateTime date); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/ReportService.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.List; 5 | 6 | public interface ReportService { 7 | void executeReport(@Nonnull ReportType type); 8 | 9 | List getReportTypes(); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/ReportType.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting; 2 | 3 | public enum ReportType { 4 | CUSTOMER_BIRTHDAY, 5 | CUSTOMER_DEBT, 6 | CUSTOMER_PAYMENT, 7 | EMPLOYEE_EVENT, 8 | CURRENCY_RATE 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/ScheduledReportTaskSubmitter.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.boot.context.event.ApplicationReadyEvent; 6 | import org.springframework.context.ApplicationListener; 7 | import org.springframework.scheduling.TaskScheduler; 8 | import org.springframework.scheduling.support.CronTrigger; 9 | 10 | import javax.annotation.Nonnull; 11 | import java.time.ZoneId; 12 | import java.util.Map; 13 | import java.util.TimeZone; 14 | 15 | import static com.whiskels.notifier.utilities.formatters.StringFormatter.COLLECTOR_NEW_LINE; 16 | import static org.springframework.util.CollectionUtils.isEmpty; 17 | 18 | @RequiredArgsConstructor 19 | @Slf4j 20 | public class ScheduledReportTaskSubmitter implements ApplicationListener { 21 | private final TaskScheduler executor; 22 | private final ReportService service; 23 | private final Map reportCronMap; 24 | private final String timeZone; 25 | 26 | @Override 27 | public void onApplicationEvent(@Nonnull ApplicationReadyEvent event) { 28 | scheduleReports(); 29 | } 30 | 31 | private void scheduleReports() { 32 | if (isEmpty(reportCronMap)) { 33 | log.error("No cron triggers found for reports"); 34 | return; 35 | } 36 | log.info("Scheduling reports: \n{}", 37 | reportCronMap.entrySet().stream() 38 | .map(entry -> String.format(" %-8s -> %s", entry.getKey(), entry.getValue())) 39 | .collect(COLLECTOR_NEW_LINE)); 40 | 41 | reportCronMap.forEach((type, cron) -> 42 | executor.schedule(() -> service.executeReport(type), 43 | new CronTrigger(cron, TimeZone.getTimeZone(ZoneId.of(timeZone)) 44 | ) 45 | ) 46 | ); 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/domain/AbstractTimeStampedEntity.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.domain; 2 | 3 | import com.whiskels.notifier.infrastructure.domain.AbstractBaseEntity; 4 | import javax.persistence.Column; 5 | import javax.persistence.EntityListeners; 6 | import javax.persistence.MappedSuperclass; 7 | import javax.validation.constraints.NotNull; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | import lombok.Setter; 11 | import org.hibernate.Hibernate; 12 | 13 | import java.time.LocalDateTime; 14 | import java.util.Objects; 15 | 16 | @MappedSuperclass 17 | @Getter 18 | @Setter 19 | @EntityListeners(AbstractTimeStampedEntityListener.class) 20 | @NoArgsConstructor 21 | public abstract class AbstractTimeStampedEntity extends AbstractBaseEntity { 22 | @Column(nullable = false) 23 | @NotNull 24 | protected LocalDateTime loadDateTime; 25 | 26 | @Override 27 | public boolean equals(Object o) { 28 | if (this == o) return true; 29 | if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; 30 | AbstractTimeStampedEntity that = (AbstractTimeStampedEntity) o; 31 | return id != null && Objects.equals(id, that.id); 32 | } 33 | 34 | @Override 35 | public int hashCode() { 36 | return getClass().hashCode(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/domain/AbstractTimeStampedEntityListener.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.domain; 2 | 3 | import javax.persistence.PrePersist; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.time.Clock; 9 | import java.time.LocalDateTime; 10 | 11 | @Slf4j 12 | @Component 13 | public class AbstractTimeStampedEntityListener { 14 | 15 | private Clock clock; 16 | 17 | @Autowired //JPA spec requires @PrePersist classes to have no-args constructor 18 | public void setClock(Clock clock) { 19 | this.clock = clock; 20 | } 21 | 22 | @PrePersist 23 | void prePersist(AbstractTimeStampedEntity entity) { 24 | log.debug("Setting load time and date to timestamped entity {}", entity); 25 | entity.setLoadDateTime(LocalDateTime.now(clock)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/domain/HasBirthday.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.domain; 2 | 3 | import java.time.LocalDate; 4 | import java.util.Comparator; 5 | 6 | public interface HasBirthday { 7 | static Comparator comparator() { 8 | return Comparator.comparing(T::birthday, Comparator.nullsLast(Comparator.naturalOrder())).thenComparing(T::name, Comparator.nullsLast(Comparator.naturalOrder())); 9 | } 10 | 11 | 12 | String name(); 13 | 14 | LocalDate birthday(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/exception/ExceptionEvent.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.exception; 2 | 3 | import com.whiskels.notifier.reporting.ReportType; 4 | 5 | public record ExceptionEvent(String message, ReportType type) { 6 | public static ExceptionEvent of(String message, ReportType type) { 7 | return new ExceptionEvent(message, type); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/DataFetchService.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service; 2 | 3 | import javax.annotation.Nonnull; 4 | 5 | public interface DataFetchService { 6 | @Nonnull 7 | ReportData fetch(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/GenericReportService.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service; 2 | 3 | import com.whiskels.notifier.reporting.ReportType; 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | 7 | @RequiredArgsConstructor 8 | public class GenericReportService { 9 | @Getter 10 | private final ReportType type; 11 | private final DataFetchService dataFetchService; 12 | private final ReportMessageConverter messageCreator; 13 | 14 | public Iterable prepareReports() { 15 | var data = dataFetchService.fetch(); 16 | return messageCreator.convert(data); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/Report.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Getter; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | @Builder 13 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 14 | @Getter 15 | @EqualsAndHashCode 16 | public class Report { 17 | private final String header; 18 | @Builder.Default 19 | private final List body = new ArrayList<>(); 20 | private final boolean notifyChannel; 21 | private final String banner; 22 | 23 | public Report addBody(String text) { 24 | body.add(ReportBodyBlock.builder().text(text).build()); 25 | return this; 26 | } 27 | 28 | public Report addBody(String text, String mediaContentUrl) { 29 | body.add(ReportBodyBlock.builder().text(text).mediaContentUrl(mediaContentUrl).build()); 30 | return this; 31 | } 32 | 33 | @Builder 34 | public record ReportBodyBlock(String text, String mediaContentUrl) { 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/ReportData.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service; 2 | 3 | 4 | import java.time.LocalDate; 5 | import java.util.List; 6 | 7 | public record ReportData(List data, LocalDate requestDate) { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/ReportMessageConverter.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service; 2 | 3 | import javax.annotation.Nonnull; 4 | 5 | @FunctionalInterface 6 | public interface ReportMessageConverter { 7 | @Nonnull 8 | Iterable convert(@Nonnull final ReportData data); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/ReportServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service; 2 | 3 | import com.whiskels.notifier.infrastructure.report.ReportExecutor; 4 | import com.whiskels.notifier.reporting.ReportService; 5 | import com.whiskels.notifier.reporting.ReportType; 6 | import com.whiskels.notifier.reporting.exception.ExceptionEvent; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.context.ApplicationEventPublisher; 9 | import org.springframework.stereotype.Component; 10 | 11 | import javax.annotation.Nonnull; 12 | import java.util.Collection; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.function.Function; 16 | import java.util.stream.Collectors; 17 | 18 | @Slf4j 19 | @Component 20 | public class ReportServiceImpl implements ReportService { 21 | private final Map> processors; 22 | private final ApplicationEventPublisher publisher; 23 | private final List reportExecutors; 24 | 25 | public ReportServiceImpl(final Collection> processors, 26 | final List reportExecutors, 27 | final ApplicationEventPublisher publisher) { 28 | this.processors = processors.stream() 29 | .collect(Collectors.toMap(GenericReportService::getType, Function.identity())); 30 | this.publisher = publisher; 31 | this.reportExecutors = reportExecutors; 32 | } 33 | 34 | @Override 35 | public void executeReport(@Nonnull final ReportType type) { 36 | try { 37 | log.info("[{}] Starting...", type); 38 | var processor = processors.get(type); 39 | 40 | if (processor == null) { 41 | throw new IllegalStateException(STR."No processor found for type \{type}"); 42 | } 43 | 44 | for (var report : processor.prepareReports()) { 45 | reportExecutors.forEach(executor -> executor.send(type, report)); 46 | } 47 | 48 | log.info("[{}] ...Completed", type); 49 | } catch (Exception e) { 50 | String exceptionMessage = STR."[\{type}] Exception while processing: \{e.getLocalizedMessage()}"; 51 | publisher.publishEvent(ExceptionEvent.of(exceptionMessage, type)); 52 | throw new RuntimeException(e); 53 | } 54 | } 55 | 56 | @Override 57 | public List getReportTypes() { 58 | return processors.keySet().stream().toList(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/SingleReportMessageConverter.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service; 2 | 3 | import com.slack.api.webhook.Payload; 4 | import javax.annotation.Nonnull; 5 | import javax.annotation.Nullable; 6 | 7 | @FunctionalInterface 8 | public interface SingleReportMessageConverter { 9 | @Nullable 10 | Payload convert(@Nonnull final ReportData data); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/audit/AuditDataFetchResult.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.audit; 2 | 3 | import com.whiskels.notifier.reporting.ReportType; 4 | 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.Target; 7 | 8 | import static java.lang.annotation.ElementType.METHOD; 9 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 10 | 11 | @Retention(RUNTIME) 12 | @Target(METHOD) 13 | public @interface AuditDataFetchResult { 14 | ReportType reportType(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/audit/AuditDataFetchResultAspect.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.audit; 2 | 3 | import com.whiskels.notifier.reporting.service.ReportData; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.aspectj.lang.ProceedingJoinPoint; 7 | import org.aspectj.lang.annotation.Around; 8 | import org.aspectj.lang.annotation.Aspect; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.context.annotation.EnableAspectJAutoProxy; 11 | 12 | @Slf4j 13 | @Aspect 14 | @Configuration 15 | @EnableAspectJAutoProxy 16 | @RequiredArgsConstructor 17 | class AuditDataFetchResultAspect { 18 | private final LoadAuditRepository auditRepository; 19 | 20 | @Around(value = "@annotation(dataFetchResultAudit)") 21 | public Object aroundAdvice( 22 | final ProceedingJoinPoint joinPoint, 23 | final AuditDataFetchResult dataFetchResultAudit 24 | ) throws Throwable { 25 | var reportType = dataFetchResultAudit.reportType(); 26 | log.info("[{}] Fetching...", reportType); 27 | 28 | Object result = joinPoint.proceed(); 29 | 30 | int count = 0; 31 | if (result instanceof ReportData) { 32 | count = ((ReportData) result).data().size(); 33 | } else if (result != null) { 34 | count = 1; 35 | } 36 | auditRepository.save(LoadAudit.loadAudit(count, reportType)); 37 | log.info("[{}] DataFetchResult Audit results saved: {} from method: {}", reportType, count, joinPoint.getSignature().getName()); 38 | return result; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/audit/LoadAudit.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.audit; 2 | 3 | import com.whiskels.notifier.reporting.ReportType; 4 | import com.whiskels.notifier.reporting.domain.AbstractTimeStampedEntity; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import org.hibernate.Hibernate; 10 | 11 | import javax.persistence.Column; 12 | import javax.persistence.Entity; 13 | import javax.persistence.Enumerated; 14 | import javax.persistence.Table; 15 | import javax.validation.constraints.NotBlank; 16 | import javax.validation.constraints.PositiveOrZero; 17 | import java.util.Objects; 18 | 19 | import static javax.persistence.EnumType.STRING; 20 | 21 | @Entity 22 | @Table(name = "load_audit") 23 | @AllArgsConstructor 24 | @NoArgsConstructor 25 | @Getter 26 | @Builder 27 | public class LoadAudit extends AbstractTimeStampedEntity { 28 | @Enumerated(STRING) 29 | @Column(nullable = false) 30 | @NotBlank 31 | private ReportType reportType; 32 | 33 | @Column(nullable = false) 34 | @PositiveOrZero 35 | private int count; 36 | 37 | public static LoadAudit loadAudit(int count, ReportType reportType) { 38 | return LoadAudit.builder() 39 | .count(count) 40 | .reportType(reportType) 41 | .build(); 42 | } 43 | 44 | @Override 45 | public boolean equals(Object o) { 46 | if (this == o) return true; 47 | if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; 48 | LoadAudit loadAudit = (LoadAudit) o; 49 | return id != null && Objects.equals(id, loadAudit.id); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/audit/LoadAuditRepository.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.audit; 2 | 3 | import com.whiskels.notifier.infrastructure.repository.AbstractRepository; 4 | import com.whiskels.notifier.reporting.ReportType; 5 | import org.springframework.context.annotation.Lazy; 6 | import org.springframework.data.domain.PageRequest; 7 | import org.springframework.data.jpa.repository.Query; 8 | import org.springframework.data.repository.query.Param; 9 | import org.springframework.stereotype.Repository; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | import java.time.LocalDateTime; 13 | import java.util.List; 14 | import java.util.Optional; 15 | 16 | @Lazy 17 | @Repository 18 | public interface LoadAuditRepository extends AbstractRepository { 19 | @Transactional(readOnly = true) 20 | @Query("select max(la.loadDateTime) from LoadAudit la where la.reportType=:reportType") 21 | Optional findLastUpdateDateTime(@Param("reportType") ReportType reportType); 22 | 23 | @Transactional(readOnly = true) 24 | @Query("from LoadAudit a order by a.loadDateTime desc") 25 | List getLast(PageRequest pageRequest); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/cleaner/DatabaseCleaner.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.cleaner; 2 | 3 | import com.whiskels.notifier.infrastructure.repository.AbstractRepository; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.aop.framework.AopProxyUtils; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.scheduling.annotation.Scheduled; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.time.Clock; 11 | import java.time.LocalDateTime; 12 | import java.util.List; 13 | 14 | import static com.whiskels.notifier.utilities.DateTimeUtil.subtractWorkingDays; 15 | import static java.time.LocalDate.now; 16 | 17 | @Slf4j 18 | @Component 19 | class DatabaseCleaner { 20 | private final List> repositories; 21 | private final int workingDaysToDeleteAfter; 22 | private final Clock clock; 23 | 24 | public DatabaseCleaner(List> repositories, 25 | @Value("${database.cleaner.working-days-to-delete-after:14}") int workingDaysToDeleteAfter, 26 | Clock clock) { 27 | this.repositories = repositories; 28 | this.workingDaysToDeleteAfter = workingDaysToDeleteAfter; 29 | this.clock = clock; 30 | } 31 | 32 | @Scheduled(cron = "${database.cleaner.cron:0 0 9 * * *}", zone = "${common.timezone}") 33 | void deleteOldEntries() { 34 | LocalDateTime deleteBeforeDate = subtractWorkingDays(now(clock), workingDaysToDeleteAfter).atStartOfDay(); 35 | repositories.forEach(repository -> { 36 | int deletedCount = repository.deleteByDateBefore(deleteBeforeDate); 37 | log.info("{} Deleted {} old entries loaded before {}", 38 | AopProxyUtils.proxiedUserInterfaces(repository)[0].getSimpleName(), deletedCount, deleteBeforeDate); 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/birthday/config/CustomerBirthdayInfoFetchConfig.java: -------------------------------------------------------------------------------- 1 | 2 | package com.whiskels.notifier.reporting.service.customer.birthday.config; 3 | 4 | import com.whiskels.notifier.infrastructure.googlesheets.GoogleSheetsReader; 5 | import com.whiskels.notifier.reporting.service.DataFetchService; 6 | import com.whiskels.notifier.reporting.service.customer.birthday.domain.CustomerBirthdayInfo; 7 | import com.whiskels.notifier.reporting.service.customer.birthday.fetch.CustomerBirthdayInfoFetchService; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.context.annotation.Primary; 12 | 13 | import java.time.Clock; 14 | 15 | import static com.whiskels.notifier.reporting.service.customer.birthday.config.CustomerBirthdayInfoFetchConfig.CUSTOMER_BIRTHDAY_PROPERTIES_PREFIX; 16 | 17 | @Configuration 18 | @ConditionalOnProperty(prefix = CUSTOMER_BIRTHDAY_PROPERTIES_PREFIX, name = {"spreadsheet", "cell-range"}) 19 | class CustomerBirthdayInfoFetchConfig { 20 | public static final String CUSTOMER_BIRTHDAY_PROPERTIES_PREFIX = "report.parameters.customer-birthday"; 21 | 22 | @Bean 23 | @Primary 24 | DataFetchService customerBirthdayInfoDataFetchService( 25 | final Clock clock, 26 | final GoogleSheetsReader spreadsheetLoader, 27 | final CustomerBirthdaySpreadsheetProperties properties 28 | ) { 29 | return new CustomerBirthdayInfoFetchService(clock, spreadsheetLoader, properties.getSpreadsheet(), properties.getCellRange()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/birthday/config/CustomerBirthdaySpreadsheetProperties.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.birthday.config; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.stereotype.Component; 7 | 8 | import static com.whiskels.notifier.reporting.service.customer.birthday.config.CustomerBirthdayInfoFetchConfig.CUSTOMER_BIRTHDAY_PROPERTIES_PREFIX; 9 | 10 | 11 | @Getter 12 | @Setter 13 | @ConfigurationProperties(prefix = CUSTOMER_BIRTHDAY_PROPERTIES_PREFIX) 14 | @Component 15 | public class CustomerBirthdaySpreadsheetProperties { 16 | private String spreadsheet; 17 | private String cellRange; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/birthday/convert/ReportContext.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.birthday.convert; 2 | 3 | import com.whiskels.notifier.reporting.service.customer.birthday.domain.CustomerBirthdayInfo; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | 7 | import java.time.LocalDate; 8 | import java.util.function.BiPredicate; 9 | import java.util.function.Function; 10 | import java.util.function.Predicate; 11 | 12 | @Getter 13 | @AllArgsConstructor 14 | public class ReportContext { 15 | private final Function headerMapper; 16 | /** 17 | * Determines whether report creation should be skipped if predicate returns false 18 | */ 19 | private final Predicate skipEmptyPredicate; 20 | private final BiPredicate predicate; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/birthday/convert/context/BeforeEventReportContext.java: -------------------------------------------------------------------------------- 1 | 2 | 3 | package com.whiskels.notifier.reporting.service.customer.birthday.convert.context; 4 | 5 | 6 | import com.whiskels.notifier.reporting.service.customer.birthday.convert.ReportContext; 7 | 8 | import static com.whiskels.notifier.utilities.DateTimeUtil.isSameDay; 9 | import static com.whiskels.notifier.utilities.DateTimeUtil.reportDate; 10 | 11 | public class BeforeEventReportContext extends ReportContext { 12 | public BeforeEventReportContext(String headerPrefix, int daysBefore) { 13 | super(date -> STR."\{headerPrefix}\{reportDate(date)}", 14 | _ignored -> true, 15 | (customer, date) -> { 16 | var birthday = customer.birthday(); 17 | return isSameDay(birthday, date.plusDays(daysBefore)); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/birthday/convert/context/DailyReportContext.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.birthday.convert.context; 2 | 3 | import com.whiskels.notifier.reporting.service.customer.birthday.convert.ReportContext; 4 | import com.whiskels.notifier.reporting.service.customer.birthday.domain.CustomerBirthdayInfo; 5 | 6 | import java.time.LocalDate; 7 | import java.util.function.BiPredicate; 8 | 9 | import static com.whiskels.notifier.utilities.DateTimeUtil.isSameDay; 10 | import static com.whiskels.notifier.utilities.DateTimeUtil.reportDate; 11 | 12 | public class DailyReportContext extends ReportContext { 13 | private static final BiPredicate BIRTHDAY_PREDICATE = (customer, date) -> { 14 | var birthday = customer.birthday(); 15 | return isSameDay(birthday, date); 16 | }; 17 | 18 | public DailyReportContext(String headerPrefix) { 19 | super(date -> STR."\{headerPrefix}\{reportDate(date)}", _ignored -> true, BIRTHDAY_PREDICATE); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/birthday/convert/context/MonthMiddleReportContext.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.birthday.convert.context; 2 | 3 | import com.whiskels.notifier.reporting.service.customer.birthday.convert.ReportContext; 4 | import com.whiskels.notifier.reporting.service.customer.birthday.domain.CustomerBirthdayInfo; 5 | 6 | import java.time.LocalDate; 7 | import java.util.function.BiPredicate; 8 | 9 | import static com.whiskels.notifier.utilities.DateTimeUtil.isLater; 10 | import static com.whiskels.notifier.utilities.DateTimeUtil.isSameMonth; 11 | 12 | public class MonthMiddleReportContext extends ReportContext { 13 | private static final BiPredicate BIRTHDAY_PREDICATE = (customer, date) -> { 14 | var birthday = customer.birthday(); 15 | return isMiddleOfMonth(date) && isSameMonth(birthday, date) && isLater(birthday, date); 16 | }; 17 | 18 | 19 | public MonthMiddleReportContext(String header) { 20 | super(_ignored -> header, date -> !isMiddleOfMonth(date), BIRTHDAY_PREDICATE); 21 | } 22 | 23 | private static boolean isMiddleOfMonth(LocalDate date) { 24 | return date.getDayOfMonth() == 15; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/birthday/convert/context/MonthStartReportContext.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.birthday.convert.context; 2 | 3 | import com.whiskels.notifier.reporting.service.customer.birthday.convert.ReportContext; 4 | import com.whiskels.notifier.reporting.service.customer.birthday.domain.CustomerBirthdayInfo; 5 | 6 | import java.time.LocalDate; 7 | import java.util.function.BiPredicate; 8 | 9 | import static com.whiskels.notifier.utilities.DateTimeUtil.isSameMonth; 10 | 11 | public class MonthStartReportContext extends ReportContext { 12 | private static final BiPredicate BIRTHDAY_PREDICATE = (customer, date) -> { 13 | var birthday = customer.birthday(); 14 | return isStartOfMonth(date) && isSameMonth(birthday, date); 15 | }; 16 | 17 | 18 | public MonthStartReportContext(String header) { 19 | super(_ignored -> header, date -> !isStartOfMonth(date), BIRTHDAY_PREDICATE); 20 | } 21 | 22 | private static boolean isStartOfMonth(LocalDate date) { 23 | return date.getDayOfMonth() == 1; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/birthday/domain/CustomerBirthdayInfo.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.birthday.domain; 2 | 3 | import com.whiskels.notifier.reporting.domain.HasBirthday; 4 | import lombok.Builder; 5 | 6 | import java.time.LocalDate; 7 | 8 | import static com.whiskels.notifier.utilities.Util.defaultIfNull; 9 | import static com.whiskels.notifier.utilities.formatters.DateTimeFormatter.BIRTHDAY_FORMATTER; 10 | import static java.lang.String.format; 11 | 12 | @Builder 13 | public record CustomerBirthdayInfo(String responsible, String responsibleEmail, 14 | String clientId, String company, String name, 15 | String surname, String email, String telegram, 16 | String phone, LocalDate birthday, 17 | String position) implements HasBirthday { 18 | public String toString() { 19 | return format("%s %s %s (%s)", name, surname, defaultIfNull(BIRTHDAY_FORMATTER.format(birthday), "unknown"), company); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/birthday/mock/CustomerBirthdayInfoFetchServiceMock.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.birthday.mock; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.whiskels.notifier.reporting.service.DataFetchService; 5 | import com.whiskels.notifier.reporting.service.ReportData; 6 | import com.whiskels.notifier.reporting.service.customer.birthday.domain.CustomerBirthdayInfo; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.springframework.context.annotation.Profile; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.time.Clock; 14 | import java.time.LocalDate; 15 | import java.util.List; 16 | 17 | import static com.whiskels.notifier.infrastructure.mock.MockUtil.read; 18 | 19 | @Service 20 | @Profile("mock") 21 | @Slf4j 22 | @RequiredArgsConstructor 23 | class CustomerBirthdayInfoFetchServiceMock implements DataFetchService { 24 | private static final List MOCKED_DATA = read("mocks/customer.json", new TypeReference<>() { 25 | }); 26 | private final Clock clock; 27 | 28 | @NotNull 29 | @Override 30 | public ReportData fetch() { 31 | log.warn("Returning mocked customer birthday info"); 32 | return new ReportData<>(MOCKED_DATA, LocalDate.now(clock)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/debt/config/CustomerDebtFetchConfig.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.debt.config; 2 | 3 | import com.whiskels.notifier.reporting.service.DataFetchService; 4 | import com.whiskels.notifier.reporting.service.customer.debt.domain.CurrencyRate; 5 | import com.whiskels.notifier.reporting.service.customer.debt.domain.CustomerDebt; 6 | import com.whiskels.notifier.reporting.service.customer.debt.fetch.CurrencyRateDataFetchService; 7 | import com.whiskels.notifier.reporting.service.customer.debt.fetch.CurrencyRateFeignClient; 8 | import com.whiskels.notifier.reporting.service.customer.debt.fetch.CustomerDebtDebtDataFetchService; 9 | import com.whiskels.notifier.reporting.service.customer.debt.fetch.CustomerDebtFeignClient; 10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.context.annotation.Primary; 14 | 15 | import java.time.Clock; 16 | 17 | @Configuration 18 | @ConditionalOnProperty(CustomerDebtFeignClient.DEBT_URL) 19 | public class CustomerDebtFetchConfig { 20 | @Bean 21 | DataFetchService currencyRateDataFetchService( 22 | final CurrencyRateFeignClient client, 23 | final Clock clock 24 | ) { 25 | return new CurrencyRateDataFetchService(client, clock); 26 | } 27 | 28 | @Bean 29 | @Primary 30 | DataFetchService customerDebtDataFetchService( 31 | final DataFetchService rateReportSupplier, 32 | final CustomerDebtFeignClient debtClient, 33 | final Clock clock 34 | ) { 35 | return new CustomerDebtDebtDataFetchService(rateReportSupplier, debtClient, clock); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/debt/config/CustomerDebtReportConfig.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.debt.config; 2 | 3 | import com.whiskels.notifier.reporting.service.DataFetchService; 4 | import com.whiskels.notifier.reporting.service.GenericReportService; 5 | import com.whiskels.notifier.reporting.service.ReportMessageConverter; 6 | import com.whiskels.notifier.reporting.service.customer.debt.convert.CustomerDebtReportMessageConverter; 7 | import com.whiskels.notifier.reporting.service.customer.debt.domain.CustomerDebt; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | import static com.whiskels.notifier.reporting.ReportType.CUSTOMER_DEBT; 14 | 15 | @Configuration 16 | @ConditionalOnBean(value = CustomerDebt.class, parameterizedContainer = DataFetchService.class) 17 | public class CustomerDebtReportConfig { 18 | @Bean 19 | ReportMessageConverter customerDebtReportMessageConverter( 20 | @Value("${report.parameters.customer-debt.header:\uD83E\uDDFE Debt report on}") String header, 21 | @Value("${report.parameters.customer-debt.no-data:Nobody}") String noData 22 | ) { 23 | return new CustomerDebtReportMessageConverter(header, noData); 24 | } 25 | 26 | @Bean 27 | GenericReportService customerDebtReportRequestProcessor( 28 | DataFetchService dataFetchService, 29 | ReportMessageConverter messageCreator 30 | ) { 31 | return new GenericReportService<>(CUSTOMER_DEBT, dataFetchService, messageCreator); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/debt/convert/CustomerDebtDto.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.debt.convert; 2 | 3 | import com.whiskels.notifier.reporting.service.customer.debt.domain.CustomerDebt; 4 | import com.whiskels.notifier.utilities.formatters.StringFormatter; 5 | import lombok.Builder; 6 | 7 | import java.math.BigDecimal; 8 | 9 | import static com.whiskels.notifier.utilities.Util.defaultIfNull; 10 | import static java.math.BigDecimal.ZERO; 11 | 12 | @Builder 13 | record CustomerDebtDto(String contractor, String financeSubject, String wayOfPayment, 14 | String accountManager, String currency, String comment, 15 | BigDecimal total) { 16 | public static CustomerDebtDto from(CustomerDebt customerDebt) { 17 | return CustomerDebtDto.builder() 18 | .contractor(customerDebt.getContractor()) 19 | .accountManager(customerDebt.getAccountManager()) 20 | .financeSubject(customerDebt.getFinanceSubject()) 21 | .wayOfPayment(customerDebt.getWayOfPayment()) 22 | .financeSubject(customerDebt.getFinanceSubject()) 23 | .currency(customerDebt.getCurrency()) 24 | .comment(customerDebt.getComment()) 25 | .total(customerDebt.getTotal()) 26 | .build(); 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return String.format("*%s*%n %s%n %s%n %s%n *%s %s*%n%s" 32 | , defaultIfNull(contractor, "Contractor not defined") 33 | , defaultIfNull(financeSubject, "Finance subject not defined") 34 | , defaultIfNull(wayOfPayment, "Way of payment not defined") 35 | , defaultIfNull(accountManager, "Account manager not defined") 36 | , StringFormatter.format(defaultIfNull(total, ZERO)) 37 | , defaultIfNull(currency, "Currency not defined") 38 | , defaultIfNull(comment, "No comment") 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/debt/domain/CurrencyRate.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.debt.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import lombok.Data; 6 | 7 | import java.math.BigDecimal; 8 | import java.util.Map; 9 | import java.util.Optional; 10 | 11 | import static java.math.BigDecimal.ONE; 12 | 13 | @Data 14 | @JsonIgnoreProperties(ignoreUnknown = true) 15 | public class CurrencyRate { 16 | @JsonProperty("rub") 17 | private Map rubRates; 18 | 19 | public BigDecimal getRate(String currency) { 20 | return Optional.ofNullable(currency) 21 | .map(cur -> rubRates.get(cur.toLowerCase())) 22 | .orElse(ONE); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/debt/fetch/CurrencyRateDataFetchService.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.debt.fetch; 2 | 3 | import com.whiskels.notifier.reporting.service.DataFetchService; 4 | import com.whiskels.notifier.reporting.service.ReportData; 5 | import com.whiskels.notifier.reporting.service.audit.AuditDataFetchResult; 6 | import com.whiskels.notifier.reporting.service.customer.debt.domain.CurrencyRate; 7 | import lombok.NonNull; 8 | import lombok.RequiredArgsConstructor; 9 | 10 | import java.time.Clock; 11 | import java.time.LocalDate; 12 | import java.util.List; 13 | 14 | import static com.whiskels.notifier.reporting.ReportType.CURRENCY_RATE; 15 | import static java.util.Collections.emptyList; 16 | import static java.util.Collections.singletonList; 17 | 18 | @RequiredArgsConstructor 19 | public class CurrencyRateDataFetchService implements DataFetchService { 20 | private final CurrencyRateFeignClient client; 21 | private final Clock clock; 22 | 23 | @AuditDataFetchResult(reportType = CURRENCY_RATE) 24 | @NonNull 25 | @Override 26 | public ReportData fetch() { 27 | var data = client.get(); 28 | List wrappedData = data != null ? singletonList(data) : emptyList(); 29 | 30 | return new ReportData<>(wrappedData, LocalDate.now(clock)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/debt/fetch/CurrencyRateFeignClient.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.debt.fetch; 2 | 3 | import com.whiskels.notifier.reporting.service.customer.debt.domain.CurrencyRate; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 5 | import org.springframework.cloud.openfeign.FeignClient; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | 8 | import static org.springframework.web.bind.annotation.RequestMethod.GET; 9 | 10 | 11 | @FeignClient( 12 | name = "currencyRateClient", 13 | url = "${report.parameters.currency-rate.url:https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies/rub.json}" 14 | ) 15 | @ConditionalOnProperty(CustomerDebtFeignClient.DEBT_URL) 16 | public 17 | interface CurrencyRateFeignClient { 18 | @RequestMapping(method = GET) 19 | CurrencyRate get(); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/debt/fetch/CustomerDebtData.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.debt.fetch; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.whiskels.notifier.reporting.service.customer.debt.domain.CustomerDebt; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.util.List; 10 | 11 | @Getter 12 | @JsonIgnoreProperties(ignoreUnknown = true) 13 | @NoArgsConstructor 14 | @EqualsAndHashCode 15 | public class CustomerDebtData { 16 | private List content; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/debt/fetch/CustomerDebtFeignClient.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.debt.fetch; 2 | 3 | import com.whiskels.notifier.infrastructure.config.feign.FeignProxyConfig; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 5 | import org.springframework.cloud.openfeign.FeignClient; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | 8 | import static com.whiskels.notifier.reporting.service.customer.debt.fetch.CustomerDebtFeignClient.DEBT_URL; 9 | import static org.springframework.web.bind.annotation.RequestMethod.GET; 10 | 11 | 12 | @ConditionalOnProperty(DEBT_URL) 13 | @FeignClient(name = "debtClient", url = "${" + DEBT_URL + "}", configuration = FeignProxyConfig.class) 14 | public interface CustomerDebtFeignClient { 15 | String DEBT_URL = "report.parameters.customer-debt.url"; 16 | 17 | @RequestMapping(method = GET) 18 | CustomerDebtData get(); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/debt/mock/CustomerDebtFetchServiceMock.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.debt.mock; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.whiskels.notifier.reporting.service.DataFetchService; 5 | import com.whiskels.notifier.reporting.service.ReportData; 6 | import com.whiskels.notifier.reporting.service.customer.debt.domain.CustomerDebt; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.springframework.context.annotation.Profile; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.time.Clock; 14 | import java.time.LocalDate; 15 | import java.util.List; 16 | 17 | import static com.whiskels.notifier.infrastructure.mock.MockUtil.read; 18 | 19 | @Service 20 | @Profile("mock") 21 | @Slf4j 22 | @RequiredArgsConstructor 23 | class CustomerDebtFetchServiceMock implements DataFetchService { 24 | private static final List MOCKED_DATA = read("mocks/debt.json", new TypeReference<>() { 25 | }); 26 | private final Clock clock; 27 | 28 | @NotNull 29 | @Override 30 | public ReportData fetch() { 31 | log.warn("Returning mocked customer debt"); 32 | return new ReportData<>(MOCKED_DATA, LocalDate.now(clock)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/payment/config/CustomerPaymentFetchConfig.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.payment.config; 2 | 3 | import com.whiskels.notifier.reporting.service.DataFetchService; 4 | import com.whiskels.notifier.reporting.service.audit.LoadAuditRepository; 5 | import com.whiskels.notifier.reporting.service.customer.payment.domain.CustomerPaymentDto; 6 | import com.whiskels.notifier.reporting.service.customer.payment.domain.FinancialOperation; 7 | import com.whiskels.notifier.reporting.service.customer.payment.fetch.FinOperationDataFetchService; 8 | import com.whiskels.notifier.reporting.service.customer.payment.fetch.FinOperationFeignClient; 9 | import com.whiskels.notifier.reporting.service.customer.payment.fetch.FinOperationReloadScheduler; 10 | import com.whiskels.notifier.reporting.service.customer.payment.fetch.FinOperationRepository; 11 | import com.whiskels.notifier.reporting.service.customer.payment.fetch.PaymentReportDataFetchService; 12 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.context.annotation.Configuration; 15 | import org.springframework.scheduling.annotation.EnableScheduling; 16 | 17 | import java.time.Clock; 18 | 19 | import static com.whiskels.notifier.reporting.service.customer.payment.config.CustomerPaymentFetchConfig.PAYMENT_URL; 20 | import static com.whiskels.notifier.reporting.service.customer.payment.config.CustomerPaymentReportConfig.PAYMENT_PROPERTIES_PREFIX; 21 | 22 | @Configuration 23 | @ConditionalOnProperty(PAYMENT_URL) 24 | @EnableScheduling 25 | public class CustomerPaymentFetchConfig { 26 | public static final String PAYMENT_URL = PAYMENT_PROPERTIES_PREFIX + ".url"; 27 | 28 | @Bean 29 | DataFetchService financialOperationDataFetchService( 30 | final FinOperationRepository finOperationRepository, 31 | final Clock clock, 32 | final FinOperationFeignClient finOperationClient 33 | ) { 34 | return new FinOperationDataFetchService(finOperationRepository, clock, finOperationClient); 35 | } 36 | 37 | @Bean 38 | FinOperationReloadScheduler finOperationReloadScheduler(DataFetchService dataFetchService) { 39 | return new FinOperationReloadScheduler(dataFetchService); 40 | } 41 | 42 | @Bean 43 | DataFetchService customerPaymentDtoDataFetchService( 44 | final FinOperationRepository repository, 45 | final LoadAuditRepository auditRepository 46 | ) { 47 | return new PaymentReportDataFetchService(repository, auditRepository); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/payment/config/CustomerPaymentReportConfig.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.payment.config; 2 | 3 | import com.whiskels.notifier.reporting.service.DataFetchService; 4 | import com.whiskels.notifier.reporting.service.GenericReportService; 5 | import com.whiskels.notifier.reporting.service.ReportMessageConverter; 6 | import com.whiskels.notifier.reporting.service.customer.payment.domain.CustomerPaymentDto; 7 | import com.whiskels.notifier.reporting.service.customer.payment.messaging.PaymentReportMessageConverter; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.scheduling.annotation.EnableScheduling; 13 | 14 | import static com.whiskels.notifier.reporting.ReportType.CUSTOMER_PAYMENT; 15 | 16 | @Configuration 17 | @ConditionalOnBean(value = CustomerPaymentDto.class, parameterizedContainer = DataFetchService.class) 18 | @EnableScheduling 19 | public class CustomerPaymentReportConfig { 20 | public static final String PAYMENT_PROPERTIES_PREFIX = "report.parameters.customer-payment"; 21 | 22 | @Bean 23 | ReportMessageConverter customerPaymentDtoReportMessageConverter( 24 | @Value("${report.parameters.customer-payment.header:\uD83D\uDCB8 Payment report on}") final String header, 25 | @Value("${report.parameters.customer-payment.no-data:Nothing}") final String noData, 26 | final CustomerPaymentReportPicProperties properties 27 | ) { 28 | return new PaymentReportMessageConverter(header, noData, properties.getPics()); 29 | } 30 | 31 | @Bean 32 | GenericReportService customerOperationReportService( 33 | DataFetchService dataFetchService, 34 | ReportMessageConverter messageCreator 35 | ) { 36 | return new GenericReportService<>(CUSTOMER_PAYMENT, dataFetchService, messageCreator); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/payment/config/CustomerPaymentReportPicProperties.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.payment.config; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import static com.whiskels.notifier.reporting.service.customer.payment.config.CustomerPaymentReportConfig.PAYMENT_PROPERTIES_PREFIX; 11 | 12 | @ConfigurationProperties(PAYMENT_PROPERTIES_PREFIX) 13 | @Getter 14 | @Setter 15 | class CustomerPaymentReportPicProperties { 16 | private Map> pics; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/payment/domain/CustomerPaymentDto.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.payment.domain; 2 | 3 | import lombok.Builder; 4 | import lombok.Value; 5 | import lombok.extern.jackson.Jacksonized; 6 | 7 | import javax.annotation.Nonnull; 8 | import java.math.BigDecimal; 9 | import java.util.Comparator; 10 | 11 | import static com.whiskels.notifier.utilities.formatters.StringFormatter.format; 12 | 13 | @Value 14 | @Builder 15 | @Jacksonized 16 | public class CustomerPaymentDto implements Comparable { 17 | private static final Comparator AMOUNT_COMPARATOR = Comparator.comparing(CustomerPaymentDto::getAmount) 18 | .thenComparing(CustomerPaymentDto::getContractor).reversed(); 19 | 20 | String currency; 21 | BigDecimal amount; 22 | String contractor; 23 | BigDecimal amountRub; 24 | 25 | @Override 26 | public String toString() { 27 | return String.format("%s — %s %s", 28 | contractor, format(amount), currency); 29 | } 30 | 31 | @Override 32 | public int compareTo(@Nonnull CustomerPaymentDto o) { 33 | return AMOUNT_COMPARATOR.compare(this, o); 34 | } 35 | 36 | public static CustomerPaymentDto from(FinancialOperation operation) { 37 | return CustomerPaymentDto.builder() 38 | .amount(operation.getAmount()) 39 | .amountRub(operation.getAmountRub()) 40 | .contractor(operation.getContractor()) 41 | .currency(operation.getCurrency()) 42 | .build(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/payment/fetch/FinOperationFeignClient.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.payment.fetch; 2 | 3 | import com.whiskels.notifier.infrastructure.config.feign.FeignProxyConfig; 4 | import com.whiskels.notifier.reporting.service.customer.payment.domain.FinancialOperation; 5 | import feign.RequestInterceptor; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 8 | import org.springframework.cloud.openfeign.FeignClient; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Import; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | 13 | import java.time.Clock; 14 | import java.time.LocalDate; 15 | import java.time.format.DateTimeFormatter; 16 | import java.util.List; 17 | 18 | import static com.whiskels.notifier.reporting.service.customer.payment.config.CustomerPaymentFetchConfig.PAYMENT_URL; 19 | import static com.whiskels.notifier.reporting.service.customer.payment.config.CustomerPaymentReportConfig.PAYMENT_PROPERTIES_PREFIX; 20 | import static com.whiskels.notifier.utilities.DateTimeUtil.subtractWorkingDays; 21 | import static java.time.LocalDate.now; 22 | import static org.springframework.web.bind.annotation.RequestMethod.GET; 23 | 24 | 25 | @ConditionalOnProperty(PAYMENT_URL) 26 | @FeignClient(name = "finOperationClient", 27 | configuration = FinOperationFeignClient.FinOperationRequestInterceptorConfig.class, 28 | url = "https://") 29 | public interface FinOperationFeignClient { 30 | @RequestMapping(method = GET) 31 | List get(); 32 | 33 | @Import(FeignProxyConfig.class) 34 | class FinOperationRequestInterceptorConfig { 35 | private static final DateTimeFormatter YEAR_MONTH_DAY_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); 36 | 37 | @Value("${" + PAYMENT_URL + "}") 38 | private String url; 39 | @Value("${" + PAYMENT_PROPERTIES_PREFIX + ".working-days-to-load:2}") 40 | private int workingDaysToLoad; 41 | 42 | @Bean 43 | public RequestInterceptor urlPreparingInterceptor(Clock clock) { 44 | return requestTemplate -> { 45 | LocalDate endDate = now(clock).minusDays(1); 46 | LocalDate startDate = subtractWorkingDays(endDate, workingDaysToLoad); 47 | var finalUrl = url 48 | .replace("startDate", startDate.format(YEAR_MONTH_DAY_FORMATTER)) 49 | .replace("endDate", endDate.format(YEAR_MONTH_DAY_FORMATTER)); 50 | requestTemplate.target(finalUrl); 51 | }; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/payment/fetch/FinOperationReloadScheduler.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.payment.fetch; 2 | 3 | import com.whiskels.notifier.reporting.service.DataFetchService; 4 | import com.whiskels.notifier.reporting.service.customer.payment.domain.FinancialOperation; 5 | import com.whiskels.notifier.reporting.service.customer.payment.config.CustomerPaymentReportConfig; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.scheduling.annotation.Scheduled; 8 | 9 | @RequiredArgsConstructor 10 | public class FinOperationReloadScheduler { 11 | private final DataFetchService dataFetchService; 12 | 13 | @Scheduled(cron = "${" + CustomerPaymentReportConfig.PAYMENT_PROPERTIES_PREFIX + ".cron:0 5 12 * * MON-FRI}", zone = "${common.timezone}") 14 | public void loadScheduled() { 15 | dataFetchService.fetch(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/payment/fetch/FinOperationRepository.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.payment.fetch; 2 | 3 | import com.whiskels.notifier.infrastructure.repository.AbstractRepository; 4 | import com.whiskels.notifier.reporting.service.customer.payment.domain.FinancialOperation; 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 6 | import org.springframework.data.jpa.repository.Query; 7 | import org.springframework.data.repository.query.Param; 8 | import org.springframework.stereotype.Repository; 9 | 10 | import java.time.LocalDateTime; 11 | import java.util.List; 12 | import java.util.Set; 13 | 14 | import static com.whiskels.notifier.reporting.service.customer.payment.config.CustomerPaymentFetchConfig.PAYMENT_URL; 15 | 16 | 17 | @Repository 18 | @ConditionalOnProperty(PAYMENT_URL) 19 | public interface FinOperationRepository extends AbstractRepository { 20 | @Query("select r.crmId from FinancialOperation r") 21 | Set getPresentCrmIds(); 22 | 23 | @Query("select r from FinancialOperation r" + 24 | " where r.loadDateTime between :startDate and :endDate" + 25 | " and r.category = :category" + 26 | " order by r.amountRub desc") 27 | List getAllByCategoryAndDateBetween( 28 | @Param("category") String category, 29 | @Param("startDate") LocalDateTime startDate, 30 | @Param("endDate") LocalDateTime endDate 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/payment/fetch/PaymentReportDataFetchService.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.payment.fetch; 2 | 3 | import com.whiskels.notifier.reporting.ReportType; 4 | import com.whiskels.notifier.reporting.service.DataFetchService; 5 | import com.whiskels.notifier.reporting.service.ReportData; 6 | import com.whiskels.notifier.reporting.service.audit.LoadAuditRepository; 7 | import com.whiskels.notifier.reporting.service.customer.payment.domain.CustomerPaymentDto; 8 | import com.whiskels.notifier.reporting.service.customer.payment.domain.FinancialOperation; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | import javax.annotation.Nonnull; 13 | import java.time.LocalTime; 14 | import java.util.Collections; 15 | import java.util.List; 16 | 17 | import static com.whiskels.notifier.utilities.collections.StreamUtil.map; 18 | import static java.time.LocalDate.now; 19 | 20 | @Slf4j 21 | @RequiredArgsConstructor 22 | public class PaymentReportDataFetchService implements DataFetchService { 23 | private static final String DB_CATEGORY_PAYMENT = "Revenue"; 24 | 25 | private final FinOperationRepository repository; 26 | private final LoadAuditRepository auditRepository; 27 | 28 | @Nonnull 29 | @Override 30 | public ReportData fetch() { 31 | return auditRepository.findLastUpdateDateTime(ReportType.CUSTOMER_PAYMENT) 32 | .map(dateTime -> { 33 | List selectedOperations = 34 | repository.getAllByCategoryAndDateBetween( 35 | DB_CATEGORY_PAYMENT, 36 | dateTime.with(LocalTime.MIN), 37 | dateTime.with(LocalTime.MAX) 38 | ); 39 | log.info("Selected {} payment operations from db", selectedOperations.size()); 40 | return new ReportData<>(map(selectedOperations, CustomerPaymentDto::from), dateTime.toLocalDate()); 41 | }) 42 | .orElseGet(() -> new ReportData<>(Collections.emptyList(), now())); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/customer/payment/mock/CustomerPaymentDtoFetchMock.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.payment.mock; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.whiskels.notifier.reporting.service.DataFetchService; 5 | import com.whiskels.notifier.reporting.service.ReportData; 6 | import com.whiskels.notifier.reporting.service.customer.payment.domain.CustomerPaymentDto; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.springframework.context.annotation.Profile; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.time.Clock; 14 | import java.time.LocalDate; 15 | import java.util.List; 16 | 17 | import static com.whiskels.notifier.infrastructure.mock.MockUtil.read; 18 | 19 | @Service 20 | @Profile("mock") 21 | @Slf4j 22 | @RequiredArgsConstructor 23 | class CustomerPaymentDtoFetchMock implements DataFetchService { 24 | private static final List MOCKED_DATA = read("mocks/payment.json", new TypeReference<>() { 25 | }); 26 | private final Clock clock; 27 | 28 | @NotNull 29 | @Override 30 | public ReportData fetch() { 31 | log.warn("Returning mocked customer payment"); 32 | return new ReportData<>(MOCKED_DATA, LocalDate.now(clock)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/employee/config/EmployeeEventFetchConfig.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.employee.config; 2 | 3 | import com.whiskels.notifier.reporting.service.DataFetchService; 4 | import com.whiskels.notifier.reporting.service.employee.domain.Employee; 5 | import com.whiskels.notifier.reporting.service.employee.fetch.EmployeeDataFetchService; 6 | import com.whiskels.notifier.reporting.service.employee.fetch.EmployeeFeignClient; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | import java.time.Clock; 12 | 13 | @Configuration 14 | @ConditionalOnProperty(EmployeeEventFetchConfig.EMPLOYEE_URL) 15 | public class EmployeeEventFetchConfig { 16 | public static final String EMPLOYEE_URL = "report.parameters.employee-event.url"; 17 | 18 | @Bean 19 | DataFetchService employeeDataFetchService( 20 | final EmployeeFeignClient client, 21 | final Clock clock 22 | ) { 23 | return new EmployeeDataFetchService(client, clock); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/employee/convert/EmployeeDto.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.employee.convert; 2 | 3 | import com.whiskels.notifier.reporting.domain.HasBirthday; 4 | import com.whiskels.notifier.reporting.service.employee.domain.Employee; 5 | import lombok.Builder; 6 | 7 | import java.time.Clock; 8 | import java.time.LocalDate; 9 | 10 | import static com.whiskels.notifier.utilities.formatters.DateTimeFormatter.BIRTHDAY_FORMATTER; 11 | 12 | @Builder 13 | record EmployeeDto(String name, LocalDate birthday, LocalDate appointmentDate) implements HasBirthday { 14 | public static EmployeeDto from(Employee e) { 15 | return EmployeeDto.builder() 16 | .name(e.name()) 17 | .appointmentDate(e.getAppointmentDate()) 18 | .birthday(e.birthday()) 19 | .build(); 20 | } 21 | 22 | public String toBirthdayString() { 23 | return STR."\{name} \{BIRTHDAY_FORMATTER.format(birthday)}"; 24 | } 25 | 26 | public String toWorkAnniversaryString(Clock clock) { 27 | final int totalWorkingYears = LocalDate.now(clock).getYear() - appointmentDate.getYear(); 28 | return STR."\{name} \{BIRTHDAY_FORMATTER.format(appointmentDate)} (\{totalWorkingYears})"; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/employee/convert/ReportContext.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.employee.convert; 2 | 3 | import com.whiskels.notifier.reporting.service.employee.domain.Employee; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | 7 | import java.time.LocalDate; 8 | import java.util.function.BiPredicate; 9 | import java.util.function.Function; 10 | import java.util.function.Predicate; 11 | 12 | @Getter 13 | @AllArgsConstructor 14 | public class ReportContext { 15 | private final Function headerMapper; 16 | private final Predicate skipEmpty; 17 | private final BiPredicate birthdayPredicate; 18 | private final BiPredicate anniversaryPredicate; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/employee/convert/context/BeforeEventReportContext.java: -------------------------------------------------------------------------------- 1 | 2 | 3 | package com.whiskels.notifier.reporting.service.employee.convert.context; 4 | 5 | import com.whiskels.notifier.reporting.service.employee.convert.ReportContext; 6 | 7 | import static com.whiskels.notifier.utilities.DateTimeUtil.isSameDay; 8 | import static com.whiskels.notifier.utilities.DateTimeUtil.reportDate; 9 | 10 | public class BeforeEventReportContext extends ReportContext { 11 | public BeforeEventReportContext(String headerPrefix, int daysBefore) { 12 | super(date -> STR."\{headerPrefix}\{reportDate(date)}", 13 | _ -> true, 14 | (employee, date) -> { 15 | var birthday = employee.getBirthday(); 16 | return isSameDay(birthday, date.plusDays(daysBefore)); 17 | } 18 | , (employee, date) -> { 19 | var appointmentDate = employee.getAppointmentDate(); 20 | return isSameDay(appointmentDate, date.plusDays(daysBefore)); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/employee/convert/context/DailyReportContext.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.employee.convert.context; 2 | 3 | import com.whiskels.notifier.reporting.service.employee.convert.ReportContext; 4 | import com.whiskels.notifier.reporting.service.employee.domain.Employee; 5 | 6 | import java.time.LocalDate; 7 | import java.util.function.BiPredicate; 8 | 9 | import static com.whiskels.notifier.utilities.DateTimeUtil.isSameDay; 10 | import static com.whiskels.notifier.utilities.DateTimeUtil.reportDate; 11 | 12 | public class DailyReportContext extends ReportContext { 13 | private static final BiPredicate BIRTHDAY_PREDICATE = (employee, date) -> { 14 | var birthday = employee.getBirthday(); 15 | return isSameDay(birthday, date); 16 | }; 17 | 18 | private static final BiPredicate ANNIVERSARY_PREDICATE = (employee, date) -> { 19 | var appointmentDate = employee.getAppointmentDate(); 20 | return isSameDay(appointmentDate, date); 21 | }; 22 | 23 | public DailyReportContext(String headerPrefix) { 24 | super(date -> STR."\{headerPrefix}\{reportDate(date)}", _ -> true, BIRTHDAY_PREDICATE, ANNIVERSARY_PREDICATE); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/employee/convert/context/MonthMiddleReportContext.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.employee.convert.context; 2 | 3 | import com.whiskels.notifier.reporting.service.employee.convert.ReportContext; 4 | import com.whiskels.notifier.reporting.service.employee.domain.Employee; 5 | 6 | import java.time.LocalDate; 7 | import java.util.function.BiPredicate; 8 | 9 | import static com.whiskels.notifier.utilities.DateTimeUtil.isLater; 10 | import static com.whiskels.notifier.utilities.DateTimeUtil.isSameMonth; 11 | 12 | public class MonthMiddleReportContext extends ReportContext { 13 | private static final BiPredicate BIRTHDAY_PREDICATE = (employee, date) -> { 14 | var birthday = employee.getBirthday(); 15 | return isMiddleOfMonth(date) && isSameMonth(birthday, date) && isLater(birthday, date); 16 | }; 17 | 18 | private static final BiPredicate ANNIVERSARY_PREDICATE = (employee, date) -> { 19 | var appointmentDate = employee.getAppointmentDate(); 20 | return isMiddleOfMonth(date) && isSameMonth(appointmentDate, date) && isLater(appointmentDate, date); 21 | }; 22 | 23 | public MonthMiddleReportContext(String header) { 24 | super(_ignored -> header, date -> !isMiddleOfMonth(date), BIRTHDAY_PREDICATE, ANNIVERSARY_PREDICATE); 25 | } 26 | 27 | private static boolean isMiddleOfMonth(LocalDate date) { 28 | return date.getDayOfMonth() == 15; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/employee/convert/context/MonthStartReportContext.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.employee.convert.context; 2 | 3 | import com.whiskels.notifier.reporting.service.employee.convert.ReportContext; 4 | import com.whiskels.notifier.reporting.service.employee.domain.Employee; 5 | 6 | import java.time.LocalDate; 7 | import java.util.function.BiPredicate; 8 | 9 | import static com.whiskels.notifier.utilities.DateTimeUtil.isSameMonth; 10 | 11 | public class MonthStartReportContext extends ReportContext { 12 | private static final BiPredicate BIRTHDAY_PREDICATE = (employee, date) -> { 13 | var birthday = employee.getBirthday(); 14 | return isStartOfMonth(date) && isSameMonth(birthday, date); 15 | }; 16 | 17 | private static final BiPredicate ANNIVERSARY_PREDICATE = (employee, date) -> { 18 | var appointmentDate = employee.getAppointmentDate(); 19 | return isStartOfMonth(date) && isSameMonth(appointmentDate, date); 20 | }; 21 | 22 | public MonthStartReportContext(String header) { 23 | super(_ignored -> header, date -> !isStartOfMonth(date), BIRTHDAY_PREDICATE, ANNIVERSARY_PREDICATE); 24 | } 25 | 26 | private static boolean isStartOfMonth(LocalDate date) { 27 | return date.getDayOfMonth() == 1; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/employee/domain/BirthdayDeserializer.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.employee.domain; 2 | 3 | import com.fasterxml.jackson.core.JsonParser; 4 | import com.fasterxml.jackson.databind.DeserializationContext; 5 | import com.fasterxml.jackson.databind.JsonDeserializer; 6 | 7 | import java.io.IOException; 8 | import java.time.LocalDate; 9 | 10 | import static com.whiskels.notifier.utilities.DateTimeUtil.parseDate; 11 | 12 | 13 | class BirthdayDeserializer extends JsonDeserializer { 14 | @Override 15 | public LocalDate deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { 16 | return parseDate(jsonParser.getText()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/employee/domain/Employee.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.employee.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 7 | import com.whiskels.notifier.reporting.domain.HasBirthday; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | 12 | import java.time.LocalDate; 13 | 14 | import static com.whiskels.notifier.utilities.formatters.DateTimeFormatter.BIRTHDAY_FORMAT; 15 | 16 | /** 17 | * Employee data is received from JSON of the following syntax: 18 | * [{"row_number":1, 19 | * "id":1111, 20 | * "name":"Company employee", 21 | * "employee_id":111, 22 | * "full_name":"Employee full name", 23 | * "email":"employee111@company.com", 24 | * "appointment_date":"2020-10-10", 25 | * "removal_date":null, 26 | * "phone":"896812345678", 27 | * "department":"Sales", 28 | * "position":"Sales Manager", 29 | * "grade":"Senior", 30 | * "birthday":"01.01", 31 | * "status":"\u0420\u0430\u0431\u043e\u0442\u0430\u0435\u0442", 32 | * "status_system":"working", 33 | * "office":"Moscow", 34 | * "is_new_employee":false},... 35 | */ 36 | @Data 37 | @JsonIgnoreProperties(ignoreUnknown = true) 38 | @AllArgsConstructor 39 | @NoArgsConstructor 40 | public class Employee implements HasBirthday { 41 | private String name; 42 | @JsonDeserialize(using = BirthdayDeserializer.class) 43 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = BIRTHDAY_FORMAT) 44 | private LocalDate birthday; 45 | @JsonProperty("appointment_date") 46 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") 47 | private LocalDate appointmentDate; 48 | private String status; 49 | @JsonProperty("status_system") 50 | private String statusSystem; 51 | 52 | @Override 53 | public String name() { 54 | return name; 55 | } 56 | 57 | @Override 58 | public LocalDate birthday() { 59 | return birthday; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/employee/fetch/EmployeeDataFetchService.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.employee.fetch; 2 | 3 | import com.whiskels.notifier.reporting.service.DataFetchService; 4 | import com.whiskels.notifier.reporting.service.ReportData; 5 | import com.whiskels.notifier.reporting.service.audit.AuditDataFetchResult; 6 | import com.whiskels.notifier.reporting.domain.HasBirthday; 7 | import com.whiskels.notifier.reporting.service.employee.domain.Employee; 8 | import lombok.RequiredArgsConstructor; 9 | 10 | import javax.annotation.Nonnull; 11 | import java.time.Clock; 12 | import java.util.List; 13 | import java.util.Optional; 14 | import java.util.function.Predicate; 15 | 16 | import static com.whiskels.notifier.reporting.ReportType.EMPLOYEE_EVENT; 17 | import static com.whiskels.notifier.utilities.collections.StreamUtil.filterAndSort; 18 | import static java.time.LocalDate.now; 19 | import static java.util.Collections.emptyList; 20 | import static java.util.Objects.nonNull; 21 | 22 | @RequiredArgsConstructor 23 | public class EmployeeDataFetchService implements DataFetchService { 24 | private static final String STATUS_SYSTEM_FIRED = "fired"; 25 | private static final String STATUS_DECREE = "Декрет"; 26 | private static final Predicate NOT_FIRED = e -> nonNull(e.getStatusSystem()) && !e.getStatusSystem().equals(STATUS_SYSTEM_FIRED); 27 | private static final Predicate NOT_DECREE = e -> nonNull(e.getStatus()) && !e.getStatus().equals(STATUS_DECREE); 28 | 29 | private final EmployeeFeignClient employeeClient; 30 | private final Clock clock; 31 | 32 | @AuditDataFetchResult(reportType = EMPLOYEE_EVENT) 33 | @Nonnull 34 | @Override 35 | public ReportData fetch() { 36 | List employeeList = Optional.ofNullable(employeeClient.get()) 37 | .map(loaded -> filterAndSort(loaded, HasBirthday.comparator(), NOT_FIRED, NOT_DECREE)) 38 | .orElse(emptyList()); 39 | return new ReportData<>(employeeList, now(clock)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/employee/fetch/EmployeeFeignClient.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.employee.fetch; 2 | 3 | import com.whiskels.notifier.infrastructure.config.feign.FeignProxyConfig; 4 | import com.whiskels.notifier.reporting.service.employee.domain.Employee; 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 6 | import org.springframework.cloud.openfeign.FeignClient; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | 9 | import java.util.List; 10 | 11 | import static com.whiskels.notifier.reporting.service.employee.config.EmployeeEventFetchConfig.EMPLOYEE_URL; 12 | import static org.springframework.web.bind.annotation.RequestMethod.GET; 13 | 14 | 15 | @ConditionalOnProperty(EMPLOYEE_URL) 16 | @FeignClient(name = "employeeClient", url = "${" + EMPLOYEE_URL + "}", configuration = FeignProxyConfig.class) 17 | public interface EmployeeFeignClient { 18 | 19 | @RequestMapping(method = GET) 20 | List get(); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/reporting/service/employee/mock/EmployeeFetchMock.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.employee.mock; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.whiskels.notifier.reporting.service.DataFetchService; 5 | import com.whiskels.notifier.reporting.service.ReportData; 6 | import com.whiskels.notifier.reporting.service.employee.domain.Employee; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.springframework.context.annotation.Profile; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.time.Clock; 14 | import java.time.LocalDate; 15 | import java.util.List; 16 | 17 | import static com.whiskels.notifier.infrastructure.mock.MockUtil.read; 18 | 19 | @Service 20 | @Profile("mock") 21 | @Slf4j 22 | @RequiredArgsConstructor 23 | class EmployeeFetchMock implements DataFetchService { 24 | private static final List MOCKED_DATA = read("mocks/employee.json", new TypeReference<>() { 25 | }); 26 | private final Clock clock; 27 | 28 | @NotNull 29 | @Override 30 | public ReportData fetch() { 31 | log.warn("Returning mocked employee"); 32 | return new ReportData<>(MOCKED_DATA, LocalDate.now(clock)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/utilities/Util.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.utilities; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | 6 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 7 | public final class Util { 8 | public static T defaultIfNull(T val, T defaultVal) { 9 | return val != null ? val : defaultVal; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/utilities/collections/StreamUtil.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.utilities.collections; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | 6 | import java.util.Collection; 7 | import java.util.Comparator; 8 | import java.util.List; 9 | import java.util.function.Function; 10 | import java.util.function.Predicate; 11 | import java.util.stream.Stream; 12 | 13 | import static com.whiskels.notifier.utilities.formatters.StringFormatter.COLLECTOR_NEW_LINE; 14 | import static java.util.stream.Collectors.toList; 15 | 16 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 17 | public final class StreamUtil { 18 | @SafeVarargs 19 | public static > List filterAndSort(List list, Predicate... predicates) { 20 | return list.stream() 21 | .filter(Stream.of(predicates).reduce(_ -> true, Predicate::and)) 22 | .sorted() 23 | .collect(toList()); 24 | } 25 | 26 | @SafeVarargs 27 | public static List filterAndSort(List list, Comparator comparator, Predicate... predicates) { 28 | return list.stream() 29 | .filter(Stream.of(predicates).reduce(_ -> true, Predicate::and)) 30 | .sorted(comparator) 31 | .collect(toList()); 32 | } 33 | 34 | public static List map(Collection coll, Function func) { 35 | return coll.stream().map(func).collect(toList()); 36 | } 37 | 38 | public static String collectToBulletListString(Collection collection, Function toStringFunc) { 39 | return collection.stream() 40 | .map(o -> STR."• \{toStringFunc.apply(o)}") 41 | .collect(COLLECTOR_NEW_LINE); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/utilities/formatters/DateTimeFormatter.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.utilities.formatters; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | 6 | import java.time.LocalDate; 7 | import java.time.format.DateTimeFormatterBuilder; 8 | import java.time.temporal.ChronoField; 9 | import java.util.Locale; 10 | 11 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 12 | public final class DateTimeFormatter { 13 | public static final String BIRTHDAY_FORMAT = "dd.MM"; 14 | public static final java.time.format.DateTimeFormatter DAY_MONTH_YEAR_FORMATTER = java.time.format.DateTimeFormatter.ofPattern("dd-MM-yyyy"); 15 | public static final java.time.format.DateTimeFormatter BIRTHDAY_FORMATTER = java.time.format.DateTimeFormatter.ofPattern(BIRTHDAY_FORMAT); 16 | 17 | public static java.time.format.DateTimeFormatter FORMATTER_WITH_YEAR = new DateTimeFormatterBuilder() 18 | .appendPattern("[dd.MM.yyyy]") 19 | .appendPattern("[dd.M.yyyy]") 20 | .appendPattern("[dd/MM/yyyy]") 21 | .toFormatter(Locale.ENGLISH); 22 | 23 | public static java.time.format.DateTimeFormatter FORMATTER_WITHOUT_YEAR = new DateTimeFormatterBuilder() 24 | .appendPattern("[dd.MM]") 25 | .appendOptional(new DateTimeFormatterBuilder().appendLiteral('.').toFormatter()) 26 | .appendPattern("[dd/MM]") 27 | .appendOptional(new DateTimeFormatterBuilder().appendLiteral('/').toFormatter()) 28 | .parseDefaulting(ChronoField.YEAR, LocalDate.now().getYear()) 29 | .toFormatter(Locale.ENGLISH); 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/whiskels/notifier/utilities/formatters/StringFormatter.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.utilities.formatters; 2 | 3 | 4 | import lombok.AccessLevel; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.text.DecimalFormat; 8 | import java.text.DecimalFormatSymbols; 9 | import java.util.Locale; 10 | import java.util.stream.Collector; 11 | import java.util.stream.Collectors; 12 | 13 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 14 | public final class StringFormatter { 15 | public static final Collector COLLECTOR_NEW_LINE = Collectors.joining(String.format( 16 | "%n")); 17 | 18 | public static String format(Number value) { 19 | DecimalFormatSymbols formatSymbols = new DecimalFormatSymbols(Locale.ENGLISH); 20 | formatSymbols.setDecimalSeparator('.'); 21 | formatSymbols.setGroupingSeparator(' '); 22 | 23 | DecimalFormat formatter = new DecimalFormat("###,###,###,###.#", formatSymbols); 24 | return formatter.format(value); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/resources/application-mock.yaml: -------------------------------------------------------------------------------- 1 | report: 2 | schedule: 3 | CUSTOMER_BIRTHDAY: 0 * * * * * 4 | CUSTOMER_DEBT: 0 * * * * * 5 | CUSTOMER_PAYMENT: 0 * * * * * 6 | EMPLOYEE_EVENT: 0 * * * * * 7 | webhooks: 8 | PLAIN: 9 | CUSTOMER_BIRTHDAY: ${WEBHOOK_URL} 10 | CUSTOMER_DEBT: ${WEBHOOK_URL} 11 | CUSTOMER_PAYMENT: ${WEBHOOK_URL} 12 | EMPLOYEE_EVENT: ${WEBHOOK_URL} 13 | -------------------------------------------------------------------------------- /src/main/resources/application-prod.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: ${DATABASE_URL} 4 | report: 5 | parameters: 6 | customer-birthday: 7 | spreadsheet: ${CUSTOMER_BIRTHDAY_SPREADSHEET} 8 | cell-range: Контакты!A2:L 9 | customer-debt: 10 | url: ${CUSTOMER_DEBT_JSON_URL} 11 | customer-payment: 12 | url: ${CUSTOMER_RECEIVABLE_JSON_URL} 13 | employee-event: 14 | url: ${EMPLOYEE_URL} 15 | schedule: 16 | CUSTOMER_BIRTHDAY: 0 0 9 * * * 17 | CUSTOMER_PAYMENT: 0 1 13 * * MON-FRI 18 | EMPLOYEE_EVENT: 0 0 9 * * * 19 | webhooks: 20 | PLAIN: 21 | CUSTOMER_BIRTHDAY: ${CUSTOMER_BIRTHDAY_WEBHOOK} 22 | CUSTOMER_PAYMENT: ${CUSTOMER_PAYMENT_WEBHOOK} 23 | EMPLOYEE_EVENT: ${EMPLOYEE_WEBHOOK} 24 | google: 25 | spreadsheets: 26 | parameters: 27 | app.name: ${GOOGLE_APP_NAME} 28 | email: ${GOOGLE_APP_EMAIL} 29 | credentials: ${GOOGLE_CREDENTIALS_JSON} -------------------------------------------------------------------------------- /src/main/resources/application-telegram.yaml: -------------------------------------------------------------------------------- 1 | telegram: 2 | bot: 3 | token: ${TELEGRAM_BOT_TOKEN} 4 | admin: ${TELEGRAM_BOT_ADMIN} -------------------------------------------------------------------------------- /src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | driver-class-name: org.postgresql.Driver 4 | jpa: 5 | database-platform: org.hibernate.dialect.PostgreSQLDialect 6 | hibernate: 7 | ddl-auto: update 8 | sql.init.mode: never 9 | common: 10 | timezone: Europe/Moscow 11 | report: 12 | parameters: 13 | customer-payment: 14 | pics: 15 | 0: 16 | - https://i.imgur.com/Os451Qo.jpeg 17 | - https://i.imgur.com/unzfnaD.jpg 18 | - https://i.imgur.com/0gf5s.jpg 19 | - https://media.giphy.com/media/ND6xkVPaj8tHO/giphy.gif 20 | 100000: 21 | - https://i.imgur.com/bDiTy7t.jpeg 22 | - https://i.imgur.com/3rYHhEu.jpg 23 | - https://i.imgur.com/gckZshj.jpg 24 | - https://media.giphy.com/media/G4qAZYIFr1Cww/giphy.gif 25 | 1000000: 26 | - https://i.imgur.com/fj138M6.jpeg 27 | - https://i.imgur.com/ObVSUb6.jpg 28 | - https://i.imgur.com/EAFSXYp.png 29 | - https://i.imgur.com/uCKlPXG.jpg 30 | - https://i.imgur.com/zsxTYeC.jpg 31 | - https://media.giphy.com/media/cLLgfNJiKppgA/giphy.gif 32 | - https://media.giphy.com/media/w2JmkbOHFoq8U/giphy.gif 33 | - https://media.giphy.com/media/eDdEiM0Jq8Ene/giphy.gif 34 | - https://media.giphy.com/media/YBsd8wdchmxqg/giphy.gif -------------------------------------------------------------------------------- /src/main/resources/db/migration/V001__initialize.sql: -------------------------------------------------------------------------------- 1 | CREATE SEQUENCE global_seq START WITH 100000; 2 | 3 | CREATE TABLE users 4 | ( 5 | id INTEGER DEFAULT nextval('global_seq') PRIMARY KEY, 6 | chat_id INTEGER UNIQUE NOT NULL, 7 | name VARCHAR NOT NULL 8 | ); 9 | CREATE UNIQUE INDEX users_unique_chatid_idx ON users (chat_id); 10 | 11 | CREATE TABLE user_roles 12 | ( 13 | user_id INTEGER NOT NULL, 14 | role VARCHAR, 15 | CONSTRAINT user_roles_idx UNIQUE (user_id, role), 16 | FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE 17 | ); 18 | 19 | CREATE TABLE schedule 20 | ( 21 | id INTEGER DEFAULT nextval('global_seq') PRIMARY KEY, 22 | user_id INTEGER NOT NULL, 23 | hour INTEGER NOT NULL, 24 | minutes INTEGER DEFAULT 0 NOT NULL, 25 | FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE 26 | ); 27 | CREATE UNIQUE INDEX schedule_unique_user_time_idx ON schedule (user_id, hour, minutes); 28 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V002__create_table_receivable.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE receivable 2 | ( 3 | id INTEGER DEFAULT nextval('global_seq') PRIMARY KEY, 4 | crm_id INTEGER NOT NULL, 5 | date DATE NOT NULL, 6 | currency VARCHAR NOT NULL, 7 | amount DOUBLE PRECISION NOT NULL, 8 | amount_usd DOUBLE PRECISION, 9 | amount_rub DOUBLE PRECISION, 10 | bank VARCHAR, 11 | bank_account VARCHAR, 12 | legal_name VARCHAR, 13 | contractor VARCHAR NOT NULL, 14 | type VARCHAR , 15 | contractor_account VARCHAR, 16 | contractor_legal_name VARCHAR, 17 | category VARCHAR NOT NULL, 18 | subcategory VARCHAR, 19 | project VARCHAR, 20 | office VARCHAR, 21 | description VARCHAR, 22 | load_date DATE NOT NULL DEFAULT current_date 23 | ); 24 | 25 | CREATE UNIQUE INDEX receivable_unique_crm_id_idx ON receivable (crm_id); -------------------------------------------------------------------------------- /src/main/resources/db/migration/V003__alter_receivable.sql: -------------------------------------------------------------------------------- 1 | update receivable 2 | set amount_rub = 0 3 | where amount is null; 4 | 5 | alter table receivable 6 | alter column amount_rub set not null; -------------------------------------------------------------------------------- /src/main/resources/db/migration/V004__alter_receivable.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE receivable RENAME TO financial_operation; 2 | ALTER INDEX receivable_unique_crm_id_idx RENAME TO financial_operation_unique_crm_id_idx; -------------------------------------------------------------------------------- /src/main/resources/db/migration/V005__create_load_audit.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE load_audit 2 | ( 3 | id INTEGER DEFAULT nextval('global_seq') PRIMARY KEY, 4 | date DATE NOT NULL, 5 | loader VARCHAR NOT NULL, 6 | count INTEGER NOT NULL 7 | ) -------------------------------------------------------------------------------- /src/main/resources/db/migration/V006__alter_load_audit.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE load_audit ADD COLUMN load_date_time timestamp; 2 | ALTER TABLE load_audit RENAME COLUMN date TO load_date; -------------------------------------------------------------------------------- /src/main/resources/db/migration/V007__alter_receivable.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE financial_operation ADD COLUMN load_date_time timestamp; -------------------------------------------------------------------------------- /src/main/resources/db/migration/V008__alter_users.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users ALTER COLUMN chat_id TYPE BIGINT; -------------------------------------------------------------------------------- /src/main/resources/db/migration/V009__alter_financial_operation.sql: -------------------------------------------------------------------------------- 1 | alter table financial_operation alter column amount type numeric; 2 | alter table financial_operation alter column amount_usd type numeric; 3 | alter table financial_operation alter column amount_rub type numeric; -------------------------------------------------------------------------------- /src/main/resources/db/migration/V010__alter_load_audit.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE load_audit RENAME COLUMN loader TO report_type; 2 | 3 | UPDATE load_audit 4 | SET report_type = CASE report_type 5 | WHEN 'FINANCIAL_OPERATION' THEN 'CUSTOMER_PAYMENT' 6 | WHEN 'EMPLOYEE' THEN 'EMPLOYEE_BIRTHDAY' 7 | WHEN 'DEBT' THEN 'CUSTOMER_DEBT' 8 | ELSE report_type 9 | END 10 | WHERE report_type IN ('FINANCIAL_OPERATION', 'EMPLOYEE', 'DEBT'); -------------------------------------------------------------------------------- /src/main/resources/db/migration/V011__drop_tables.sql: -------------------------------------------------------------------------------- 1 | drop table users cascade ; 2 | drop table schedule; -------------------------------------------------------------------------------- /src/main/resources/db/migration/V012__drop_date_columns.sql: -------------------------------------------------------------------------------- 1 | alter table load_audit drop column load_date; 2 | alter table financial_operation drop column load_date; -------------------------------------------------------------------------------- /src/main/resources/db/migration/V013__drop_table_user_roles.sql: -------------------------------------------------------------------------------- 1 | drop table user_roles cascade; -------------------------------------------------------------------------------- /src/main/resources/mocks/payment.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "currency": "USD", 4 | "amount": 1500.00, 5 | "contractor": "Alpha Corp", 6 | "amountRub": 112500.00 7 | }, 8 | { 9 | "currency": "EUR", 10 | "amount": 2000.00, 11 | "contractor": "Beta Ltd", 12 | "amountRub": 153000.00 13 | }, 14 | { 15 | "currency": "GBP", 16 | "amount": 1200.00, 17 | "contractor": "Gamma Inc", 18 | "amountRub": 91800.00 19 | }, 20 | { 21 | "currency": "USD", 22 | "amount": 1800.00, 23 | "contractor": "Delta Enterprises", 24 | "amountRub": 135000.00 25 | }, 26 | { 27 | "currency": "EUR", 28 | "amount": 2500.00, 29 | "contractor": "Epsilon Co", 30 | "amountRub": 191250.00 31 | }, 32 | { 33 | "currency": "GBP", 34 | "amount": 1700.00, 35 | "contractor": "Zeta Group", 36 | "amountRub": 129750.00 37 | }, 38 | { 39 | "currency": "USD", 40 | "amount": 2200.00, 41 | "contractor": "Eta Corporation", 42 | "amountRub": 165000.00 43 | }, 44 | { 45 | "currency": "EUR", 46 | "amount": 2800.00, 47 | "contractor": "Theta Industries", 48 | "amountRub": 214200.00 49 | }, 50 | { 51 | "currency": "GBP", 52 | "amount": 1900.00, 53 | "contractor": "Iota Ltd", 54 | "amountRub": 145350.00 55 | }, 56 | { 57 | "currency": "USD", 58 | "amount": 2400.00, 59 | "contractor": "Kappa Limited", 60 | "amountRub": 180000.00 61 | } 62 | ] -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/DisabledDataSourceConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier; 2 | 3 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 4 | import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; 5 | import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 6 | import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; 7 | import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; 8 | import org.springframework.boot.test.context.TestConfiguration; 9 | 10 | @TestConfiguration 11 | @EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class, 12 | DataSourceTransactionManagerAutoConfiguration.class, 13 | HibernateJpaAutoConfiguration.class, 14 | FlywayAutoConfiguration.class}) 15 | public class DisabledDataSourceConfiguration { 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/JsonUtils.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 5 | import lombok.experimental.UtilityClass; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | @UtilityClass 10 | public class JsonUtils { 11 | public static final ObjectMapper MAPPER = new ObjectMapper().registerModule(new JavaTimeModule()); 12 | 13 | @SuppressWarnings("unchecked") 14 | public static void assertEqualsWithJson(String expected, T actual) { 15 | T expectedObject; 16 | try { 17 | expectedObject = MAPPER.readValue(expected, (Class) actual.getClass()); 18 | } catch (Exception e) { 19 | throw new IllegalStateException(STR."Failed to read json \{expected}"); 20 | } 21 | assertEquals(expectedObject, actual); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/MockedClockConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier; 2 | 3 | import org.springframework.boot.test.context.TestConfiguration; 4 | import org.springframework.context.annotation.Bean; 5 | 6 | import java.time.Clock; 7 | import java.time.Instant; 8 | import java.time.LocalDate; 9 | import java.time.ZoneId; 10 | 11 | @TestConfiguration 12 | public class MockedClockConfiguration { 13 | private static final String EXPECTED_BOT_TIME = "2024-02-23T10:15:30Z"; 14 | public static final LocalDate EXPECTED_DATE = LocalDate.of(2024, 2, 23); 15 | public static final Clock CLOCK = Clock.fixed(Instant.parse(EXPECTED_BOT_TIME), ZoneId.of("UTC")); 16 | 17 | @Bean 18 | protected Clock mockedClock() { 19 | return CLOCK; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/SharedPostgresContainer.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier; 2 | 3 | import org.testcontainers.containers.PostgreSQLContainer; 4 | 5 | public class SharedPostgresContainer extends PostgreSQLContainer { 6 | private static final String IMAGE_VERSION = "postgres:11.1"; 7 | private static SharedPostgresContainer container; 8 | 9 | private SharedPostgresContainer() { 10 | super(IMAGE_VERSION); 11 | } 12 | 13 | public static SharedPostgresContainer getInstance() { 14 | if (container == null) { 15 | container = new SharedPostgresContainer(); 16 | } 17 | return container; 18 | } 19 | 20 | @Override 21 | public void start() { 22 | super.start(); 23 | System.setProperty("DB_URL", container.getJdbcUrl()); 24 | System.setProperty("DB_USERNAME", container.getUsername()); 25 | System.setProperty("DB_PASSWORD", container.getPassword()); 26 | } 27 | 28 | @Override 29 | public void stop() { 30 | //do nothing, JVM handles shut down 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/TestUtil.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier; 2 | 3 | import lombok.SneakyThrows; 4 | import lombok.experimental.UtilityClass; 5 | import org.springframework.core.io.ClassPathResource; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | 10 | @UtilityClass 11 | public class TestUtil { 12 | public static void assertEqualsIgnoringCR(T expected, T actual) { 13 | assertEquals(expected.toString().replace("\r",""), actual.toString().replace("\r","")); 14 | } 15 | 16 | @SneakyThrows 17 | public static String file(String path) { 18 | return new ClassPathResource(path).getURL().toString(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/infrastructure/admin/telegram/BotTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.admin.telegram; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.InjectMocks; 7 | import org.mockito.Mock; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | import org.telegram.telegrambots.meta.api.methods.send.SendDocument; 10 | import org.telegram.telegrambots.meta.api.methods.send.SendMessage; 11 | import org.telegram.telegrambots.meta.api.objects.Update; 12 | import org.telegram.telegrambots.meta.exceptions.TelegramApiValidationException; 13 | 14 | import static org.junit.jupiter.api.Assertions.assertThrows; 15 | import static org.mockito.ArgumentMatchers.any; 16 | import static org.mockito.Mockito.mock; 17 | import static org.mockito.Mockito.verify; 18 | import static org.mockito.Mockito.when; 19 | 20 | @ExtendWith(MockitoExtension.class) 21 | class BotTest { 22 | @Mock 23 | private MessageProcessor messageProcessor; 24 | 25 | @InjectMocks 26 | private Bot bot; 27 | 28 | @Test 29 | @DisplayName("Should throw exception when document doesn't have chat id") 30 | void shouldThrowExceptionWhenSendingDocumentWithoutChatIt() { 31 | assertThrows(TelegramApiValidationException.class, () -> 32 | bot.execute(new SendDocument())); 33 | } 34 | 35 | @Test 36 | @DisplayName("Should throw exception when send message doesn't have chat id") 37 | void shouldThrowExceptionWhenSendingMessageWithoutChatIt() { 38 | assertThrows(TelegramApiValidationException.class, () -> 39 | bot.execute(new SendMessage())); 40 | } 41 | 42 | @Test 43 | @DisplayName("Should execute message if it is present") 44 | void shouldExecuteMessage() { 45 | Update update = new Update(); 46 | var messageMock = mock(BotMessage.class); 47 | when(messageProcessor.onUpdateReceived(any())).thenReturn(messageMock); 48 | 49 | bot.onUpdateReceived(update); 50 | 51 | verify(messageProcessor).onUpdateReceived(update); 52 | verify(messageMock).send(bot); 53 | } 54 | 55 | @Test 56 | @DisplayName("Should not execute message if it is not present") 57 | void shouldNotExecuteMessageIfItIsNotPresent() { 58 | Update update = new Update(); 59 | 60 | bot.onUpdateReceived(update); 61 | 62 | verify(messageProcessor).onUpdateReceived(update); 63 | } 64 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/infrastructure/admin/telegram/TextBotMessageTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.admin.telegram; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.Mock; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | import org.telegram.telegrambots.meta.api.methods.send.SendMessage; 9 | import org.telegram.telegrambots.meta.exceptions.TelegramApiException; 10 | 11 | import static org.mockito.Mockito.verify; 12 | 13 | @ExtendWith(MockitoExtension.class) 14 | class TextBotMessageTest { 15 | 16 | @Mock 17 | private Bot bot; 18 | 19 | @Test 20 | @DisplayName("Should call bot") 21 | void shouldCallBot() throws TelegramApiException { 22 | SendMessage message = new SendMessage(); 23 | 24 | TextBotMessage.of(message).send(bot); 25 | 26 | verify(bot).execute(message); 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/infrastructure/admin/telegram/handler/DefaultHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.admin.telegram.handler; 2 | 3 | import com.whiskels.notifier.infrastructure.admin.telegram.Command; 4 | import com.whiskels.notifier.infrastructure.admin.telegram.CommandHandler; 5 | import com.whiskels.notifier.infrastructure.admin.telegram.TextBotMessage; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.DisplayName; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.Mock; 11 | import org.mockito.junit.jupiter.MockitoExtension; 12 | 13 | import java.util.List; 14 | 15 | import static com.whiskels.notifier.JsonUtils.assertEqualsWithJson; 16 | import static org.junit.jupiter.api.Assertions.assertEquals; 17 | import static org.mockito.Mockito.when; 18 | 19 | @ExtendWith(MockitoExtension.class) 20 | class DefaultHandlerTest { 21 | @Mock 22 | private CommandHandler mockOne; 23 | @Mock 24 | private CommandHandler mockTwo; 25 | 26 | private DefaultHandler defaultHandler; 27 | 28 | @BeforeEach 29 | void initHandler() { 30 | defaultHandler = new DefaultHandler(List.of(mockOne, mockTwo)); 31 | } 32 | 33 | @Test 34 | @DisplayName("Should prepare correct response in default handler") 35 | void shouldPrepareCorrectResponseInDefaultHandler() { 36 | final var userId = "user1"; 37 | final var message = "message"; 38 | final var expected = """ 39 | { 40 | "chat_id" : "user1", 41 | "text" : "👋 Welcome to admin module\\nHere is what you can do\\n", 42 | "reply_markup" : { 43 | "inline_keyboard" : [ [ { 44 | "text" : "🕹 Fetch logs", 45 | "callback_data" : "GET_LOGS" 46 | } ], [ { 47 | "text" : "📈 Reload data", 48 | "callback_data" : "RELOAD_DATA" 49 | } ] ] 50 | }, 51 | "method" : "sendmessage" 52 | }"""; 53 | 54 | when(mockOne.getCommand()).thenReturn(Command.GET_LOGS); 55 | when(mockTwo.getCommand()).thenReturn(Command.RELOAD_DATA); 56 | 57 | var botMessage = ((TextBotMessage) defaultHandler.handle(userId, message)).getMessage(); 58 | 59 | assertEqualsWithJson(expected, botMessage); 60 | } 61 | 62 | @Test 63 | @DisplayName("Should return correct command for default handler") 64 | void shouldReturnCorrectCommandForDefaultHandler() { 65 | assertEquals(defaultHandler.getCommand(), Command.DEFAULT); 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/infrastructure/admin/telegram/handler/log/service/LogHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.admin.telegram.handler.log.service; 2 | 3 | import com.whiskels.notifier.infrastructure.admin.telegram.Command; 4 | import com.whiskels.notifier.infrastructure.admin.telegram.DocumentBotMessage; 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.InjectMocks; 9 | import org.mockito.Mock; 10 | import org.mockito.junit.jupiter.MockitoExtension; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | import static org.mockito.Mockito.when; 15 | 16 | @ExtendWith(MockitoExtension.class) 17 | class LogHandlerTest { 18 | @Mock 19 | private LogService logService; 20 | 21 | @InjectMocks 22 | private LogHandler logHandler; 23 | 24 | @Test 25 | @DisplayName("Should prepare correct message with logs attached") 26 | void shouldPrepareLogMessage() throws Exception { 27 | final var userId = "user1"; 28 | final var message = "message"; 29 | final byte[] fileContent = new byte[]{84, 101, 115, 116}; 30 | when(logService.getLogsAsByteArray()).thenReturn(fileContent); 31 | 32 | var actual = ((DocumentBotMessage) logHandler.handle(userId, message)).getMessage(); 33 | 34 | assertEquals(actual.getChatId(), userId); 35 | assertEquals(actual.getDocument().getAttachName(), "attach://logs.txt"); 36 | assertEquals(actual.getCaption(), "Application logs"); 37 | assertArrayEquals(actual.getDocument().getNewMediaStream().readAllBytes(), fileContent); 38 | } 39 | 40 | @Test 41 | @DisplayName("Should return correct command for log handler") 42 | void shouldReturnCorrectCommandForLogHandler() { 43 | Command command = logHandler.getCommand(); 44 | 45 | assertEquals(Command.GET_LOGS, command); 46 | } 47 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/infrastructure/admin/telegram/handler/log/service/LogServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.admin.telegram.handler.log.service; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.InjectMocks; 7 | import org.mockito.Mock; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | 10 | import java.util.List; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 13 | import static org.mockito.Mockito.when; 14 | 15 | @ExtendWith(MockitoExtension.class) 16 | class LogServiceTest { 17 | @Mock 18 | private PapertrailClient papertrailClient; 19 | 20 | @InjectMocks 21 | private LogService logService; 22 | 23 | @Test 24 | @DisplayName("Should convert to byte array") 25 | void shouldConvertToByteArray() { 26 | when(papertrailClient.getLogs()).thenReturn(List.of("Test")); 27 | 28 | byte[] byteArray = logService.getLogsAsByteArray(); 29 | 30 | assertArrayEquals(byteArray, new byte[]{84, 101, 115, 116}); 31 | } 32 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/infrastructure/admin/telegram/handler/reload/DataReloadHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.admin.telegram.handler.reload; 2 | 3 | import com.whiskels.notifier.infrastructure.admin.telegram.Command; 4 | import com.whiskels.notifier.reporting.service.DataFetchService; 5 | import com.whiskels.notifier.reporting.service.ReportData; 6 | import com.whiskels.notifier.reporting.service.audit.LoadAuditRepository; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.DisplayName; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.extension.ExtendWith; 11 | import org.mockito.Mock; 12 | import org.mockito.junit.jupiter.MockitoExtension; 13 | 14 | import java.util.Collections; 15 | import java.util.List; 16 | 17 | import static org.junit.jupiter.api.Assertions.assertEquals; 18 | import static org.junit.jupiter.api.Assertions.assertNotNull; 19 | import static org.mockito.ArgumentMatchers.any; 20 | import static org.mockito.Mockito.verify; 21 | import static org.mockito.Mockito.verifyNoInteractions; 22 | import static org.mockito.Mockito.when; 23 | 24 | @ExtendWith(MockitoExtension.class) 25 | class DataReloadHandlerTest { 26 | 27 | @Mock 28 | private DataFetchService dataFetchService; 29 | 30 | @Mock 31 | private LoadAuditRepository loadAuditRepository; 32 | 33 | private DataReloadHandler dataReloadHandler; 34 | 35 | @BeforeEach 36 | void setUp() { 37 | dataReloadHandler = new DataReloadHandler(Collections.singletonList(dataFetchService), loadAuditRepository); 38 | } 39 | 40 | @Test 41 | @DisplayName("Should handle message without context with menu") 42 | void shouldHandleMessageWithoutContextWithMenu() { 43 | var result = dataReloadHandler.handle("someChatId", "RELOAD_DATA"); 44 | 45 | verify(loadAuditRepository).getLast(any()); 46 | verifyNoInteractions(dataFetchService); 47 | assertNotNull(result); 48 | } 49 | 50 | @Test 51 | @DisplayName("Should handle message with context correctly") 52 | void shouldHandleMessageWithContextCorrectly() { 53 | when(dataFetchService.fetch()).thenReturn(new ReportData<>(List.of(1,2), null)); 54 | 55 | var result = dataReloadHandler.handle("someChatId", STR."RELOAD_DATA \{dataFetchService.getClass().getSimpleName()}"); 56 | 57 | verifyNoInteractions(loadAuditRepository); 58 | assertNotNull(result); 59 | } 60 | 61 | @Test 62 | @DisplayName("Should return correct command for reload handler") 63 | void shouldReturnCorrectCommandForReloadHandler() { 64 | assertEquals(Command.RELOAD_DATA, dataReloadHandler.getCommand()); 65 | } 66 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/infrastructure/admin/telegram/util/TelegramUtilTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.admin.telegram.util; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup; 6 | import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton; 7 | 8 | import java.util.List; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | 12 | class TelegramUtilTest { 13 | 14 | @Test 15 | @DisplayName("Should extract arguments from message") 16 | void shouldExtractArguments() { 17 | String message = "/command argument1 argument2"; 18 | assertEquals("argument1 argument2", TelegramUtil.extractArguments(message)); 19 | } 20 | 21 | @Test 22 | @DisplayName("Should extract arguments when") 23 | void testExtractArgumentsNoSpace() { 24 | String message = "/command"; 25 | assertEquals(message, TelegramUtil.extractArguments(message)); 26 | } 27 | 28 | @Test 29 | void testCreateMarkup() { 30 | var button = new InlineKeyboardButton(); 31 | button.setText("button"); 32 | button.setCallbackData("callbackDate"); 33 | List> keyboard = List.of( 34 | List.of(button), 35 | List.of(button) 36 | ); 37 | InlineKeyboardMarkup markup = TelegramUtil.createMarkup(keyboard); 38 | assertEquals(keyboard, markup.getKeyboard()); 39 | } 40 | 41 | @Test 42 | void testButton() { 43 | InlineKeyboardButton button = TelegramUtil.button("text", "callbackData"); 44 | assertEquals("text", button.getText()); 45 | assertEquals("callbackData", button.getCallbackData()); 46 | } 47 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/infrastructure/config/feign/FixieProxyPropertiesProviderTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.config.feign; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.Mock; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | import org.springframework.core.env.Environment; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.assertThrows; 12 | import static org.mockito.Mockito.when; 13 | 14 | @ExtendWith({MockitoExtension.class}) 15 | public class FixieProxyPropertiesProviderTest { 16 | 17 | @Mock 18 | private Environment environment; 19 | 20 | @Test 21 | @DisplayName("Should correctly initialize via constructor") 22 | void testConstructorSuccess() { 23 | when(environment.getProperty(FixieProxyPropertiesProvider.FIXIE_ENV_VAR)) 24 | .thenReturn("http://user:password@host:8080"); 25 | 26 | FixieProxyPropertiesProvider fixieProxyPropertiesProvider = new FixieProxyPropertiesProvider(environment); 27 | 28 | assertEquals("user", fixieProxyPropertiesProvider.getUser()); 29 | assertEquals("password", fixieProxyPropertiesProvider.getPassword()); 30 | assertEquals("host", fixieProxyPropertiesProvider.getHost()); 31 | assertEquals(8080, fixieProxyPropertiesProvider.getPort()); 32 | } 33 | 34 | @Test 35 | @DisplayName("Should fail to initialize via constructor when env var not present") 36 | void testConstructorFailure() { 37 | when(environment.getProperty(FixieProxyPropertiesProvider.FIXIE_ENV_VAR)).thenReturn(null); 38 | 39 | assertThrows(NullPointerException.class, () -> new FixieProxyPropertiesProvider(environment)); 40 | } 41 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/infrastructure/slack/SlackClientTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.infrastructure.slack; 2 | 3 | import com.slack.api.Slack; 4 | import com.slack.api.webhook.Payload; 5 | import com.slack.api.webhook.WebhookResponse; 6 | import com.whiskels.notifier.infrastructure.report.slack.SlackClient; 7 | import org.junit.jupiter.api.DisplayName; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.InjectMocks; 11 | import org.mockito.Mock; 12 | import org.mockito.junit.jupiter.MockitoExtension; 13 | import org.springframework.http.HttpStatus; 14 | 15 | import java.io.IOException; 16 | 17 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 18 | import static org.junit.jupiter.api.Assertions.assertEquals; 19 | import static org.junit.jupiter.api.Assertions.assertThrows; 20 | import static org.mockito.Mockito.when; 21 | 22 | @ExtendWith(MockitoExtension.class) 23 | public class SlackClientTest { 24 | 25 | @Mock 26 | private Slack slack; 27 | 28 | @InjectMocks 29 | private SlackClient slackClient; 30 | 31 | 32 | @Test 33 | @DisplayName("Should send payload") 34 | void testSendSuccess() throws IOException { 35 | String webhook = "https://hooks.slack.com/services/..."; 36 | Payload payload = Payload.builder().text("Hello, world!").build(); 37 | WebhookResponse response = WebhookResponse.builder() 38 | .code(HttpStatus.OK.value()) 39 | .build(); 40 | 41 | when(slack.send(webhook, payload)).thenReturn(response); 42 | 43 | assertDoesNotThrow(() -> slackClient.send(webhook, payload)); 44 | 45 | } 46 | 47 | @Test 48 | @DisplayName("Should throw exception when payload execution failed") 49 | void testSendFailure() throws IOException { 50 | String webhook = "https://hooks.slack.com/services/..."; 51 | Payload payload = Payload.builder().text("Hello, world!").build(); 52 | WebhookResponse response = WebhookResponse.builder() 53 | .code(HttpStatus.INTERNAL_SERVER_ERROR.value()) 54 | .build(); 55 | 56 | when(slack.send(webhook, payload)).thenReturn(response); 57 | 58 | var exception = assertThrows(RuntimeException.class, () -> slackClient.send(webhook, payload)); 59 | assertEquals("Error on slack call: 500: null", exception.getMessage()); 60 | } 61 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/GenericReportServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting; 2 | 3 | import com.whiskels.notifier.reporting.service.DataFetchService; 4 | import com.whiskels.notifier.reporting.service.GenericReportService; 5 | import com.whiskels.notifier.reporting.service.Report; 6 | import com.whiskels.notifier.reporting.service.ReportData; 7 | import com.whiskels.notifier.reporting.service.ReportMessageConverter; 8 | import org.junit.jupiter.api.DisplayName; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.extension.ExtendWith; 11 | import org.mockito.InjectMocks; 12 | import org.mockito.Mock; 13 | import org.mockito.junit.jupiter.MockitoExtension; 14 | 15 | import java.time.LocalDate; 16 | import java.util.List; 17 | 18 | import static org.junit.jupiter.api.Assertions.assertEquals; 19 | import static org.mockito.ArgumentMatchers.any; 20 | import static org.mockito.Mockito.when; 21 | 22 | @ExtendWith(MockitoExtension.class) 23 | class GenericReportServiceTest { 24 | 25 | @Mock 26 | private DataFetchService dataFetchService; 27 | 28 | @Mock 29 | private ReportMessageConverter messageCreator; 30 | 31 | @InjectMocks 32 | private GenericReportService genericReportService; 33 | 34 | @Test 35 | @DisplayName("Should prepare reports") 36 | void shouldPrepareReports() { 37 | var payload = Report.builder().build(); 38 | when(dataFetchService.fetch()).thenReturn(new ReportData<>(List.of("Data1", "Data2"), LocalDate.now())); 39 | when(messageCreator.convert(any())).thenReturn(List.of(payload)); 40 | 41 | Iterable payloads = genericReportService.prepareReports(); 42 | List payloadList = (List) payloads; 43 | 44 | assertEquals(1, payloadList.size()); 45 | assertEquals(payloadList.getFirst(), payload); 46 | } 47 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/ReportPropertiesTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.context.properties.ConfigurationPropertiesScan; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.test.context.TestPropertySource; 10 | 11 | import java.util.Map; 12 | 13 | import static com.whiskels.notifier.reporting.ReportType.EMPLOYEE_EVENT; 14 | import static org.junit.jupiter.api.Assertions.assertEquals; 15 | 16 | @SpringBootTest(classes = ReportPropertiesTest.TestConfig.class) 17 | @TestPropertySource(properties = { 18 | "report.schedule.EMPLOYEE_EVENT=0 0 12 * * MON-FRI", 19 | "report.webhooks.SLACK.EMPLOYEE_EVENT=https://webhook.example.com/report1" 20 | }) 21 | public class ReportPropertiesTest { 22 | 23 | @Autowired 24 | private _ReportConfig.ReportProperties reportProperties; 25 | 26 | @Test 27 | @DisplayName("Should bind properties") 28 | void shouldBingProperties() { 29 | Map schedule = reportProperties.getSchedule(); 30 | Map<_ReportConfig.WebhookType, Map> webhooks = reportProperties.getWebhooks(); 31 | 32 | assertEquals("0 0 12 * * MON-FRI", schedule.get(EMPLOYEE_EVENT)); 33 | assertEquals("https://webhook.example.com/report1", webhooks.get(_ReportConfig.WebhookType.SLACK).get(EMPLOYEE_EVENT)); 34 | } 35 | 36 | @Configuration 37 | @ConfigurationPropertiesScan 38 | static class TestConfig { 39 | } 40 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/ReportTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting; 2 | 3 | import com.whiskels.notifier.reporting.service.Report; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertNull; 10 | import static org.junit.jupiter.api.Assertions.assertTrue; 11 | 12 | class ReportTest { 13 | 14 | @Test 15 | @DisplayName("Should construct SimpleReport via constructor") 16 | void shouldConstructReport() { 17 | var header = "Test Header"; 18 | var banner = "Test Banner"; 19 | var body = "Test Body"; 20 | Report report = Report.builder() 21 | .header(header) 22 | .notifyChannel(true) 23 | .banner(banner) 24 | .build() 25 | .addBody(body); 26 | 27 | assertEquals(header, report.getHeader()); 28 | assertEquals(banner, report.getBanner()); 29 | assertTrue(report.isNotifyChannel()); 30 | assertThat(report.getBody()).hasSize(1) 31 | .allSatisfy(bodyBlock -> { 32 | assertEquals(body, bodyBlock.text()); 33 | assertNull(bodyBlock.mediaContentUrl()); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/WireMockTestConfig.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting; 2 | 3 | import com.github.tomakehurst.wiremock.WireMockServer; 4 | import com.github.tomakehurst.wiremock.client.WireMock; 5 | import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; 6 | import org.springframework.boot.test.context.TestConfiguration; 7 | import org.springframework.cloud.openfeign.FeignAutoConfiguration; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Import; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.http.MediaType; 12 | 13 | @TestConfiguration 14 | @Import({FeignAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class}) 15 | public class WireMockTestConfig { 16 | @Bean(initMethod = "start", destroyMethod = "stop") 17 | public WireMockServer wireMockServer() { 18 | return new WireMockServer(9561); 19 | } 20 | 21 | public static void setMockResponse(WireMockServer mockService, String url, String response) { 22 | mockService.stubFor(WireMock.get(WireMock.urlEqualTo(url)) 23 | .willReturn(WireMock.aResponse() 24 | .withStatus(HttpStatus.OK.value()) 25 | .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) 26 | .withBody(response))); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/domain/AbstractTimeStampedEntityListenerTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.domain; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.InjectMocks; 7 | import org.mockito.Mock; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | 10 | import java.time.Clock; 11 | import java.time.Instant; 12 | import java.time.LocalDateTime; 13 | import java.time.ZoneId; 14 | 15 | import static org.mockito.ArgumentMatchers.eq; 16 | import static org.mockito.Mockito.verify; 17 | import static org.mockito.Mockito.when; 18 | 19 | @ExtendWith(MockitoExtension.class) 20 | class AbstractTimeStampedEntityListenerTest { 21 | 22 | @Mock 23 | private Clock clock; 24 | 25 | @InjectMocks 26 | private AbstractTimeStampedEntityListener listener; 27 | 28 | @Mock 29 | private AbstractTimeStampedEntity entity; 30 | 31 | @Test 32 | @DisplayName("Should set load date time") 33 | void shouldSetLoadDateTime() { 34 | LocalDateTime now = LocalDateTime.of(2023, 8, 19, 12, 0); 35 | Instant instant = now.atZone(ZoneId.systemDefault()).toInstant(); 36 | when(clock.instant()).thenReturn(instant); 37 | when(clock.getZone()).thenReturn(ZoneId.systemDefault()); 38 | 39 | listener.prePersist(entity); 40 | 41 | verify(entity).setLoadDateTime(eq(now)); 42 | } 43 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/domain/HasBirthdayTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.domain; 2 | 3 | import lombok.Builder; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.time.LocalDate; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | class HasBirthdayTest { 13 | 14 | @Test 15 | void testComparator() { 16 | var first = "FIRST"; 17 | var second = "SECOND"; 18 | var third = "THIRD"; 19 | var fourth = "FOURTH"; 20 | 21 | List input = new ArrayList<>(); 22 | input.add( 23 | HasBirthdayImpl.builder() 24 | .name(fourth) 25 | .birthday(null) 26 | .build() 27 | ); 28 | input.add( 29 | HasBirthdayImpl.builder() 30 | .name(null) 31 | .birthday(null) 32 | .build() 33 | ); 34 | input.add( 35 | HasBirthdayImpl.builder() 36 | .name(first) 37 | .birthday(LocalDate.now()) 38 | .build() 39 | ); 40 | input.add( 41 | HasBirthdayImpl.builder() 42 | .name(second) 43 | .birthday(LocalDate.now().plusDays(1)) 44 | .build() 45 | ); 46 | input.add( 47 | HasBirthdayImpl.builder() 48 | .name(third) 49 | .birthday(LocalDate.now().plusDays(1)) 50 | .build() 51 | ); 52 | 53 | input.sort(HasBirthday.comparator()); 54 | 55 | assertThat(input).extracting(HasBirthday::name) 56 | .containsExactly(first, second, third, fourth, null); 57 | } 58 | 59 | @Builder 60 | record HasBirthdayImpl(String name, LocalDate birthday) implements HasBirthday { 61 | } 62 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/exception/ExceptionEventTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.exception; 2 | 3 | import com.whiskels.notifier.reporting.ReportType; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static com.whiskels.notifier.reporting.ReportType.EMPLOYEE_EVENT; 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertNotNull; 10 | import static org.junit.jupiter.api.Assertions.assertTrue; 11 | 12 | class ExceptionEventTest { 13 | @Test 14 | @DisplayName("Should initialize exception event via constructor") 15 | void testRecordCreation() { 16 | String message = "Test Message"; 17 | ReportType type = EMPLOYEE_EVENT; 18 | 19 | 20 | ExceptionEvent event = new ExceptionEvent(message, type); 21 | 22 | assertEquals(message, event.message()); 23 | assertEquals(type, event.type()); 24 | } 25 | 26 | @Test 27 | @DisplayName("Should initialize exception event via static constructor") 28 | void testFactoryMethod() { 29 | String message = "Another Test Message"; 30 | ReportType type = EMPLOYEE_EVENT; 31 | 32 | ExceptionEvent event = ExceptionEvent.of(message, type); 33 | 34 | assertNotNull(event); 35 | assertEquals(message, event.message()); 36 | assertEquals(type, event.type()); 37 | } 38 | 39 | @Test 40 | @DisplayName("Should map exception to string") 41 | void testToString() { 42 | String message = "Test Message"; 43 | ReportType type = EMPLOYEE_EVENT; 44 | 45 | ExceptionEvent event = new ExceptionEvent(message, type); 46 | 47 | assertTrue(event.toString().contains(message)); 48 | assertTrue(event.toString().contains(type.toString())); 49 | } 50 | 51 | @Test 52 | void testEqualsAndHashCode() { 53 | String message = "Test Message"; 54 | ReportType type = EMPLOYEE_EVENT; 55 | 56 | ExceptionEvent event1 = new ExceptionEvent(message, type); 57 | ExceptionEvent event2 = new ExceptionEvent(message, type); 58 | 59 | assertEquals(event1, event2); 60 | assertEquals(event1.hashCode(), event2.hashCode()); 61 | } 62 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/executor/SlackReportExecutorTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.executor; 2 | 3 | import com.slack.api.webhook.Payload; 4 | import com.whiskels.notifier.infrastructure.report.slack.SlackClient; 5 | import com.whiskels.notifier.infrastructure.report.slack.SlackPayloadMapper; 6 | import com.whiskels.notifier.infrastructure.report.slack.SlackReportExecutor; 7 | import com.whiskels.notifier.reporting.ReportType; 8 | import com.whiskels.notifier.reporting.service.Report; 9 | import org.junit.jupiter.api.DisplayName; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.mockito.Mock; 13 | import org.mockito.junit.jupiter.MockitoExtension; 14 | 15 | import java.io.IOException; 16 | import java.util.Map; 17 | 18 | import static com.whiskels.notifier.reporting.ReportType.CUSTOMER_BIRTHDAY; 19 | import static com.whiskels.notifier.reporting.ReportType.EMPLOYEE_EVENT; 20 | import static java.util.Collections.emptyMap; 21 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 22 | import static org.mockito.ArgumentMatchers.any; 23 | import static org.mockito.ArgumentMatchers.anyString; 24 | import static org.mockito.BDDMockito.given; 25 | import static org.mockito.Mockito.never; 26 | import static org.mockito.Mockito.verify; 27 | 28 | @ExtendWith(MockitoExtension.class) 29 | class SlackReportExecutorTest { 30 | private static final Payload DUMMY_PAYLOAD = Payload.builder().build(); 31 | 32 | @Mock 33 | private SlackClient client; 34 | @Mock 35 | private SlackPayloadMapper mapper; 36 | 37 | private SlackReportExecutor executor; 38 | 39 | @Test 40 | @DisplayName("Should send when type exists") 41 | void shouldSentWhenTypeExists() throws IOException { 42 | Map webhookMappings = Map.of(EMPLOYEE_EVENT, "https://example.com/some-webhook"); 43 | executor = new SlackReportExecutor(client, mapper, webhookMappings); 44 | given(mapper.map(any())).willReturn(DUMMY_PAYLOAD); 45 | 46 | executor.send(EMPLOYEE_EVENT, Report.builder().build()); 47 | 48 | verify(client).send(webhookMappings.get(EMPLOYEE_EVENT), DUMMY_PAYLOAD); 49 | } 50 | 51 | @Test 52 | @DisplayName("Should throw exception when type does not exist") 53 | void shouldTNothrowExceptionWhenTypeDoesNotExist() throws IOException { 54 | executor = new SlackReportExecutor(client, mapper, emptyMap()); 55 | 56 | assertDoesNotThrow(() -> executor.send(CUSTOMER_BIRTHDAY, Report.builder().build())); 57 | 58 | verify(client, never()).send(anyString(), any(Payload.class)); 59 | } 60 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/service/audit/AuditDataFetchResultAspectTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.audit; 2 | 3 | import com.whiskels.notifier.MockedClockConfiguration; 4 | import com.whiskels.notifier.reporting.service.ReportData; 5 | import org.aspectj.lang.ProceedingJoinPoint; 6 | import org.aspectj.lang.Signature; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.junit.jupiter.params.ParameterizedTest; 9 | import org.junit.jupiter.params.provider.Arguments; 10 | import org.junit.jupiter.params.provider.MethodSource; 11 | import org.mockito.ArgumentCaptor; 12 | import org.mockito.Captor; 13 | import org.mockito.InjectMocks; 14 | import org.mockito.Mock; 15 | import org.mockito.junit.jupiter.MockitoExtension; 16 | 17 | import java.util.List; 18 | import java.util.stream.Stream; 19 | 20 | import static com.whiskels.notifier.reporting.ReportType.EMPLOYEE_EVENT; 21 | import static org.junit.jupiter.api.Assertions.assertEquals; 22 | import static org.mockito.Mockito.mock; 23 | import static org.mockito.Mockito.verify; 24 | import static org.mockito.Mockito.when; 25 | 26 | @ExtendWith(MockitoExtension.class) 27 | class AuditDataFetchResultAspectTest { 28 | 29 | @Mock 30 | private LoadAuditRepository auditRepository; 31 | 32 | @Mock 33 | private ProceedingJoinPoint proceedingJoinPoint; 34 | 35 | @Captor 36 | ArgumentCaptor captor; 37 | 38 | @InjectMocks 39 | private AuditDataFetchResultAspect auditDataFetchResultAspect; 40 | 41 | @ParameterizedTest 42 | @MethodSource("aroundAdviceTestArgs") 43 | void aroundAdviceTest(T object, int count) throws Throwable { 44 | AuditDataFetchResult dataFetchResultAudit = mock(AuditDataFetchResult.class); 45 | when(dataFetchResultAudit.reportType()).thenReturn(EMPLOYEE_EVENT); 46 | when(proceedingJoinPoint.proceed()).thenReturn(object); 47 | when(proceedingJoinPoint.getSignature()).thenReturn(mock(Signature.class)); 48 | 49 | Object result = auditDataFetchResultAspect.aroundAdvice(proceedingJoinPoint, dataFetchResultAudit); 50 | 51 | assertEquals(object, result); 52 | verify(auditRepository).save(captor.capture()); 53 | assertEquals(count, captor.getValue().getCount()); 54 | } 55 | 56 | static Stream aroundAdviceTestArgs() { 57 | return Stream.of( 58 | Arguments.of(new ReportData<>(List.of("data1", "data2"), MockedClockConfiguration.EXPECTED_DATE), 2), 59 | Arguments.of(new Object(), 1), 60 | Arguments.of(null, 0) 61 | ); 62 | } 63 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/service/audit/LoadAuditTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.audit; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static com.whiskels.notifier.reporting.ReportType.EMPLOYEE_EVENT; 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 9 | 10 | class LoadAuditTest { 11 | 12 | @Test 13 | @DisplayName("Should initialize load audit via static constructor") 14 | void testLoadAudit() { 15 | LoadAudit loadAudit = LoadAudit.loadAudit(5, EMPLOYEE_EVENT); 16 | assertEquals(5, loadAudit.getCount()); 17 | assertEquals(EMPLOYEE_EVENT, loadAudit.getReportType()); 18 | } 19 | 20 | @Test 21 | @DisplayName("Should use hibernate equals") 22 | void testEquals() { 23 | LoadAudit loadAudit1 = LoadAudit.loadAudit(5, EMPLOYEE_EVENT); 24 | loadAudit1.setId(1); 25 | LoadAudit loadAudit2 = LoadAudit.loadAudit(5, EMPLOYEE_EVENT); 26 | loadAudit2.setId(1); 27 | assertEquals(loadAudit1, loadAudit2); 28 | assertNotEquals(loadAudit1, new Object()); 29 | } 30 | 31 | @Test 32 | @DisplayName("Should have equal hash codes") 33 | void testHashCode() { 34 | LoadAudit loadAudit1 = LoadAudit.loadAudit(5, EMPLOYEE_EVENT); 35 | LoadAudit loadAudit2 = LoadAudit.loadAudit(5, EMPLOYEE_EVENT); 36 | assertEquals(loadAudit1.hashCode(), loadAudit2.hashCode()); 37 | } 38 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/service/cleaner/DatabaseCleanerTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.cleaner; 2 | 3 | import com.whiskels.notifier.infrastructure.repository.AbstractRepository; 4 | import com.whiskels.notifier.reporting.service.cleaner.DatabaseCleaner; 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.Mock; 9 | import org.mockito.junit.jupiter.MockitoExtension; 10 | 11 | import java.time.Clock; 12 | import java.time.Instant; 13 | import java.time.LocalDateTime; 14 | import java.time.ZoneId; 15 | import java.util.List; 16 | 17 | import static org.mockito.ArgumentMatchers.any; 18 | import static org.mockito.Mockito.verify; 19 | import static org.mockito.Mockito.when; 20 | 21 | @ExtendWith(MockitoExtension.class) 22 | public class DatabaseCleanerTest { 23 | 24 | @Mock 25 | private AbstractRepository repo1; 26 | 27 | @Mock 28 | private AbstractRepository repo2; 29 | 30 | @Mock 31 | private Clock clock; 32 | 33 | @Test 34 | @DisplayName("Should delete old entries") 35 | void shouldDeleteOldEntries() { 36 | DatabaseCleaner databaseCleaner = new DatabaseCleaner(List.of(repo1, repo2), 1, clock); 37 | 38 | 39 | LocalDateTime now = LocalDateTime.of(2023, 8, 19, 9, 0); 40 | Instant instant = now.atZone(ZoneId.systemDefault()).toInstant(); 41 | when(clock.instant()).thenReturn(instant); 42 | when(clock.getZone()).thenReturn(ZoneId.systemDefault()); 43 | 44 | when(repo1.deleteByDateBefore(any())).thenReturn(5); 45 | when(repo2.deleteByDateBefore(any())).thenReturn(3); 46 | 47 | databaseCleaner.deleteOldEntries(); 48 | 49 | verify(repo1).deleteByDateBefore(any()); 50 | verify(repo2).deleteByDateBefore(any()); 51 | } 52 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/service/customer/birthday/convert/context/BeforeEventReportDataTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.birthday.convert.context; 2 | 3 | import com.whiskels.notifier.reporting.service.customer.birthday.domain.CustomerBirthdayInfo; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.time.LocalDate; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertFalse; 11 | import static org.junit.jupiter.api.Assertions.assertTrue; 12 | 13 | class BeforeEventReportDataTest { 14 | 15 | @Test 16 | @DisplayName("Should initialize header") 17 | void testHeaderGeneration() { 18 | String headerPrefix = "Event in"; 19 | BeforeEventReportContext context = new BeforeEventReportContext(headerPrefix, 5); 20 | LocalDate testDate = LocalDate.of(2024, 2, 20); 21 | String expectedHeader = STR."\{headerPrefix} 20-02-2024"; // Adjust based on actual formatting 22 | assertEquals(expectedHeader, context.getHeaderMapper().apply(testDate)); 23 | } 24 | 25 | @Test 26 | @DisplayName("Should execute skip data predicate") 27 | void testBirthdayPredicate() { 28 | int daysBefore = 5; 29 | BeforeEventReportContext context = new BeforeEventReportContext("Event in", daysBefore); 30 | CustomerBirthdayInfo customer = CustomerBirthdayInfo.builder() 31 | .name("John Doe") 32 | .birthday(LocalDate.of(2024, 2, 25)) 33 | .build(); 34 | LocalDate reportDate = LocalDate.of(2024, 2, 20); 35 | 36 | // Test case where customer's birthday is exactly 5 days after the report date 37 | assertTrue(context.getPredicate().test(customer, reportDate)); 38 | 39 | // Test case where customer's birthday is not 5 days after the report date 40 | assertFalse(context.getPredicate().test(customer, reportDate.minusDays(1))); 41 | } 42 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/service/customer/birthday/convert/context/DailyReportDataTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.birthday.convert.context; 2 | 3 | import com.whiskels.notifier.reporting.service.customer.birthday.domain.CustomerBirthdayInfo; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.time.LocalDate; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertFalse; 11 | import static org.junit.jupiter.api.Assertions.assertTrue; 12 | 13 | class DailyReportDataTest { 14 | 15 | @Test 16 | @DisplayName("Should initialize header") 17 | void testHeaderGeneration() { 18 | String headerPrefix = "Daily Report:"; 19 | DailyReportContext context = new DailyReportContext(headerPrefix); 20 | LocalDate testDate = LocalDate.of(2024, 2, 20); 21 | String expectedHeader = STR."\{headerPrefix} 20-02-2024"; // Adjust based on actual formatting 22 | assertEquals(expectedHeader, context.getHeaderMapper().apply(testDate)); 23 | } 24 | 25 | @Test 26 | @DisplayName("Should execute birthday predicate") 27 | void testBirthdayPredicate() { 28 | DailyReportContext context = new DailyReportContext("Daily Report:"); 29 | CustomerBirthdayInfo customer = CustomerBirthdayInfo.builder() 30 | .name("Jane Doe") 31 | .birthday(LocalDate.of(2024, 2, 20)) 32 | .build(); 33 | LocalDate reportDate = LocalDate.of(2024, 2, 20); 34 | 35 | // Test case where customer's birthday is on the report date 36 | assertTrue(context.getPredicate().test(customer, reportDate)); 37 | 38 | // Test case where customer's birthday is not on the report date 39 | assertFalse(context.getPredicate().test(customer, reportDate.plusDays(1))); 40 | } 41 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/service/customer/birthday/convert/context/MonthMiddleReportDataTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.birthday.convert.context; 2 | 3 | import com.whiskels.notifier.reporting.service.customer.birthday.domain.CustomerBirthdayInfo; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.time.LocalDate; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertFalse; 11 | import static org.junit.jupiter.api.Assertions.assertTrue; 12 | 13 | class MonthMiddleReportDataTest { 14 | 15 | @Test 16 | @DisplayName("Should initialize header") 17 | void testHeaderMapper() { 18 | String expectedHeader = "Monthly Report"; 19 | MonthMiddleReportContext context = new MonthMiddleReportContext(expectedHeader); 20 | assertEquals(expectedHeader, context.getHeaderMapper().apply(LocalDate.now())); 21 | } 22 | 23 | @Test 24 | @DisplayName("Should execute skip data predicate") 25 | void testSkipEmptyPredicate() { 26 | MonthMiddleReportContext context = new MonthMiddleReportContext("Header"); 27 | assertTrue(context.getSkipEmptyPredicate().test(LocalDate.of(2024, 2, 14))); 28 | assertFalse(context.getSkipEmptyPredicate().test(LocalDate.of(2024, 2, 15))); 29 | } 30 | 31 | @Test 32 | @DisplayName("Should execute birthday predicate") 33 | void testBirthdayPredicate() { 34 | MonthMiddleReportContext context = new MonthMiddleReportContext("Header"); 35 | CustomerBirthdayInfo customer = CustomerBirthdayInfo.builder() 36 | .name("John Doe") 37 | .birthday(LocalDate.of(1990, 2, 20)) 38 | .build(); 39 | LocalDate reportDate = LocalDate.of(2024, 2, 15); 40 | 41 | // Test case where birthday is in the same month and after the report date, and the report date is the middle of the month 42 | assertTrue(context.getPredicate().test(customer, reportDate)); 43 | 44 | // Test case where birthday is before the report date 45 | assertFalse(context.getPredicate().test(customer, LocalDate.of(2024, 2, 21))); 46 | 47 | // Test case where report date is not the middle of the month 48 | assertFalse(context.getPredicate().test(customer, LocalDate.of(2024, 2, 10))); 49 | 50 | // Test case where birthday is in a different month 51 | assertFalse(context.getPredicate().test(customer, LocalDate.of(2024, 3, 15))); 52 | } 53 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/service/customer/birthday/convert/context/MonthStartReportDataTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.birthday.convert.context; 2 | 3 | import com.whiskels.notifier.reporting.service.customer.birthday.domain.CustomerBirthdayInfo; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.time.LocalDate; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertFalse; 11 | import static org.junit.jupiter.api.Assertions.assertTrue; 12 | 13 | class MonthStartReportDataTest { 14 | 15 | @Test 16 | @DisplayName("Should initialize header") 17 | void testHeaderInitialization() { 18 | String expectedHeader = "Monthly Start Report"; 19 | MonthStartReportContext context = new MonthStartReportContext(expectedHeader); 20 | assertEquals(expectedHeader, context.getHeaderMapper().apply(LocalDate.now())); 21 | } 22 | 23 | @Test 24 | @DisplayName("Should execute skip data predicate") 25 | void testSkipPredicate() { 26 | MonthStartReportContext context = new MonthStartReportContext("Header"); 27 | // Test case for the start of the month, should not skip 28 | assertFalse(context.getSkipEmptyPredicate().test(LocalDate.of(2024, 2, 1))); 29 | // Test case for not the start of the month, should skip 30 | assertTrue(context.getSkipEmptyPredicate().test(LocalDate.of(2024, 2, 2))); 31 | } 32 | 33 | @Test 34 | @DisplayName("Should execute birthday predicate") 35 | void testBirthdayPredicate() { 36 | MonthStartReportContext context = new MonthStartReportContext("Header"); 37 | CustomerBirthdayInfo customer = CustomerBirthdayInfo.builder() 38 | .name("John Doe") 39 | .birthday(LocalDate.of(1990, 2, 20)) 40 | .build(); 41 | LocalDate reportDate = LocalDate.of(2024, 2, 1); 42 | 43 | // Test case where birthday is in the same month and after the report date, and the report date is the middle of the month 44 | assertTrue(context.getPredicate().test(customer, reportDate)); 45 | 46 | // Test case where birthday is before the report date 47 | assertFalse(context.getPredicate().test(customer, LocalDate.of(2024, 2, 21))); 48 | 49 | // Test case where report date is not the beginning of the month 50 | assertFalse(context.getPredicate().test(customer, LocalDate.of(2024, 2, 10))); 51 | 52 | // Test case where birthday is in a different month 53 | assertFalse(context.getPredicate().test(customer, LocalDate.of(2024, 3, 15))); 54 | } 55 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/service/customer/birthday/mock/CustomerBirthdayInfoFetchServiceMockTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.birthday.mock; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static com.whiskels.notifier.MockedClockConfiguration.CLOCK; 7 | import static org.junit.jupiter.api.Assertions.*; 8 | 9 | class CustomerBirthdayInfoFetchServiceMockTest { 10 | 11 | @Test 12 | @DisplayName("Should load mocked data") 13 | void shouldLoadMockedData() { 14 | var mock = new CustomerBirthdayInfoFetchServiceMock(CLOCK); 15 | 16 | var actual = mock.fetch(); 17 | 18 | assertNotNull(actual); 19 | assertEquals(17, actual.data().size()); 20 | } 21 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/service/customer/debt/convert/CustomerDebtDtoTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.debt.convert; 2 | 3 | import com.whiskels.notifier.reporting.service.customer.debt.domain.CustomerDebt; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.math.BigDecimal; 8 | 9 | import static com.whiskels.notifier.TestUtil.assertEqualsIgnoringCR; 10 | import static org.junit.jupiter.api.Assertions.assertNotNull; 11 | 12 | class CustomerDebtDtoTest { 13 | 14 | @Test 15 | @DisplayName("Should map from customer") 16 | void shouldMapFromCustomer() { 17 | CustomerDebt customerDebt = new CustomerDebt(); 18 | customerDebt.setContractor("Contractor"); 19 | customerDebt.setAccountManager("Account Manager"); 20 | customerDebt.setFinanceSubject("Finance Subject"); 21 | customerDebt.setWayOfPayment("Way Of Payment"); 22 | customerDebt.setCurrency("USD"); 23 | customerDebt.setComment("Comment"); 24 | customerDebt.setTotal(BigDecimal.valueOf(1000)); 25 | 26 | CustomerDebtDto dto = CustomerDebtDto.from(customerDebt); 27 | 28 | assertNotNull(dto); 29 | assertEqualsIgnoringCR(""" 30 | *Contractor* 31 | Finance Subject 32 | Way Of Payment 33 | Account Manager 34 | *1 000 USD* 35 | Comment""", dto.toString()); 36 | 37 | } 38 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/service/customer/debt/fetch/CurrencyRateDataFetchServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.debt.fetch; 2 | 3 | import com.whiskels.notifier.MockedClockConfiguration; 4 | import com.whiskels.notifier.reporting.service.customer.debt.domain.CurrencyRate; 5 | import com.whiskels.notifier.reporting.service.customer.debt.fetch.CurrencyRateDataFetchService; 6 | import com.whiskels.notifier.reporting.service.customer.debt.fetch.CurrencyRateFeignClient; 7 | import org.junit.jupiter.api.DisplayName; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.Mock; 11 | import org.mockito.junit.jupiter.MockitoExtension; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | import static org.mockito.Mockito.when; 15 | 16 | @ExtendWith(MockitoExtension.class) 17 | class CurrencyRateDataFetchServiceTest { 18 | 19 | @Mock 20 | private CurrencyRateFeignClient client; 21 | 22 | private CurrencyRateDataFetchService currencyRateDataFetchService; 23 | 24 | @Test 25 | @DisplayName("Should fetch report data") 26 | void shouldFetchReportData() { 27 | currencyRateDataFetchService = new CurrencyRateDataFetchService(client, MockedClockConfiguration.CLOCK); 28 | when(client.get()).thenReturn(new CurrencyRate()); 29 | 30 | var actual = currencyRateDataFetchService.fetch(); 31 | 32 | assertEquals(1, actual.data().size()); 33 | assertEquals(MockedClockConfiguration.EXPECTED_DATE, actual.requestDate()); 34 | } 35 | 36 | @Test 37 | @DisplayName("Should fetch report data when no data is present") 38 | void shouldFetchReportDataWhenNoDataPresent() { 39 | currencyRateDataFetchService = new CurrencyRateDataFetchService(client, MockedClockConfiguration.CLOCK); 40 | when(client.get()).thenReturn(null); 41 | 42 | var actual = currencyRateDataFetchService.fetch(); 43 | 44 | assertEquals(0, actual.data().size()); 45 | assertEquals(MockedClockConfiguration.EXPECTED_DATE, actual.requestDate()); 46 | } 47 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/service/customer/debt/mock/CustomerDebtFetchServiceMockTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.debt.mock; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static com.whiskels.notifier.MockedClockConfiguration.CLOCK; 7 | import static org.junit.jupiter.api.Assertions.*; 8 | 9 | class CustomerDebtFetchServiceMockTest { 10 | 11 | @Test 12 | @DisplayName("Should load mocked data") 13 | void shouldLoadMockedData() { 14 | var mock = new CustomerDebtFetchServiceMock(CLOCK); 15 | 16 | var actual = mock.fetch(); 17 | 18 | assertNotNull(actual); 19 | assertEquals(13, actual.data().size()); 20 | } 21 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/service/customer/payment/fetch/FinOperationReloadSchedulerTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.payment.fetch; 2 | 3 | import com.whiskels.notifier.reporting.service.DataFetchService; 4 | import com.whiskels.notifier.reporting.service.customer.payment.domain.FinancialOperation; 5 | import com.whiskels.notifier.reporting.service.customer.payment.fetch.FinOperationReloadScheduler; 6 | import org.junit.jupiter.api.DisplayName; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | import org.mockito.junit.jupiter.MockitoExtension; 12 | 13 | import static org.mockito.Mockito.verify; 14 | 15 | @ExtendWith(MockitoExtension.class) 16 | public class FinOperationReloadSchedulerTest { 17 | 18 | @Mock 19 | private DataFetchService dataFetchService; 20 | 21 | @InjectMocks 22 | private FinOperationReloadScheduler finOperationReloadScheduler; 23 | 24 | @Test 25 | @DisplayName("Should execute scheduled reload") 26 | public void shouldExecuteScheduledReload() { 27 | finOperationReloadScheduler.loadScheduled(); 28 | 29 | verify(dataFetchService).fetch(); 30 | } 31 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/service/customer/payment/fetch/PaymentReportDataFetchServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.payment.fetch; 2 | 3 | import com.whiskels.notifier.reporting.service.audit.LoadAuditRepository; 4 | import com.whiskels.notifier.reporting.service.customer.payment.domain.FinancialOperation; 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.InjectMocks; 9 | import org.mockito.Mock; 10 | import org.mockito.junit.jupiter.MockitoExtension; 11 | 12 | import java.time.LocalDate; 13 | import java.time.LocalDateTime; 14 | import java.time.LocalTime; 15 | import java.util.List; 16 | import java.util.Optional; 17 | 18 | import static com.whiskels.notifier.MockedClockConfiguration.CLOCK; 19 | import static org.junit.jupiter.api.Assertions.assertEquals; 20 | import static org.mockito.ArgumentMatchers.any; 21 | import static org.mockito.Mockito.verifyNoInteractions; 22 | import static org.mockito.Mockito.when; 23 | 24 | @ExtendWith(MockitoExtension.class) 25 | class PaymentReportDataFetchServiceTest { 26 | @Mock 27 | private FinOperationRepository repository; 28 | @Mock 29 | private LoadAuditRepository auditRepository; 30 | 31 | @InjectMocks 32 | private PaymentReportDataFetchService paymentReportDataFetchService; 33 | 34 | @Test 35 | @DisplayName("Should fetch payment data") 36 | void shouldFetchPaymentData() { 37 | var expectedDateTime = LocalDateTime.now(CLOCK); 38 | when(auditRepository.findLastUpdateDateTime(any())) 39 | .thenReturn(Optional.of(expectedDateTime)); 40 | when(repository.getAllByCategoryAndDateBetween( 41 | "Revenue", expectedDateTime.with(LocalTime.MIN), expectedDateTime.with(LocalTime.MAX)) 42 | ).thenReturn(List.of(new FinancialOperation())); 43 | 44 | var actual = paymentReportDataFetchService.fetch(); 45 | 46 | assertEquals(expectedDateTime.toLocalDate(), actual.requestDate()); 47 | assertEquals(1, actual.data().size()); 48 | } 49 | 50 | @Test 51 | @DisplayName("Should return empty payment data when no last update date time") 52 | void shouldFetchEmptyPaymentData() { 53 | when(auditRepository.findLastUpdateDateTime(any())) 54 | .thenReturn(Optional.empty()); 55 | 56 | var actual = paymentReportDataFetchService.fetch(); 57 | 58 | verifyNoInteractions(repository); 59 | assertEquals(LocalDate.now(), actual.requestDate()); 60 | assertEquals(0, actual.data().size()); 61 | } 62 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/service/customer/payment/mock/CustomerPaymentDtoFetchMockTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.customer.payment.mock; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static com.whiskels.notifier.MockedClockConfiguration.CLOCK; 7 | import static org.junit.jupiter.api.Assertions.*; 8 | 9 | class CustomerPaymentDtoFetchMockTest { 10 | 11 | @Test 12 | @DisplayName("Should load mocked data") 13 | void shouldLoadMockedData() { 14 | var mock = new CustomerPaymentDtoFetchMock(CLOCK); 15 | 16 | var actual = mock.fetch(); 17 | 18 | assertNotNull(actual); 19 | assertEquals(10, actual.data().size()); 20 | } 21 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/service/employee/convert/EmployeeDtoTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.employee.convert; 2 | 3 | import com.whiskels.notifier.reporting.service.employee.domain.Employee; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.time.Clock; 8 | import java.time.LocalDate; 9 | import java.time.ZoneId; 10 | 11 | import static org.junit.jupiter.api.Assertions.*; 12 | 13 | class EmployeeDtoTest { 14 | 15 | @Test 16 | @DisplayName("Should create dto from employee") 17 | void shouldCreateFromEmployee() { 18 | Employee employee = new Employee("John Doe", LocalDate.of(1990, 1, 1), LocalDate.of(2015, 1, 1), null, null); 19 | EmployeeDto dto = EmployeeDto.from(employee); 20 | assertEquals("John Doe", dto.name()); 21 | assertEquals(LocalDate.of(1990, 1, 1), dto.birthday()); 22 | assertEquals(LocalDate.of(2015, 1, 1), dto.appointmentDate()); 23 | } 24 | 25 | @Test 26 | @DisplayName("Should map to work anniversary string") 27 | void testToWorkAnniversaryWithMockedClock() { 28 | Clock fixedClock = Clock.fixed(LocalDate.of(2024, 1, 1).atStartOfDay(ZoneId.systemDefault()).toInstant(), ZoneId.systemDefault()); 29 | EmployeeDto dto = new EmployeeDto("John Doe", LocalDate.of(1990, 1, 1), LocalDate.of(2015, 1, 1)); 30 | String expected = "John Doe 01.01 (9)"; 31 | assertEquals(expected, dto.toWorkAnniversaryString(fixedClock)); 32 | } 33 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/service/employee/convert/context/BeforeEventReportDataTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.employee.convert.context; 2 | 3 | import com.whiskels.notifier.reporting.service.employee.domain.Employee; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.time.LocalDate; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertFalse; 11 | import static org.junit.jupiter.api.Assertions.assertTrue; 12 | 13 | class BeforeEventReportDataTest { 14 | 15 | @Test 16 | @DisplayName("Should initialize header") 17 | void testHeaderGeneration() { 18 | String headerPrefix = "Event in"; 19 | BeforeEventReportContext context = new BeforeEventReportContext(headerPrefix, 5); 20 | LocalDate testDate = LocalDate.of(2024, 2, 20); 21 | String expectedHeader = STR."\{headerPrefix} 20-02-2024"; // Adjust based on actual formatting 22 | assertEquals(expectedHeader, context.getHeaderMapper().apply(testDate)); 23 | } 24 | 25 | @Test 26 | @DisplayName("Should execute predicates") 27 | void testPredicate() { 28 | int daysBefore = 5; 29 | BeforeEventReportContext context = new BeforeEventReportContext("Event in", daysBefore); 30 | Employee employee = new Employee(); 31 | employee.setBirthday(LocalDate.of(2000, 2, 25)); 32 | employee.setAppointmentDate(LocalDate.of(2022, 1, 6)); 33 | 34 | assertTrue(context.getBirthdayPredicate().test(employee, LocalDate.of(2024, 2, 20))); 35 | assertTrue(context.getAnniversaryPredicate().test(employee, LocalDate.of(2023, 1, 1))); 36 | 37 | assertFalse(context.getBirthdayPredicate().test(employee, LocalDate.of(2024, 2, 19))); 38 | assertFalse(context.getAnniversaryPredicate().test(employee, LocalDate.of(2023, 1, 2))); 39 | } 40 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/service/employee/convert/context/DailyReportDataTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.employee.convert.context; 2 | 3 | import com.whiskels.notifier.reporting.service.employee.domain.Employee; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.time.LocalDate; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertFalse; 11 | import static org.junit.jupiter.api.Assertions.assertTrue; 12 | 13 | class DailyReportDataTest { 14 | 15 | @Test 16 | @DisplayName("Should initialize header") 17 | void testHeaderGeneration() { 18 | String headerPrefix = "Daily Report:"; 19 | DailyReportContext context = new DailyReportContext(headerPrefix); 20 | LocalDate testDate = LocalDate.of(2024, 2, 20); 21 | String expectedHeader = STR."\{headerPrefix} 20-02-2024"; // Adjust based on actual formatting 22 | assertEquals(expectedHeader, context.getHeaderMapper().apply(testDate)); 23 | } 24 | 25 | @Test 26 | @DisplayName("Should execute predicates") 27 | void testPredicates() { 28 | DailyReportContext context = new DailyReportContext("Daily Report:"); 29 | Employee employee = new Employee(); 30 | employee.setBirthday(LocalDate.of(2000, 2, 25)); 31 | employee.setAppointmentDate(LocalDate.of(2022, 1, 6)); 32 | 33 | assertTrue(context.getBirthdayPredicate().test(employee, LocalDate.of(2024, 2, 25))); 34 | assertTrue(context.getAnniversaryPredicate().test(employee, LocalDate.of(2024, 1, 6))); 35 | 36 | assertFalse(context.getBirthdayPredicate().test(employee, LocalDate.of(2024, 3, 25))); 37 | assertFalse(context.getAnniversaryPredicate().test(employee, LocalDate.of(2024, 2, 6))); 38 | } 39 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/service/employee/domain/BirthdayDeserializerTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.employee.domain; 2 | 3 | import com.fasterxml.jackson.core.JsonParser; 4 | import com.fasterxml.jackson.databind.DeserializationContext; 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.Mock; 9 | import org.mockito.junit.jupiter.MockitoExtension; 10 | 11 | import java.io.IOException; 12 | import java.time.LocalDate; 13 | 14 | import static org.junit.jupiter.api.Assertions.assertEquals; 15 | import static org.mockito.Mockito.when; 16 | 17 | @ExtendWith(MockitoExtension.class) 18 | class BirthdayDeserializerTest { 19 | private final BirthdayDeserializer deserializer = new BirthdayDeserializer(); 20 | 21 | @Mock 22 | private JsonParser jsonParser; 23 | 24 | @Mock 25 | private DeserializationContext deserializationContext; 26 | 27 | 28 | @Test 29 | @DisplayName("Should deserialize using deserializer") 30 | void testDeserialize() throws IOException { 31 | String birthdayStr = "20.12"; 32 | LocalDate expectedDate = LocalDate.of(LocalDate.now().getYear(), 12, 20); 33 | 34 | when(jsonParser.getText()).thenReturn(birthdayStr); 35 | 36 | LocalDate actualDate = deserializer.deserialize(jsonParser, deserializationContext); 37 | 38 | assertEquals(expectedDate, actualDate); 39 | } 40 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/service/employee/domain/EmployeeTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.employee.domain; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.time.LocalDate; 8 | 9 | import static com.whiskels.notifier.JsonUtils.MAPPER; 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | 12 | class EmployeeTest { 13 | 14 | private Employee employee; 15 | 16 | @BeforeEach 17 | void setUp() { 18 | employee = new Employee(); 19 | } 20 | 21 | @Test 22 | @DisplayName("Should retrieve data via getters") 23 | void testProperties() { 24 | employee.setName("John Doe"); 25 | employee.setBirthday(LocalDate.of(1990, 1, 1)); 26 | employee.setAppointmentDate(LocalDate.of(2022, 1, 1)); 27 | employee.setStatus("Active"); 28 | employee.setStatusSystem("SystemX"); 29 | 30 | assertEquals("John Doe", employee.getName()); 31 | assertEquals(LocalDate.of(1990, 1, 1), employee.getBirthday()); 32 | assertEquals(LocalDate.of(2022, 1, 1), employee.getAppointmentDate()); 33 | assertEquals("Active", employee.getStatus()); 34 | assertEquals("SystemX", employee.getStatusSystem()); 35 | } 36 | 37 | @Test 38 | @DisplayName("Should deserialize from json") 39 | void testDeserialization() throws Exception { 40 | String json = """ 41 | { 42 | "name": "John Doe", 43 | "birthday": "01.01", 44 | "appointment_date": "2022-01-01", 45 | "status": "Active", 46 | "status_system": "SystemX" 47 | } 48 | """; 49 | 50 | Employee deserializedEmployee = MAPPER.readValue(json, Employee.class); 51 | 52 | assertEquals("John Doe", deserializedEmployee.getName()); 53 | assertEquals(LocalDate.of(LocalDate.now().getYear(), 1, 1), deserializedEmployee.getBirthday()); 54 | assertEquals(LocalDate.of(2022, 1, 1), deserializedEmployee.getAppointmentDate()); 55 | assertEquals("Active", deserializedEmployee.getStatus()); 56 | assertEquals("SystemX", deserializedEmployee.getStatusSystem()); 57 | } 58 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/service/employee/fetch/EmployeeFeignClientTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.employee.fetch; 2 | 3 | import com.github.tomakehurst.wiremock.WireMockServer; 4 | import com.whiskels.notifier.reporting.WireMockTestConfig; 5 | import com.whiskels.notifier.reporting.service.employee.domain.Employee; 6 | import org.junit.jupiter.api.DisplayName; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.cloud.openfeign.EnableFeignClients; 11 | import org.springframework.context.annotation.Import; 12 | import org.springframework.test.annotation.DirtiesContext; 13 | import org.springframework.test.context.ActiveProfiles; 14 | 15 | import java.time.LocalDate; 16 | import java.util.List; 17 | 18 | import static com.whiskels.notifier.reporting.WireMockTestConfig.setMockResponse; 19 | import static org.junit.jupiter.api.Assertions.assertEquals; 20 | 21 | @ActiveProfiles("test") 22 | @DirtiesContext 23 | @SpringBootTest(properties = "report.parameters.employee-event.url= http://localhost:9561/employees") 24 | @EnableFeignClients(clients = EmployeeFeignClient.class) 25 | @Import({WireMockTestConfig.class}) 26 | class EmployeeFeignClientTest { 27 | 28 | @Autowired 29 | private WireMockServer wireMockServer; 30 | 31 | @Autowired 32 | private EmployeeFeignClient employeeFeignClient; 33 | 34 | @Test 35 | @DisplayName("Should fetch employee data") 36 | void shouldFetchEmployeeData() { 37 | String responseBody = """ 38 | [ 39 | { 40 | "name": "John Doe", 41 | "birthday": "01.01", 42 | "appointment_date": "2022-01-01", 43 | "status": "Active", 44 | "status_system": "SystemX" 45 | } 46 | ] 47 | """; 48 | setMockResponse(wireMockServer, "/employees", responseBody); 49 | 50 | List employees = employeeFeignClient.get(); 51 | 52 | assertEquals(1, employees.size()); 53 | Employee employee = employees.getFirst(); 54 | assertEquals("John Doe", employee.getName()); 55 | assertEquals(LocalDate.of(LocalDate.now().getYear(), 1, 1), employee.getBirthday()); 56 | assertEquals(LocalDate.of(2022, 1, 1), employee.getAppointmentDate()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/reporting/service/employee/mock/EmployeeFetchMockTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.reporting.service.employee.mock; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static com.whiskels.notifier.MockedClockConfiguration.CLOCK; 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | import static org.junit.jupiter.api.Assertions.assertNotNull; 9 | 10 | class EmployeeFetchMockTest { 11 | @Test 12 | @DisplayName("Should load mocked data") 13 | void shouldLoadMockedData() { 14 | var mock = new EmployeeFetchMock(CLOCK); 15 | 16 | var actual = mock.fetch(); 17 | 18 | assertNotNull(actual); 19 | assertEquals(20, actual.data().size()); 20 | } 21 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/utilities/UtilTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.utilities; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import static org.junit.jupiter.api.Assertions.assertNull; 8 | 9 | class UtilTest { 10 | @Test 11 | @DisplayName("Should return value if it is not null") 12 | void shouldReturnValueWhenArgumentIsNotNull() { 13 | String result = Util.defaultIfNull("Hello", "Default"); 14 | assertEquals("Hello", result); 15 | } 16 | 17 | @Test 18 | @DisplayName("Should return default value if argument is null") 19 | void shouldReturnDefaultValueWhenArgumentIsNull() { 20 | String result = Util.defaultIfNull(null, "Default"); 21 | assertEquals("Default", result); 22 | } 23 | 24 | @Test 25 | @DisplayName("Should return null if both values are null") 26 | void shouldReturnNullWhenBothValuesAreNull() { 27 | String result = Util.defaultIfNull(null, null); 28 | assertNull(result); 29 | } 30 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/utilities/collections/StreamUtilTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.utilities.collections; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.Arrays; 7 | import java.util.Comparator; 8 | import java.util.List; 9 | import java.util.function.Predicate; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | 13 | class StreamUtilTest { 14 | @Test 15 | @DisplayName("Should filter and sort Comparable") 16 | void filterAndSortComparable() { 17 | List list = Arrays.asList(5, 3, 2, 4, 1); 18 | 19 | Predicate greaterThanThree = num -> num > 3; 20 | List result = StreamUtil.filterAndSort(list, greaterThanThree); 21 | 22 | assertEquals(Arrays.asList(4, 5), result); 23 | } 24 | 25 | @Test 26 | @DisplayName("Should filter and sort using comparator") 27 | void filterAndSortWithComparator() { 28 | List list = Arrays.asList("apple", "banana", "cherry", "date"); 29 | 30 | Predicate lengthGreaterThanFour = str -> str.length() > 4; 31 | Comparator reverseAlphabeticalOrder = Comparator.reverseOrder(); 32 | List result = StreamUtil.filterAndSort(list, reverseAlphabeticalOrder, lengthGreaterThanFour); 33 | 34 | assertEquals(Arrays.asList("cherry", "banana", "apple"), result); 35 | } 36 | 37 | @Test 38 | @DisplayName("Should map correctly") 39 | void map() { 40 | List list = Arrays.asList(1, 2, 3, 4, 5); 41 | List result = StreamUtil.map(list, Object::toString); 42 | 43 | assertEquals(Arrays.asList("1", "2", "3", "4", "5"), result); 44 | } 45 | 46 | @Test 47 | @DisplayName("Should collect to bullet list") 48 | void collectToBulletListString() { 49 | List list = Arrays.asList("apple", "banana", "cherry"); 50 | String result = StreamUtil.collectToBulletListString(list, String::toUpperCase); 51 | 52 | assertEquals(String.format("• APPLE%n• BANANA%n• CHERRY"), result); 53 | } 54 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/utilities/formatters/DateTimeFormatterTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.utilities.formatters; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.time.LocalDate; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | class DateTimeFormatterTest { 11 | 12 | @Test 13 | @DisplayName("Should correctly represent birthday format") 14 | void birthdayFormatPattern() { 15 | assertEquals("dd.MM", DateTimeFormatter.BIRTHDAY_FORMAT); 16 | } 17 | 18 | @Test 19 | @DisplayName("Should correctly convert date") 20 | void dayMonthYearFormatter() { 21 | LocalDate date = LocalDate.of(2023, 8, 21); 22 | String formatted = date.format(DateTimeFormatter.DAY_MONTH_YEAR_FORMATTER); 23 | 24 | assertEquals("21-08-2023", formatted); 25 | } 26 | 27 | @Test 28 | @DisplayName("Should correctly format birthday") 29 | void birthdayFormatter() { 30 | LocalDate date = LocalDate.of(2023, 8, 21); 31 | String formatted = date.format(DateTimeFormatter.BIRTHDAY_FORMATTER); 32 | 33 | assertEquals("21.08", formatted); 34 | } 35 | } -------------------------------------------------------------------------------- /src/test/java/com/whiskels/notifier/utilities/formatters/StringFormatterTest.java: -------------------------------------------------------------------------------- 1 | package com.whiskels.notifier.utilities.formatters; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.stream.Stream; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | class StringFormatterTest { 11 | @Test 12 | @DisplayName("Should collect string with new lines") 13 | void shouldCollectStringWithNewLine() { 14 | String joined = Stream.of("Hello", "World") 15 | .collect(StringFormatter.COLLECTOR_NEW_LINE); 16 | 17 | assertEquals(String.format("Hello%nWorld"), joined); 18 | } 19 | 20 | @Test 21 | @DisplayName("Should format number correctly") 22 | void shouldFormatNumberCorrectly() { 23 | String formatted = StringFormatter.format(1234567890.5678); 24 | 25 | assertEquals("1 234 567 890.6", formatted); // Rounded to 1 decimal place 26 | } 27 | 28 | @Test 29 | @DisplayName("Should format number without decimal part correctly") 30 | void shouldFormatNumberWithoutDecimalCorrectly() { 31 | String formatted = StringFormatter.format(1234567890); 32 | 33 | assertEquals("1 234 567 890", formatted); 34 | } 35 | } -------------------------------------------------------------------------------- /src/test/resources/application-test-containers.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: jdbc:tc:postgresql:16.2:///test_database 4 | username: username 5 | password: password 6 | driverClassName: org.testcontainers.jdbc.ContainerDatabaseDriver 7 | jpa: 8 | hibernate: 9 | ddl-auto: create 10 | -------------------------------------------------------------------------------- /src/test/resources/json/telegram/update/callbackquery/token.json: -------------------------------------------------------------------------------- 1 | { 2 | "update_id": 488830481, 3 | "callback_query": { 4 | "id": "377835151230330148", 5 | "from": { 6 | "id": 1, 7 | "is_bot": false, 8 | "first_name": "Test", 9 | "last_name": "User", 10 | "username": "test_user1", 11 | "language_code": "ru" 12 | }, 13 | "message": { 14 | "message_id": 1751, 15 | "from": { 16 | "id": 1, 17 | "is_bot": true, 18 | "first_name": "bot", 19 | "username": "bot" 20 | }, 21 | "chat": { 22 | "id": 1, 23 | "first_name": "Test", 24 | "last_name": "User", 25 | "username": "test_user1", 26 | "language_code": "ru", 27 | "type": "private" 28 | }, 29 | "date": 1618139308, 30 | "text": "Hello. I'm TelegramNotifierBot\nHere are your available commands\nUse /help command to display this message", 31 | "entities": [ 32 | { 33 | "offset": 11, 34 | "length": 19, 35 | "type": "bold" 36 | }, 37 | { 38 | "offset": 68, 39 | "length": 5, 40 | "type": "bot_command" 41 | } 42 | ], 43 | "reply_markup": { 44 | "inline_keyboard": [ 45 | [ 46 | { 47 | "text": "Show your token", 48 | "callback_data": "/TOKEN" 49 | } 50 | ] 51 | ] 52 | } 53 | }, 54 | "chat_instance": "12112341", 55 | "data": "/TOKEN" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/resources/json/telegram/update/message/help.json: -------------------------------------------------------------------------------- 1 | { 2 | "update_id": 1234567, 3 | "message": { 4 | "message_id": 123, 5 | "from": { 6 | "id": 1, 7 | "is_bot": false, 8 | "first_name": "Test", 9 | "last_name": "User", 10 | "username": "test_user1", 11 | "language_code": "ru" 12 | }, 13 | "chat": { 14 | "id": 123, 15 | "first_name": "Test", 16 | "last_name": "User", 17 | "username": "test_user1", 18 | "type": "private" 19 | }, 20 | "date": 1620126119, 21 | "text": "/HELP", 22 | "entities": [ 23 | { 24 | "offset": 0, 25 | "length": 12, 26 | "type": "bot_command" 27 | } 28 | ] 29 | } 30 | } -------------------------------------------------------------------------------- /system.properties: -------------------------------------------------------------------------------- 1 | java.runtime.version=21.0.2 --------------------------------------------------------------------------------