diff --git a/.devops/performance-test-pipelines.yml b/.devops/performance-test-pipelines.yml new file mode 100644 index 0000000..ea3c4e5 --- /dev/null +++ b/.devops/performance-test-pipelines.yml @@ -0,0 +1,106 @@ +# azure-pipelines.yml +trigger: none + +parameters: + - name: "ENVIRONMENT" + displayName: "Environment" + type: string + values: + - "dev" + - "uat" + default: "uat" + - name: "TEST_TYPE" + displayName: "Test type" + type: string + values: + - "constant" + - "constant-prod" + - "load" + - "spike" + - "stress" + default: "constant" + - name: "SCRIPT" + displayName: "Script name" + type: string + values: + - receipt_processor + - receipt_flow_simulation + default: "receipt_processor" + - name : "SLEEP_INTERVAL" + displayName: "Sleep before monitoring" + type: number + default: 300 + - name: "DB_NAME" + displayName: "DB name" + type: string + values: + - pagopa_receipt_pdf_helpdeskk6 + +variables: + ${{ if eq(parameters['ENVIRONMENT'], 'dev') }}: + receiptCosmosSubscriptionKey: "$(DEV_RECEIPT_COSMOS_DB_SUBSCRIPTION_KEY)" + bizEventCosmosSubscriptionKey: "$(DEV_BIZEVENT_COSMOS_DB_SUBSCRIPTION_KEY)" + blobStorageConnectionString: "$(DEV_BLOB_STORAGE_CONNECTION_STRING)" + receiptCosmosConnectionString: "$(DEV_RECEIPT_COSMOS_DB_CONNECTION_STRING)" + bizeventCosmosConnectionString: "$(DEV_BIZ_COSMOS_DB_CONNECTION_STRING)" + poolImage: "pagopa-dev-loadtest-linux" + ${{ if eq(parameters['ENVIRONMENT'], 'uat') }}: + receiptCosmosSubscriptionKey: "$(UAT_RECEIPT_COSMOS_DB_SUBSCRIPTION_KEY)" + bizEventCosmosSubscriptionKey: "$(UAT_BIZEVENT_COSMOS_DB_SUBSCRIPTION_KEY)" + blobStorageConnectionString: "$(UAT_BLOB_STORAGE_CONNECTION_STRING)" + receiptCosmosConnectionString: "$(UAT_RECEIPT_COSMOS_DB_CONNECTION_STRING)" + bizeventCosmosConnectionString: "$(UAT_BIZ_COSMOS_DB_CONNECTION_STRING)" + poolImage: "pagopa-uat-loadtest-linux" + +pool: + name: $(poolImage) + +steps: + - script: | + cd ./performance-test/src + docker pull grafana/k6 + displayName: Pull k6 image + - script: | + cd ./performance-test + sh ./run_performance_test.sh ${{ parameters.ENVIRONMENT }} ${{ parameters.TEST_TYPE }} ${{ parameters.SCRIPT }} ${{ parameters.DB_NAME }} $BIZEVENT_COSMOS_SUBSCRIPTION_KEY $RECEIPT_COSMOS_SUBSCRIPTION_KEY + displayName: Run k6 ${{ parameters.SCRIPT }} on ${{ parameters.ENVIRONMENT }} + env: + RECEIPT_COSMOS_SUBSCRIPTION_KEY: ${{ variables.receiptCosmosSubscriptionKey }} + BIZEVENT_COSMOS_SUBSCRIPTION_KEY: ${{ variables.bizEventCosmosSubscriptionKey }} + - script: | + sleep ${{ parameters.SLEEP_INTERVAL}} + displayName: Wait receipt to be processed + condition: ${{ eq(parameters['SCRIPT'], 'receipt_flow_simulation') }} + - script: | + cd ./performance-test/src + docker build -f ./DockerfileReview -t exec-node . + docker run --rm --name initToRunk6 \ + -e BLOB_STORAGE_CONN_STRING=${BLOB_STORAGE_CONN_STRING} \ + -e RECEIPT_COSMOS_CONN_STRING=${RECEIPT_COSMOS_CONN_STRING} \ + -e BIZEVENT_COSMOS_CONN_STRING=${BIZEVENT_COSMOS_CONN_STRING} \ + -e ENVIRONMENT_STRING="${ENVIRONMENT_STRING}" \ + exec-node + displayName: Run Receipts Timestamp Review + condition: ${{ eq(parameters['SCRIPT'], 'receipt_flow_simulation') }} + env: + RECEIPT_COSMOS_CONN_STRING: ${{ variables.receiptCosmosConnectionString }} + BIZEVENT_COSMOS_CONN_STRING: ${{ variables.bizeventCosmosConnectionString }} + BLOB_STORAGE_CONN_STRING: ${{ variables.blobStorageConnectionString }} + ENVIRONMENT_STRING: ${{ parameters.ENVIRONMENT }} +# - script: | +# cd ./performance-test/src +# docker build -f ./DockerfileTeardown -t exec-node . +# docker run --rm --name initToRunk6 \ +# -e BLOB_STORAGE_CONN_STRING=${BLOB_STORAGE_CONN_STRING} \ +# -e RECEIPT_COSMOS_CONN_STRING=${RECEIPT_COSMOS_CONN_STRING} \ +# -e BIZEVENT_COSMOS_CONN_STRING=${BIZEVENT_COSMOS_CONN_STRING} \ +# -e ENVIRONMENT_STRING="${ENVIRONMENT_STRING}" \ +# exec-node +# displayName: Run Receipts Teardown +# condition: ${{ eq(parameters['SCRIPT'], 'receipt_flow_simulation') }} +# env: +# RECEIPT_COSMOS_CONN_STRING: ${{ variables.receiptCosmosConnectionString }} +# BIZEVENT_COSMOS_CONN_STRING: ${{ variables.bizeventCosmosConnectionString }} +# BLOB_STORAGE_CONN_STRING: ${{ variables.blobStorageConnectionString }} +# ENVIRONMENT_STRING: ${{ parameters.ENVIRONMENT }} + diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..540ba56 --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ +FUNCTIONS_WORKER_RUNTIME=java +AzureWebJobsStorage=DefaultEndpointsProtocol=https;AccountName= + +RECEIPT_QUEUE_CONN_STRING=DefaultEndpointsProtocol=https;AccountName= +RECEIPT_QUEUE_TOPIC=pagopa-d-weu-receipts-queue-receipt-waiting-4-gen +RECEIPT_QUEUE_DELAY=1 + +COSMOS_BIZ_EVENT_CONN_STRING=AccountEndpoint=https://pagopa-d-weu-bizevents-ds-cosmos-account.documents.azure.com:443/;AccountKey= +COSMOS_RECEIPTS_CONN_STRING=AccountEndpoint=https://pagopa-d-weu-receipts-ds-cosmos-account.documents.azure.com:443/;AccountKey= + +COSMOS_RECEIPT_SERVICE_ENDPOINT=https://pagopa-d-weu-receipts-ds-cosmos-account.documents.azure.com:443/ +COSMOS_RECEIPT_KEY= +COSMOS_RECEIPT_DB_NAME=db +COSMOS_RECEIPT_CONTAINER_NAME=receipts + +COSMOS_BIZ_EVENT_SERVICE_ENDPOINT=https://pagopa-d-weu-bizevents-ds-cosmos-account.documents.azure.com:443/ +COSMOS_BIZ_EVENT_KEY= +COSMOS_BIZ_EVENT_DB_NAME=db +COSMOS_BIZ_EVENT_CONTAINER_NAME=biz-events + +MAX_DATE_DIFF_MILLIS=360000 diff --git a/.github/maven_code_review/action.yml b/.github/maven_code_review/action.yml new file mode 100644 index 0000000..a268959 --- /dev/null +++ b/.github/maven_code_review/action.yml @@ -0,0 +1,101 @@ +name: Maven Code Review +description: "Code Review for Pull Request" + +inputs: + github_token: + required: true + type: string + description: Github Token + sonar_token: + required: true + type: string + description: Sonar Token for the login + project_key: + required: true + type: string + description: Key of the project on SonarCloud + coverage_exclusions: + required: false + type: string + description: Files to exclude from coverage + default: '**/config/*,**/*Mock*,**/model/**,**/entity/*' + cpd_exclusions: + required: false + type: string + description: Files to exclude from code duplication + default: '**/model/**,**/entity/*' + jdk_version: + required: true + type: string + description: JDK version + default: 11 + maven_version: + required: true + type: string + description: Maven version + default: 3.8.2 + +runs: + using: "composite" + steps: + - uses: actions/checkout@v2 + + - name: Set up JDK + uses: actions/setup-java@v1 + with: + java-version: ${{ inputs.jdk_version }} + + - name: Set up Maven + uses: stCarolas/setup-maven@v4.5 + with: + maven-version: ${{ inputs.maven_version }} + + - name: Cache Maven packages + uses: actions/cache@v1 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + + - name: Cache SonarCloud packages + uses: actions/cache@v1 + with: + path: ~/.sonar-project.properties/cache + key: ${{ runner.os }}-sonar-project.properties + restore-keys: ${{ runner.os }}-sonar-project.properties + + - name: Build and analyze on Pull Requests + if: ${{ github.event_name == 'pull_request' }} + shell: bash + run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar + -Dsonar.organization=pagopa + -Dsonar.projectKey=${{ env.PROJECT_KEY }} + -Dsonar.coverage.jacoco.xmlReportPaths=./target/jacoco-report/jacoco.xml + -Dsonar.coverage.exclusions=${{inputs.coverage_exclusions}} + -Dsonar.cpd.exclusions=${{inputs.cpd_exclusions}} + -Dsonar.host.url=https://sonarcloud.io + -Dsonar.login=${{ inputs.sonar_token }} + -Dsonar.pullrequest.key=${{ github.event.pull_request.number }} + -Dsonar.pullrequest.branch=${{ github.head_ref }} + -Dsonar.pullrequest.base=${{ github.base_ref }} + env: + # Needed to get some information about the pull request, if any + GITHUB_TOKEN: ${{ inputs.github_token }} + # SonarCloud access token should be generated from https://sonarcloud.io/account/security/ + SONAR_TOKEN: ${{ inputs.sonar_token }} + + - name: Build and analyze on Push main + if: ${{ github.event_name != 'pull_request' }} + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + SONAR_TOKEN: ${{ inputs.sonar_token }} + run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar + -Dsonar.organization=pagopa + -Dsonar.projectKey=${{ env.PROJECT_KEY }} + -Dsonar.coverage.jacoco.xmlReportPaths=./target/site/jacoco/jacoco.xml + -Dsonar.coverage.exclusions=${{inputs.coverage_exclusions}} + -Dsonar.cpd.exclusions=${{inputs.cpd_exclusions}} + -Dsonar.branch.name=${{ github.head_ref }} + -Dsonar.host.url=https://sonarcloud.io + -Dsonar.login=${{ inputs.sonar_token }} \ No newline at end of file diff --git a/.github/workflows/assignee.yml b/.github/workflows/assignee.yml deleted file mode 100644 index 0611917..0000000 --- a/.github/workflows/assignee.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Auto Assign - -# Controls when the workflow will run -on: - # Triggers the workflow on push or pull request events but only for the main branch - pull_request_target: - branches: - - main - types: [ opened, reopened ] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - build: - # The type of runner that the job will run on - runs-on: ubuntu-latest - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - - name: Assign Me - # You may pin to the exact commit or the version. - uses: kentaro-m/auto-assign-action@v1.2.1 - with: - configuration-path: '.github/auto_assign.yml' diff --git a/.github/workflows/check_metadata_pr.yml b/.github/workflows/check_metadata_pr.yml deleted file mode 100644 index c687c53..0000000 --- a/.github/workflows/check_metadata_pr.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Check PR - -# Controls when the workflow will run -on: - pull_request_target: - branches: - - main - types: [ opened, labeled, unlabeled, reopened ] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - build: - name: Check Labels - # The type of runner that the job will run on - runs-on: ubuntu-latest - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - - - name: Verify PR Labels - uses: jesusvasquez333/verify-pr-label-action@v1.4.0 - with: - github-token: '${{ secrets.GITHUB_TOKEN }}' - valid-labels: 'bug, enhancement, breaking-change, ignore-for-release' - pull-request-number: '${{ github.event.pull_request.number }}' - - - name: Label Check - if: ${{ !contains(github.event.pull_request.labels.*.name, 'breaking-change') && !contains(github.event.pull_request.labels.*.name, 'enhancement') && !contains(github.event.pull_request.labels.*.name, 'bug') && !contains(github.event.pull_request.labels.*.name, 'ignore-for-release') }} - uses: actions/github-script@v3 - with: - script: | - core.setFailed('Missing required labels') diff --git a/.github/workflows/check_pr.yml b/.github/workflows/check_pr.yml new file mode 100644 index 0000000..57a5458 --- /dev/null +++ b/.github/workflows/check_pr.yml @@ -0,0 +1,226 @@ +name: Check PR + +# Controls when the workflow will run +on: + pull_request: + branches: + - main + types: [ opened, synchronize, labeled, unlabeled, reopened, edited ] + + +permissions: + pull-requests: write + + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + auto_assign: + name: Auto Assign + + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - name: Assign Me + # You may pin to the exact commit or the version. + uses: kentaro-m/auto-assign-action@v1.2.1 + with: + configuration-path: '.github/auto_assign.yml' + + check_labels: + name: Check Required Labels + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - name: Verify PR Labels + if: ${{ !contains(github.event.pull_request.labels.*.name, 'major') && !contains(github.event.pull_request.labels.*.name, 'minor') && !contains(github.event.pull_request.labels.*.name, 'patch') && !contains(github.event.pull_request.labels.*.name, 'skip') }} + uses: actions/github-script@v6.3.3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + var comments = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + for (const comment of comments.data) { + if (comment.body.includes('This pull request does not contain a valid label')){ + github.rest.issues.deleteComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: comment.id + }) + } + } + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'This pull request does not contain a valid label. Please add one of the following labels: `[patch, minor, major, skip]`' + }) + core.setFailed('Missing required labels') + + + check_format: + name: Check Format + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Formatting + id: format + continue-on-error: true + uses: axel-op/googlejavaformat-action@v3 + with: + args: "--set-exit-if-changed" + + - uses: actions/github-script@v6.3.3 + if: steps.format.outcome != 'success' + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + console.log(context); + var comments = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + for (const comment of comments.data) { + console.log(comment); + if (comment.body.includes('Comment this PR with')){ + github.rest.issues.deleteComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: comment.id + }) + } + } + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'Comment this PR with *update_code* to update `openapi.json` and format the code. Consider to use pre-commit to format the code.' + }) + core.setFailed('Format your code.') + + check_size: + runs-on: ubuntu-latest + name: Check Size + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Check Size + uses: actions/github-script@v6.3.3 + env: + IGNORED_FILES: openapi.json, openapi-node.json + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const additions = context.payload.pull_request.additions || 0 + const deletions = context.payload.pull_request.deletions || 0 + var changes = additions + deletions + console.log('additions: '+additions+' + deletions: '+deletions+ ' = total changes: ' + changes); + + const { IGNORED_FILES } = process.env + const ignored_files = IGNORED_FILES.trim().split(',').filter(word => word.length > 0); + if (ignored_files.length > 0){ + var ignored = 0 + const execSync = require('child_process').execSync; + for (const file of IGNORED_FILES.trim().split(',')) { + + const ignored_additions_str = execSync('git --no-pager diff --numstat origin/main..origin/${{ github.head_ref}} | grep ' + file + ' | cut -f 1', { encoding: 'utf-8' }) + const ignored_deletions_str = execSync('git --no-pager diff --numstat origin/main..origin/${{ github.head_ref}} | grep ' + file + ' | cut -f 2', { encoding: 'utf-8' }) + + const ignored_additions = ignored_additions_str.split('\n').map(elem=> parseInt(elem || 0)).reduce( + (accumulator, currentValue) => accumulator + currentValue, + 0); + const ignored_deletions = ignored_deletions_str.split('\n').map(elem=> parseInt(elem || 0)).reduce( + (accumulator, currentValue) => accumulator + currentValue, + 0); + + ignored += ignored_additions + ignored_deletions; + } + changes -= ignored + console.log('ignored lines: ' + ignored + ' , consider changes: ' + changes); + } + + if (changes < 200){ + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['size/small'] + }) + + + var labels = await github.rest.issues.listLabelsOnIssue({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + + if (labels.data.find(label => label.name == 'size/large')){ + github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: 'size/large' + }) + } + } + + if (changes > 400){ + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['size/large'] + }) + + var comments = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + for (const comment of comments.data) { + if (comment.body.includes('This PR exceeds the recommended size')){ + github.rest.issues.deleteComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: comment.id + }) + } + } + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'This PR exceeds the recommended size of 400 lines. Please make sure you are NOT addressing multiple issues with one PR. _Note this PR might be rejected due to its size._' + }) + + var labels = await github.rest.issues.listLabelsOnIssue({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + + if (labels.data.find(label => label.name == 'size/small')){ + github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: 'size/small' + }) + } + + } diff --git a/.github/workflows/code_review.yml b/.github/workflows/code_review.yml new file mode 100644 index 0000000..5622527 --- /dev/null +++ b/.github/workflows/code_review.yml @@ -0,0 +1,124 @@ +name: Code Review + +# Controls when the workflow will run +on: + pull_request: + branches: + - main + types: + - opened + - synchronize + - reopened + push: + branches: + - main + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +env: + PROJECT_KEY: pagopa_pagopa-receipt-pdf-helpdesk + +permissions: + id-token: write + contents: read + deployments: write + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + code-review: + name: Code Review + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - uses: actions/checkout@v2.3.4 + - name: Code Review + uses: ./.github/maven_code_review + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + sonar_token: ${{ secrets.SONAR_TOKEN }} + project_key: ${{env.PROJECT_KEY}} + jdk_version: 17 + maven_version: 3.9.3 + coverage_exclusions: "**/config/*,**/*Mock*,**/model/**,**/entity/*,**/producer/**,**/enumeration/**" + cpd_exclusions: "**/model/**,**/entity/*" + + smoke-test: + name: Smoke Test + runs-on: ubuntu-latest + environment: + name: dev + steps: + - name: Checkout + id: checkout + uses: actions/checkout@1f9a0c22da41e6ebfa534300ef656657ea2c6707 + + - name: Login + id: login + # from https://github.com/Azure/login/commits/master + uses: azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 + with: + client-id: ${{ secrets.CLIENT_ID }} + tenant-id: ${{ secrets.TENANT_ID }} + subscription-id: ${{ secrets.SUBSCRIPTION_ID }} + + - name: Run Service on Docker + shell: bash + id: run_service_docker + run: | + cd ./docker + chmod +x ./run_docker.sh + ./run_docker.sh local + + - name: Run Integration Tests + shell: bash + id: run_integration_test + run: | + export CUCUMBER_PUBLISH_TOKEN=${{ secrets.CUCUMBER_PUBLISH_TOKEN }} + export RECEIPTS_COSMOS_CONN_STRING='${{ secrets.RECEIPTS_COSMOS_CONN_STRING }}' + export BIZEVENTS_COSMOS_CONN_STRING='${{ secrets.BIZEVENTS_COSMOS_CONN_STRING }}' + export SUBKEY='${{ secrets.SUBKEY }}' + cd ./integration-test + chmod +x ./run_integration_test.sh + ./run_integration_test.sh local + + delete_github_deployments: + runs-on: ubuntu-latest + needs: smoke-test + if: ${{ always() }} + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJSON(github) }} + run: echo "$GITHUB_CONTEXT" + + - name: Delete Previous deployments + uses: actions/github-script@v6 + env: + SHA_HEAD: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.sha}} + with: + script: | + const { SHA_HEAD } = process.env + + const deployments = await github.rest.repos.listDeployments({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: SHA_HEAD + }); + await Promise.all( + deployments.data.map(async (deployment) => { + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id, + state: 'inactive' + }); + return github.rest.repos.deleteDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id + }); + }) + ); diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 4f4091b..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Auto Deploy - -# Controls when the workflow will run -on: - pull_request: - branches: - - main - types: [ closed ] - - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - build: - if: ${{ github.event.pull_request.merged }} - name: Call Azure Build Pipeline - # The type of runner that the job will run on - runs-on: ubuntu-latest - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - - # default skip bump versioning - - name: Set as default skip bump versioning - run: | - echo "SEMVER=skip" >> $GITHUB_ENV - - - name: Set major - run: | - echo "SEMVER=major" >> $GITHUB_ENV - if: ${{ contains(github.event.pull_request.labels.*.name, 'breaking-change') }} - - - name: Set minor - run: | - echo "SEMVER=minor" >> $GITHUB_ENV - if: ${{ contains(github.event.pull_request.labels.*.name, 'enhancement') }} - - - name: Set patch - run: | - echo "SEMVER=patch" >> $GITHUB_ENV - if: ${{ contains(github.event.pull_request.labels.*.name, 'bug') }} - - - name: Set skip - run: | - echo "SEMVER=skip" >> $GITHUB_ENV - if: ${{ contains(github.event.pull_request.labels.*.name, 'ignore-for-release') }} - - - name: Azure Pipelines Action - Jversion - uses: jacopocarlini/azure-pipelines@v1.3 - with: - azure-devops-project-url: https://dev.azure.com/pagopaspa/pagoPA-projects - azure-pipeline-name: 'pagopa-function-template.deploy' - azure-devops-token: ${{ secrets.AZURE_DEVOPS_TOKEN }} - azure-template-parameters: '{"ENV": "dev", "SEMVER": "${{env.SEMVER}}", "TEST": "true"}' - azure-pipeline-variables: '{"system.debug": "true"}' - diff --git a/.github/workflows/deploy_with_github_runner.yml b/.github/workflows/deploy_with_github_runner.yml new file mode 100644 index 0000000..5ac4385 --- /dev/null +++ b/.github/workflows/deploy_with_github_runner.yml @@ -0,0 +1,84 @@ +name: Deploy on AKS + +on: + workflow_call: + inputs: + environment: + required: true + description: The name of the environment where to deploy + type: string + target: + required: true + description: The environment target of the job + type: string + +env: + NAMESPACE: receipts + APP_NAME: pagopapagopareceiptpdfhelpdesk + +permissions: + id-token: write + contents: read + +jobs: + create_runner: + name: Create Runner + runs-on: ubuntu-22.04 + environment: + name: ${{ inputs.environment }} + if: ${{ inputs.target == inputs.environment || inputs.target == 'all' }} + outputs: + runner_name: ${{ steps.create_github_runner.outputs.runner_name }} + steps: + - name: Create GitHub Runner + id: create_github_runner + # from https://github.com/pagopa/eng-github-actions-iac-template/tree/main/azure/github-self-hosted-runner-azure-create-action + uses: pagopa/eng-github-actions-iac-template/azure/github-self-hosted-runner-azure-create-action@main + with: + client_id: ${{ secrets.CLIENT_ID }} + tenant_id: ${{ secrets.TENANT_ID }} + subscription_id: ${{ secrets.SUBSCRIPTION_ID }} + container_app_environment_name: ${{ vars.CONTAINER_APP_ENVIRONMENT_NAME }} + resource_group_name: ${{ vars.CONTAINER_APP_ENVIRONMENT_RESOURCE_GROUP_NAME }} # RG of the runner + pat_token: ${{ secrets.BOT_TOKEN_GITHUB }} + # self_hosted_runner_image_tag: "v1.4.1" + + deploy: + needs: [ create_runner ] + runs-on: [ self-hosted, "${{ needs.create_runner.outputs.runner_name }}" ] + if: ${{ inputs.target == inputs.environment || inputs.target == 'all' }} + name: Deploy on AKS + environment: ${{ inputs.environment }} + steps: + - name: Deploy + uses: pagopa/github-actions-template/aks-deploy@main + with: + branch: ${{ github.ref_name }} + client_id: ${{ secrets.CLIENT_ID }} + subscription_id: ${{ secrets.SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.TENANT_ID }} + env: ${{ inputs.environment }} + namespace: ${{ env.NAMESPACE }} + cluster_name: ${{ vars.CLUSTER_NAME }} + resource_group: ${{ vars.CLUSTER_RESOURCE_GROUP }} + app_name: ${{ env.APP_NAME }} + helm_upgrade_options: "--debug" + + cleanup_runner: + name: Cleanup Runner + needs: [ create_runner, deploy ] + if: ${{ success() || failure() && inputs.target == inputs.environment || inputs.target == 'all' }} + runs-on: ubuntu-22.04 + environment: ${{ inputs.environment }} + steps: + - name: Cleanup GitHub Runner + id: cleanup_github_runner + # from https://github.com/pagopa/eng-github-actions-iac-template/tree/main/azure/github-self-hosted-runner-azure-cleanup-action + uses: pagopa/eng-github-actions-iac-template/azure/github-self-hosted-runner-azure-cleanup-action@0ee2f58fd46d10ac7f00bce4304b98db3dbdbe9a + with: + client_id: ${{ secrets.CLIENT_ID }} + tenant_id: ${{ secrets.TENANT_ID }} + subscription_id: ${{ secrets.SUBSCRIPTION_ID }} + resource_group_name: ${{ vars.CONTAINER_APP_ENVIRONMENT_RESOURCE_GROUP_NAME }} + runner_name: ${{ needs.create_runner.outputs.runner_name }} + pat_token: ${{ secrets.BOT_TOKEN_GITHUB }} \ No newline at end of file diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml new file mode 100644 index 0000000..9939387 --- /dev/null +++ b/.github/workflows/integration_test.yml @@ -0,0 +1,151 @@ +name: Integration Tests + +on: + schedule: + - cron: '00 06 * * *' + + workflow_dispatch: + inputs: + environment: + required: true + type: choice + description: Select the Environment + options: + - dev + - uat + canary: + description: 'run the tests on canary version' + required: false + type: boolean + default: false + +permissions: + id-token: write + contents: read + deployments: write + + +jobs: + create_runner: + name: Create Runner + runs-on: ubuntu-22.04 + environment: + name: ${{(github.event.inputs == null && 'uat') || inputs.environment }} + outputs: + runner_name: ${{ steps.create_github_runner.outputs.runner_name }} + steps: + - name: Create GitHub Runner + id: create_github_runner + # from https://github.com/pagopa/eng-github-actions-iac-template/tree/main/azure/github-self-hosted-runner-azure-create-action + uses: pagopa/eng-github-actions-iac-template/azure/github-self-hosted-runner-azure-create-action@main + with: + client_id: ${{ secrets.CLIENT_ID }} + tenant_id: ${{ secrets.TENANT_ID }} + subscription_id: ${{ secrets.SUBSCRIPTION_ID }} + container_app_environment_name: ${{ vars.CONTAINER_APP_ENVIRONMENT_NAME }} + resource_group_name: ${{ vars.CONTAINER_APP_ENVIRONMENT_RESOURCE_GROUP_NAME }} # RG of the runner + pat_token: ${{ secrets.BOT_TOKEN_GITHUB }} + # self_hosted_runner_image_tag: "v1.6.0" + + integration_test: + needs: [ create_runner ] + name: Test ${{(github.event.inputs == null && 'uat') || inputs.environment }} + runs-on: [ self-hosted, "${{ needs.create_runner.outputs.runner_name }}" ] + environment: ${{(github.event.inputs == null && 'uat') || inputs.environment }} + steps: + + - name: Checkout + id: checkout + uses: actions/checkout@1f9a0c22da41e6ebfa534300ef656657ea2c6707 + + - name: Login + id: login + # from https://github.com/Azure/login/commits/master + uses: azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 + with: + client-id: ${{ secrets.CLIENT_ID }} + tenant-id: ${{ secrets.TENANT_ID }} + subscription-id: ${{ secrets.SUBSCRIPTION_ID }} + + - name: Run Integration Tests + shell: bash + run: | + export CUCUMBER_PUBLISH_TOKEN=${{ secrets.CUCUMBER_PUBLISH_TOKEN }} + export RECEIPTS_COSMOS_CONN_STRING='${{ secrets.RECEIPTS_COSMOS_CONN_STRING }}' + export BIZEVENTS_COSMOS_CONN_STRING='${{ secrets.BIZEVENTS_COSMOS_CONN_STRING }}' + export SUBKEY='${{ secrets.SUBKEY }}' + cd ./integration-test + chmod +x ./run_integration_test.sh + ./run_integration_test.sh ${{( github.event.inputs == null && 'uat') || inputs.environment }} + + notify: + needs: [ create_runner, integration_test ] + runs-on: [ self-hosted, "${{ needs.create_runner.outputs.runner_name }}" ] + name: Notify + if: always() + steps: + - name: Report Status + if: always() + uses: ravsamhq/notify-slack-action@v2 + with: + status: ${{ needs.integration_test.result }} + token: ${{ secrets.GITHUB_TOKEN }} + notify_when: 'failure,skipped' + notification_title: "<{run_url}|Scheduled Integration Test> has {status_message} in ${{( github.event.inputs == null && 'uat') || inputs.environment }} env" + message_format: '{emoji} <{run_url}|{workflow}> {status_message} in <{repo_url}|{repo}>' + footer: 'Linked to <{workflow_url}| workflow file>' + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + delete_github_deployments: + runs-on: ubuntu-latest + needs: integration_test + if: ${{ always() }} + steps: + - name: Delete Previous deployments + uses: actions/github-script@v6 + env: + SHA_HEAD: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.sha}} + with: + script: | + const { SHA_HEAD } = process.env + + const deployments = await github.rest.repos.listDeployments({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: SHA_HEAD + }); + await Promise.all( + deployments.data.map(async (deployment) => { + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id, + state: 'inactive' + }); + return github.rest.repos.deleteDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id + }); + }) + ); + + cleanup_runner: + name: Cleanup Runner + needs: [ create_runner, integration_test ] + if: ${{ always() }} + runs-on: ubuntu-22.04 + environment: ${{(github.event.inputs == null && 'uat') || inputs.environment }} + steps: + - name: Cleanup GitHub Runner + id: cleanup_github_runner + # from https://github.com/pagopa/eng-github-actions-iac-template/tree/main/azure/github-self-hosted-runner-azure-cleanup-action + uses: pagopa/eng-github-actions-iac-template/azure/github-self-hosted-runner-azure-cleanup-action@0ee2f58fd46d10ac7f00bce4304b98db3dbdbe9a + with: + client_id: ${{ secrets.CLIENT_ID }} + tenant_id: ${{ secrets.TENANT_ID }} + subscription_id: ${{ secrets.SUBSCRIPTION_ID }} + resource_group_name: ${{ vars.CONTAINER_APP_ENVIRONMENT_RESOURCE_GROUP_NAME }} + runner_name: ${{ needs.create_runner.outputs.runner_name }} + pat_token: ${{ secrets.BOT_TOKEN_GITHUB }} \ No newline at end of file diff --git a/.github/workflows/release-deploy.yml b/.github/workflows/release-deploy.yml new file mode 100644 index 0000000..8895a9a --- /dev/null +++ b/.github/workflows/release-deploy.yml @@ -0,0 +1,138 @@ +name: Release And Deploy + +# Controls when the workflow will run +on: + pull_request: + branches: + - main + types: [ closed ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + inputs: + environment: + required: true + type: choice + description: Select the Environment + options: + - dev + - uat + - prod + - all + semver: + required: true + type: choice + description: Select the new Semantic Version + options: + - major + - minor + - patch + - buildNumber + - skip + default: skip + beta: + required: false + type: boolean + description: deploy beta version on AKS + default: false + +permissions: + packages: write + contents: write + issues: write + id-token: write + actions: read + + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + setup: + name: Setup + runs-on: ubuntu-latest + outputs: + semver: ${{ steps.get_semver.outputs.semver }} + environment: ${{ steps.output.outputs.environment }} + steps: + - name: Get semver + id: get_semver + uses: pagopa/github-actions-template/semver-setup@v1.4.2 + + - if: ${{ github.event.inputs.environment == null || github.event.inputs.environment == 'dev' }} + run: echo "ENVIRNOMENT=dev" >> $GITHUB_ENV + + - if: ${{ github.event.inputs.environment == 'uat' }} + run: echo "ENVIRNOMENT=uat" >> $GITHUB_ENV + + - if: ${{ github.event.inputs.environment == 'prod' }} + run: echo "ENVIRNOMENT=prod" >> $GITHUB_ENV + + - if: ${{ github.event.inputs.environment == 'all' }} + run: echo "ENVIRNOMENT=all" >> $GITHUB_ENV + + - id: output + name: Set Output + run: | + echo "environment=${{env.ENVIRNOMENT}}" >> $GITHUB_OUTPUT + + + release: + name: Create a New Release + runs-on: ubuntu-latest + needs: [setup] + outputs: + version: ${{ steps.release.outputs.version }} + steps: + - name: Make Release + id: release + uses: pagopa/github-actions-template/maven-release@v1.5.4 + with: + semver: ${{ needs.setup.outputs.semver }} + github_token: ${{ secrets.BOT_TOKEN_GITHUB }} + beta: ${{ inputs.beta }} + skip_ci: ${{ inputs.beta }} + + image: + needs: [ setup, release ] + name: Build and Push Docker Image + runs-on: ubuntu-latest + if: ${{ inputs.semver != 'skip' }} + steps: + - name: Build and Push + id: semver + uses: pagopa/github-actions-template/ghcr-build-push@v1.5.4 + with: + branch: ${{ github.ref_name}} + github_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ needs.release.outputs.version }} + + deploy_aks: + name: Deploy on AKS + needs: [ setup, release, image ] + if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} + strategy: + matrix: + environment: [ dev, uat, prod ] + uses: ./.github/workflows/deploy_with_github_runner.yml + with: + environment: ${{ matrix.environment }} + target: ${{ needs.setup.outputs.environment }} + secrets: inherit + + notify: + needs: [ deploy_aks ] + runs-on: ubuntu-latest + name: Notify + if: always() + steps: + - name: Report Status + if: always() + uses: ravsamhq/notify-slack-action@v2 + with: + status: ${{ needs.deploy_aks.result }} + token: ${{ secrets.GITHUB_TOKEN }} + notify_when: 'failure,skipped' + notification_title: '{workflow} has {status_message}' + message_format: '{emoji} <{workflow_url}|{workflow}> {status_message} in <{repo_url}|{repo}>' + footer: 'Linked to Repo <{repo_url}|{repo}>' + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} \ No newline at end of file diff --git a/.github/workflows/sonar_analysis.yml b/.github/workflows/sonar_analysis.yml deleted file mode 100644 index 8003cbf..0000000 --- a/.github/workflows/sonar_analysis.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Sonar Analysis - -# Controls when the workflow will run -on: - push: - branches: - - main - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - build: - name: Call Azure Build Pipeline - # The type of runner that the job will run on - runs-on: ubuntu-latest - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - - name: Azure Pipelines Action - Jversion - uses: jacopocarlini/azure-pipelines@v1.3 - with: - azure-devops-project-url: https://dev.azure.com/pagopaspa/pagoPA-projects - azure-pipeline-name: 'pagopa-function-templat.code-review' - azure-devops-token: ${{ secrets.AZURE_DEVOPS_TOKEN }} - azure-pipeline-variables: '{"system.debug": "true"}' - diff --git a/.gitignore b/.gitignore index ab23ace..d6fe466 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ hs_err_pid* local.settings.json bin/ obj/ +**/.identity \ No newline at end of file diff --git a/.identity/.terraform.lock.hcl b/.identity/.terraform.lock.hcl new file mode 100644 index 0000000..b4c1819 --- /dev/null +++ b/.identity/.terraform.lock.hcl @@ -0,0 +1,89 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/azuread" { + version = "2.30.0" + constraints = "2.30.0" + hashes = [ + "h1:MimDtBEnmdMwbriZQzga/kCjDZ1G0+QLVQjrYdBEpdc=", + "h1:Uw4TcmJBEJ71h+oCwwidlkk5jFpyFRDPAFCMs/bT/cw=", + "h1:WnSPiREAFwnBUKREokMdHQ8Cjs47MzvS9pG8VS1ktec=", + "h1:xzNKb+lWPsBTxJiaAJ8ECZnY+D6QNM9tA1qpEncIba0=", + "zh:1c3e89cf19118fc07d7b04257251fc9897e722c16e0a0df7b07fcd261f8c12e7", + "zh:2e62c193030e04ebb10cc0526119cf69824bf2d7e4ea5a2f45bd5d5fb7221d36", + "zh:2f3c7a35257332d68b778cefc5201a5f044e4914dd03794a4da662ddfe756483", + "zh:35d0d3a1b58fdb8b8c4462d6b7e7016042da43ea9cc734ce897f52a73407d9b0", + "zh:47ede0cd0206ec953d40bf4a80aa6e59af64e26cbbd877614ac424533dbb693b", + "zh:48c190307d4d42ea67c9b8cc544025024753f46cef6ea64db84735e7055a72da", + "zh:6fff9b2c6a962252a70a15b400147789ab369b35a781e9d21cce3804b04d29af", + "zh:7646980cf3438bff29c91ffedb74458febbb00a996638751fbd204ab1c628c9b", + "zh:77aa2fa7ca6d5446afa71d4ff83cb87b70a2f3b72110fc442c339e8e710b2928", + "zh:e20b2b2c37175b89dd0db058a096544d448032e28e3b56e2db368343533a9684", + "zh:eab175b1dfe9865ad9404dccb6d5542899f8c435095aa7c679314b811c717ce7", + "zh:efc862bd78c55d2ff089729e2a34c1831ab4b0644fc11b36ee4ebed00a4797ba", + ] +} + +provider "registry.terraform.io/hashicorp/azurerm" { + version = "3.45.0" + constraints = "3.45.0" + hashes = [ + "h1:VQWxV5+qelZeUCjpdLvZ7iAom4RvG+fVVgK6ELvw/cs=", + "h1:gQLNY1I5e9kcle1p/VYEWb0eteQ/t5kUfnqVu2/GBNY=", + "zh:04c5dbb8845366ce5eb0dc2d55e151270cc2c0ace20993867fdae9af43b953ad", + "zh:2589585da615ccae341400d45d672ee3fae413fdd88449b5befeff12a85a44b2", + "zh:603869ed98fff5d9bf841a51afd9e06b628533c59356c8433aef4b15df63f5f7", + "zh:853fecab9c987b6772c8d9aa10362675f6c626b60ebc7118aa33ce91366fcc38", + "zh:979848c45e8e058862c36ba3a661457f7c81ef26ebb6634f479600de9c203d65", + "zh:9b512c8588ecc9c1b803b746a3a8517422561a918f0dfb0faaa707ed53ef1760", + "zh:a9601ffb58043426bcff1220662d6d137f0b2857a24f2dcf180aeac2c9cea688", + "zh:d52d2652328f0ed3ba202561d88cb9f43c174edbfaab1abf69f772125dbfe15e", + "zh:d92d91ca597c47f575bf3ae129f4b723be9b7dcb71b906ec6ec740fac29b1aaa", + "zh:ded73b730e4197b70fda9e83447c119f92f75dc37be3ff2ed45730c8f0348c28", + "zh:ec37ac332d50f8ca5827f97198346b0f8ecbf470e2e3ba1e027bb389d826b902", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.1" + hashes = [ + "h1:tSj1mL6OQ8ILGqR2mDu7OYYYWf+hoir0pf9KAQ8IzO8=", + "h1:ydA0/SNRVB1o95btfshvYsmxA+jZFRZcvKzZSB+4S1M=", + "zh:58ed64389620cc7b82f01332e27723856422820cfd302e304b5f6c3436fb9840", + "zh:62a5cc82c3b2ddef7ef3a6f2fedb7b9b3deff4ab7b414938b08e51d6e8be87cb", + "zh:63cff4de03af983175a7e37e52d4bd89d990be256b16b5c7f919aff5ad485aa5", + "zh:74cb22c6700e48486b7cabefa10b33b801dfcab56f1a6ac9b6624531f3d36ea3", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:79e553aff77f1cfa9012a2218b8238dd672ea5e1b2924775ac9ac24d2a75c238", + "zh:a1e06ddda0b5ac48f7e7c7d59e1ab5a4073bbcf876c73c0299e4610ed53859dc", + "zh:c37a97090f1a82222925d45d84483b2aa702ef7ab66532af6cbcfb567818b970", + "zh:e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2", + "zh:e80a746921946d8b6761e77305b752ad188da60688cfd2059322875d363be5f5", + "zh:fbdb892d9822ed0e4cb60f2fedbdbb556e4da0d88d3b942ae963ed6ff091e48f", + "zh:fca01a623d90d0cad0843102f9b8b9fe0d3ff8244593bd817f126582b52dd694", + ] +} + +provider "registry.terraform.io/integrations/github" { + version = "5.18.3" + constraints = "5.18.3" + hashes = [ + "h1:WbZvLB2qXKVoh4BvOOwFfEds+SZQrkINfSAWPnWFxGo=", + "h1:rv3mwpUeJ0n13sY+KZMI25WAVCSeipX4n8JMWKD1XcE=", + "zh:050b37d96628cb7451137755929ca8d21ea546bc46d11a715652584070e83ff2", + "zh:053051061f1b7f7673b0ceffac1f239ba28b0e5b375999206fd39976e85d9f2b", + "zh:0c300a977ca66d0347ed62bb116fd8fc9abb376a554d4c192d14f3ea71c83500", + "zh:1d5a1a5243eba78819d2f92ff2d504ebf9a9008a6670fb5f5660f44eb6a156d8", + "zh:a13ac15d251ebf4e7dc40acb0e40df066f443f4c7799186a29e2e44addc7d8e7", + "zh:a316d94b885953c036ebc9fba64a23da93974746bc3ac9d207462a6f02d44540", + "zh:a658a00373bff5979cc227052c693cbde8ca4c8f9fef1bc8094a3516f2e2a96d", + "zh:a7bfc6ad8465d5dc11b6f19d6805364de87fffe27622bb4f37da2319bb1c4956", + "zh:d7379a76861f1a6bfc36eca7a20f1f477711247563b105744d69d7bd1f365fad", + "zh:de1cd959fd4821248e8d21570601193408648474e74f49597f1d0c43185a4ab7", + "zh:e0b281240dd6f2aa405b2d6fe329bc15ab877161affe163fb150d1efca2fccdb", + "zh:e372c171358757a983d7aa878abfd05a84484fb4d22167e45c9c1267e78ed060", + "zh:f6d3116526030b3f6905f530cd6c04b23d42890d973fa2abe10ce9c89cb1db80", + "zh:f99eec731e03cc6a28996c875bd435887cd7ea75ec07cc77b9e768bb12da2227", + ] +} diff --git a/.identity/00_data.tf b/.identity/00_data.tf new file mode 100644 index 0000000..a45b379 --- /dev/null +++ b/.identity/00_data.tf @@ -0,0 +1,65 @@ +data "azurerm_resource_group" "dashboards" { + name = "dashboards" +} + +data "azurerm_kubernetes_cluster" "aks" { + name = local.aks_cluster.name + resource_group_name = local.aks_cluster.resource_group_name +} + +data "github_organization_teams" "all" { + root_teams_only = true + summary_only = true +} + +data "azurerm_key_vault" "key_vault_domain" { + + name = "pagopa-${var.env_short}-${local.domain}-kv" + resource_group_name = "pagopa-${var.env_short}-${local.domain}-sec-rg" +} + +data "azurerm_key_vault" "key_vault" { + + name = "pagopa-${var.env_short}-kv" + resource_group_name = "pagopa-${var.env_short}-sec-rg" +} + +data "azurerm_key_vault_secret" "key_vault_sonar" { + + name = "sonar-token" + key_vault_id = data.azurerm_key_vault.key_vault.id +} + +data "azurerm_key_vault_secret" "key_vault_bot_token" { + + name = "bot-token-github" + key_vault_id = data.azurerm_key_vault.key_vault.id +} + +data "azurerm_key_vault_secret" "key_vault_cucumber_token" { + + name = "cucumber-token" + key_vault_id = data.azurerm_key_vault.key_vault.id +} + +data "azurerm_cosmosdb_account" "receipts_cosmos" { + name = "pagopa-${var.env_short}-${local.location_short}-receipts-ds-cosmos-account" + resource_group_name = "pagopa-${var.env_short}-${local.location_short}-receipts-rg" +} + +data "azurerm_cosmosdb_account" "bizevents_cosmos" { + name = "pagopa-${var.env_short}-${local.location_short}-bizevents-ds-cosmos-account" + resource_group_name = "pagopa-${var.env_short}-${local.location_short}-bizevents-rg" +} + +data "azurerm_key_vault_secret" "key_vault_integration_test_webhook_slack" { + name = "webhook-slack" + key_vault_id = data.azurerm_key_vault.key_vault_domain.id +} + +data "azurerm_storage_account" "receipts_sa" { + name = "pagopa${var.env_short}${local.location_short}receiptsfnsa" + resource_group_name = "pagopa-${var.env_short}-${local.location_short}-receipts-st-rg" +} + + diff --git a/.identity/02_application_action.tf b/.identity/02_application_action.tf new file mode 100644 index 0000000..9478c6c --- /dev/null +++ b/.identity/02_application_action.tf @@ -0,0 +1,108 @@ +module "github_runner_app" { + source = "git::https://github.com/pagopa/github-actions-tf-modules.git//app-github-runner-creator?ref=main" + + app_name = local.app_name + + subscription_id = data.azurerm_subscription.current.id + + github_org = local.github.org + github_repository = local.github.repository + github_environment_name = var.env + + container_app_github_runner_env_rg = local.container_app_environment.resource_group +} + +resource "null_resource" "github_runner_app_permissions_to_namespace" { + triggers = { + aks_id = data.azurerm_kubernetes_cluster.aks.id + service_principal_id = module.github_runner_app.client_id + namespace = local.domain + version = "v2" + } + + provisioner "local-exec" { + command = < /dev/null; then + if [ "$ACTION" = "init" ]; then + echo "[INFO] init tf on ENV: ${ENV}" + terraform "$ACTION" -backend-config="${BACKEND_CONFIG_PATH}" $other + elif [ "$ACTION" = "output" ] || [ "$ACTION" = "state" ] || [ "$ACTION" = "taint" ]; then + # init terraform backend + terraform init -reconfigure -backend-config="${BACKEND_CONFIG_PATH}" + terraform "$ACTION" $other + else + # init terraform backend + echo "[INFO] init tf on ENV: ${ENV}" + terraform init -reconfigure -backend-config="${BACKEND_CONFIG_PATH}" + + echo "[INFO] run tf with: ${ACTION} on ENV: ${ENV} and other: >${other}<" + terraform "${ACTION}" -var-file="./env/${ENV}/terraform.tfvars" -compact-warnings $other + fi +else + echo "[ERROR] ACTION not allowed." + exit 1 +fi diff --git a/.java-version b/.java-version index b4de394..98d9bcb 100644 --- a/.java-version +++ b/.java-version @@ -1 +1 @@ -11 +17 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0d2e1f8..790419f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,9 +3,23 @@ # 3. set GITGUARDIAN_API_KEY in your develop environment (get an api key here: https://dashboard.gitguardian.com/workspace/230910/settings/personal/personal-access-tokens) # more info https://docs.gitguardian.com/internal-repositories-monitoring/integrations/git_hooks/pre_commit repos: - - repo: https://github.com/gitguardian/ggshield - rev: v1.11.0 + # - repo: https://github.com/gitguardian/ggshield + # rev: v1.11.0 + # hooks: + # - id: ggshield + # language_version: python3 + # stages: [ commit ] + - repo: https://github.com/ejba/pre-commit-maven + rev: v0.3.3 hooks: - - id: ggshield - language_version: python3 - stages: [ commit ] + - id: maven + args: [ 'clean compile', "spotless:apply" ] + - id: maven-spotless-apply + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + - repo: https://github.com/gitleaks/gitleaks + rev: v8.16.1 + hooks: + - id: gitleaks diff --git a/Dockerfile b/Dockerfile index dd42d12..201c851 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,33 @@ -ARG JAVA_VERSION=11 +ARG JAVA_VERSION=17 # This image additionally contains function core tools – useful when using custom extensions -FROM mcr.microsoft.com/azure-functions/java:3.0-java$JAVA_VERSION-build AS installer-env +FROM mcr.microsoft.com/azure-functions/java:4-java$JAVA_VERSION-build AS installer-env COPY . /src/java-function-app +RUN echo $(ls -1 /src/java-function-app) +RUN chmod 777 /src/java-function-app/agent/config.yaml RUN cd /src/java-function-app && \ + wget https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/0.19.0/jmx_prometheus_javaagent-0.19.0.jar && \ + curl -o 'opentelemetry-javaagent.jar' -L 'https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v1.25.1/opentelemetry-javaagent.jar' && \ mkdir -p /home/site/wwwroot && \ mvn clean package -Dmaven.test.skip=true && \ cd ./target/azure-functions/ && \ cd $(ls -d */|head -n 1) && \ - cp -a . /home/site/wwwroot + cp -a . /home/site/wwwroot && \ + cp /src/java-function-app/agent/config.yaml /home/site/wwwroot/config.yaml +RUN chmod 777 /src/java-function-app/jmx_prometheus_javaagent-0.19.0.jar && \ + cp /src/java-function-app/jmx_prometheus_javaagent-0.19.0.jar /home/site/wwwroot/jmx_prometheus_javaagent-0.19.0.jar + +RUN chmod 777 /src/java-function-app/opentelemetry-javaagent.jar && \ + cp /src/java-function-app/opentelemetry-javaagent.jar /home/site/wwwroot/opentelemetry-javaagent.jar # This image is ssh enabled #FROM mcr.microsoft.com/azure-functions/java:3.0-java$JAVA_VERSION-appservice # This image isn't ssh enabled -FROM mcr.microsoft.com/azure-functions/java:3.0-java$JAVA_VERSION +FROM mcr.microsoft.com/azure-functions/java:4-java$JAVA_VERSION ENV AzureWebJobsScriptRoot=/home/site/wwwroot \ AzureFunctionsJobHost__Logging__Console__IsEnabled=true EXPOSE 80 +EXPOSE 12345 COPY --from=installer-env ["/home/site/wwwroot", "/home/site/wwwroot"] \ No newline at end of file diff --git a/agent/config.yaml b/agent/config.yaml new file mode 100644 index 0000000..ef6a288 --- /dev/null +++ b/agent/config.yaml @@ -0,0 +1,3 @@ +excludeObjectNames: ["io.opentelemetry:*"] +rules: + - pattern: ".*" \ No newline at end of file diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..688cbb5 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,11 @@ +# Docker Environment 🐳 +`run_docker.sh` is a script to launch the image of this microservice and all the dependencies on Docker. + +## How to use 💻 +You can use `local`, `dev`, `uat` or `prod` images + +`sh ./run_docker.sh ` + +--- + +ℹ️ _Note_: If you run the script without the parameter, `local` is used as default. diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..df36588 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3.8' + +services: + app: + container_name: 'receipt-pdf-helpdesk' + image: ${image} + platform: linux/amd64 + build: + dockerfile: Dockerfile + context: ../ + env_file: + - ./.env + ports: + - "60486:80" diff --git a/docker/run_docker.sh b/docker/run_docker.sh new file mode 100644 index 0000000..2c48890 --- /dev/null +++ b/docker/run_docker.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# sh ./run_docker.sh + +ENV=$1 + +if [ -z "$ENV" ] +then + ENV="local" + echo "No environment specified: local is used." +fi + +pip3 install yq + +if [ "$ENV" = "local" ]; then + image="service-local:latest" + ENV="dev" +else + repository=$(yq -r '."microservice-chart".image.repository' ../helm/values-$ENV.yaml) + image="${repository}:latest" +fi +export image=${image} + +FILE=.env +if test -f "$FILE"; then + rm .env +fi +config=$(yq -r '."microservice-chart".envConfig' ../helm/values-$ENV.yaml) +for line in $(echo $config | jq -r '. | to_entries[] | select(.key) | "\(.key)=\(.value)"'); do + echo $line >> .env +done + +keyvault=$(yq -r '."microservice-chart".keyvault.name' ../helm/values-$ENV.yaml) +secret=$(yq -r '."microservice-chart".envSecret' ../helm/values-$ENV.yaml) +for line in $(echo $secret | jq -r '. | to_entries[] | select(.key) | "\(.key)=\(.value)"'); do + IFS='=' read -r -a array <<< "$line" + response=$(az keyvault secret show --vault-name $keyvault --name "${array[1]}") + value=$(echo $response | jq -r '.value') + echo "${array[0]}=$value" >> .env +# if [ "${array[0]}" = "AFM_SA_CONNECTION_STRING" ];then +# echo "Set secret env ${array[0]}" +# echo "::add-mask::$value" +# echo AFM_SA_CONNECTION_STRING=$value >> $GITHUB_ENV +# fi +done + + +stack_name=$(cd .. && basename "$PWD") +docker compose -p "${stack_name}" up -d --remove-orphans --force-recreate --build +#docker build -t receipt-pdf-helpdesk ../ +# docker run -d -p 60486:80 --name="${stack_name}" receipt-pdf-helpdesk + +# waiting the containers +printf 'Waiting for the service' +attempt_counter=0 +max_attempts=50 +until [ $(curl -s -o /dev/null -w "%{http_code}" http://localhost:60486/health) -eq 200 ]; do + if [ ${attempt_counter} -eq ${max_attempts} ];then + echo "Max attempts reached" + exit 1 + fi + + printf '.' + attempt_counter=$((attempt_counter+1)) + sleep 5 +done +echo 'Service Started' diff --git a/helm/Chart.yaml b/helm/Chart.yaml index b69df2e..03d7769 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -1,10 +1,10 @@ apiVersion: v2 -name: pagopa-functions-template +name: pagopareceiptpdfhelpdesk description: Microservice description type: application -version: 0.0.1 -appVersion: 0.0.1 +version: 0.89.0 +appVersion: 1.7.1 dependencies: - name: microservice-chart - version: 1.21.0 + version: 2.4.0 repository: "https://pagopa.github.io/aks-microservice-chart-blueprint" diff --git a/helm/values-dev.yaml b/helm/values-dev.yaml index a39b257..5c0b960 100644 --- a/helm/values-dev.yaml +++ b/helm/values-dev.yaml @@ -1,36 +1,47 @@ microservice-chart: - namespace: "" # TODO + namespace: "receipts" nameOverride: "" fullnameOverride: "" image: - repository: pagopadcommonacr.azurecr.io/pagopa # TODO - tag: "0.0.1" + repository: ghcr.io/pagopa/pagopa-receipt-pdf-helpdesk + tag: "1.7.1" pullPolicy: Always # https://github.com/Azure/azure-functions-host/blob/dev/src/WebJobs.Script.WebHost/Controllers/HostController.cs livenessProbe: httpGet: - path: /info + path: /health port: 80 initialDelaySeconds: 60 failureThreshold: 6 periodSeconds: 10 readinessProbe: httpGet: - path: /info + path: /health port: 80 initialDelaySeconds: 60 failureThreshold: 6 periodSeconds: 10 deployment: create: true - service: + serviceMonitor: create: true + endpoints: + - interval: 10s #jmx-exporter + targetPort: 12345 + path: /metrics + ports: + - 80 #http + - 12345 #jmx-exporter + service: type: ClusterIP - port: 80 + ports: + - 80 #http + - 12345 #jmx-exporter ingress: create: true - host: "weudev..internal.dev.platform.pagopa.it" # TODO - path: /pagopa--service/(.*) # TODO + host: "weudev.receipts.internal.dev.platform.pagopa.it" + path: /pagopa-receipt-pdf-helpdesk/(.*) + servicePort: 80 serviceAccount: create: false annotations: {} @@ -43,15 +54,15 @@ microservice-chart: allowPrivilegeEscalation: false resources: requests: - memory: "512Mi" - cpu: "0.25" + memory: "768Mi" + cpu: "300m" limits: - memory: "512Mi" - cpu: "0.25" + memory: "768Mi" + cpu: "300m" autoscaling: enable: true - minReplica: 3 - maxReplica: 10 + minReplica: 1 + maxReplica: 1 pollingInterval: 10 # seconds cooldownPeriod: 50 # seconds triggers: @@ -60,14 +71,79 @@ microservice-chart: # Required type: Utilization # Allowed types are 'Utilization' or 'AverageValue' value: "75" + - type: memory + metadata: + # Required + type: Utilization # Allowed types are 'Utilization' or 'AverageValue' + value: "70" + fileConfig: {} envConfig: - WEBSITE_SITE_NAME: "pagopa" # required to show cloud role name in application insights # TODO + ENV: "dev" + WEBSITE_SITE_NAME: "pagopareceiptpdfhelpdesk" # required to show cloud role name in application insights FUNCTIONS_WORKER_RUNTIME: "java" + RECEIPT_QUEUE_TOPIC: "pagopa-d-weu-receipts-queue-receipt-waiting-4-gen" + COSMOS_RECEIPT_SERVICE_ENDPOINT: "https://pagopa-d-weu-receipts-ds-cosmos-account.documents.azure.com:443/" + COSMOS_BIZ_EVENT_SERVICE_ENDPOINT: "https://pagopa-d-weu-bizevents-ds-cosmos-account.documents.azure.com:443/" + COSMOS_RECEIPT_DB_NAME: "db" + COSMOS_BIZ_EVENT_DB_NAME: "db" + COSMOS_RECEIPT_CONTAINER_NAME: "receipts" + COSMOS_BIZ_EVENT_CONTAINER_NAME: "biz-events" + PDV_TOKENIZER_BASE_PATH: "https://api.uat.tokenizer.pdv.pagopa.it/tokenizer/v1" + PDV_TOKENIZER_INITIAL_INTERVAL: "200" + PDV_TOKENIZER_MULTIPLIER: "2.0" + PDV_TOKENIZER_RANDOMIZATION_FACTOR: "0.6" + PDV_TOKENIZER_MAX_RETRIES: "3" + ENABLE_ECS_CONSOLE: "true" + CONSOLE_LOG_THRESHOLD: "DEBUG" + CONSOLE_LOG_PATTERN: "%d{HH:mm:ss.SSS}[%thread]%-5level%logger{36}-%msg%n" + CONSOLE_LOG_CHARSET: "UTF-8" + OTEL_RESOURCE_ATTRIBUTES: "service.name=pagopareceiptpdfhelpdeskotl,deployment.environment=dev" + OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.elastic-system.svc:4317" + OTEL_LOGS_EXPORTER: none + OTEL_TRACES_SAMPLER: "always_on" + MAX_DATE_DIFF_MILLIS: "360000" + AZURE_FUNCTIONS_MESH_JAVA_OPTS: "-javaagent:/home/site/wwwroot/jmx_prometheus_javaagent-0.19.0.jar=12345:/home/site/wwwroot/config.yaml -javaagent:/home/site/wwwroot/opentelemetry-javaagent.jar -Xmx768m -XX:+UseG1GC" + envFieldRef: + APP_NAME: "metadata.labels['app.kubernetes.io/instance']" + APP_VERSION: "metadata.labels['app.kubernetes.io/version']" envSecret: - APPLICATIONINSIGHTS_CONNECTION_STRING: 'ai-d-connection-string' # TODO set in kv + APPLICATIONINSIGHTS_CONNECTION_STRING: "ai-d-connection-string" + COSMOS_RECEIPTS_CONN_STRING: "cosmos-receipt-connection-string" + RECEIPT_QUEUE_CONN_STRING: "receipts-storage-account-connection-string" + COSMOS_BIZ_EVENT_CONN_STRING: "cosmos-biz-event-d-connection-string" + COSMOS_RECEIPT_KEY: "cosmos-receipt-pkey" + COSMOS_BIZ_EVENT_KEY: "cosmos-bizevent-pkey" + OTEL_EXPORTER_OTLP_HEADERS: 'elastic-otl-secret-token' + PDV_TOKENIZER_SUBSCRIPTION_KEY: "tokenizer-api-key" keyvault: - name: "pagopa-d--kv" # TODO + name: "pagopa-d-receipts-kv" tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d" nodeSelector: {} - tolerations: [] - affinity: {} + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node_type + operator: In + values: + - user + canaryDelivery: + create: false + ingress: + create: false + canary: + type: header + headerName: X-Canary + headerValue: canary + weightPercent: 0 + service: + create: false + deployment: + create: false + image: + repository: ghcr.io/pagopa/pagopa-receipt-pdf-helpdesk + tag: "1.0.4" + pullPolicy: Always + envConfig: + envSecret: diff --git a/helm/values-prod.yaml b/helm/values-prod.yaml index 83842f0..32d3d1d 100644 --- a/helm/values-prod.yaml +++ b/helm/values-prod.yaml @@ -1,36 +1,47 @@ microservice-chart: - namespace: "" # TODO + namespace: "receipts" nameOverride: "" fullnameOverride: "" image: - repository: pagopapcommonacr.azurecr.io/pagopa # TODO - tag: "0.0.1" + repository: ghcr.io/pagopa/pagopa-receipt-pdf-helpdesk + tag: "1.7.1" pullPolicy: Always # https://github.com/Azure/azure-functions-host/blob/dev/src/WebJobs.Script.WebHost/Controllers/HostController.cs livenessProbe: httpGet: - path: /info + path: /health port: 80 initialDelaySeconds: 60 failureThreshold: 6 periodSeconds: 10 readinessProbe: httpGet: - path: /info + path: /health port: 80 initialDelaySeconds: 60 failureThreshold: 6 periodSeconds: 10 deployment: create: true - service: + serviceMonitor: create: true + endpoints: + - interval: 10s #jmx-exporter + targetPort: 12345 + path: /metrics + ports: + - 80 #http + - 12345 #jmx-exporter + service: type: ClusterIP - port: 80 + ports: + - 80 #http + - 12345 #jmx-exporter ingress: create: true - host: "weuprod..internal.platform.pagopa.it" # TODO - path: /pagopa--service/(.*) # TODO + host: "weuprod.receipts.internal.platform.pagopa.it" + path: /pagopa-receipt-pdf-helpdesk/(.*) + servicePort: 80 serviceAccount: create: false annotations: {} @@ -43,15 +54,15 @@ microservice-chart: allowPrivilegeEscalation: false resources: requests: - memory: "512Mi" - cpu: "0.25" + memory: "768Mi" + cpu: "300m" limits: - memory: "512Mi" - cpu: "0.25" + memory: "768Mi" + cpu: "500m" autoscaling: enable: true - minReplica: 3 - maxReplica: 10 + minReplica: 2 + maxReplica: 3 pollingInterval: 10 # seconds cooldownPeriod: 50 # seconds triggers: @@ -60,14 +71,79 @@ microservice-chart: # Required type: Utilization # Allowed types are 'Utilization' or 'AverageValue' value: "75" + - type: memory + metadata: + # Required + type: Utilization # Allowed types are 'Utilization' or 'AverageValue' + value: "70" + fileConfig: {} envConfig: - WEBSITE_SITE_NAME: "pagopa" # required to show cloud role name in application insights # TODO + ENV: "prod" + WEBSITE_SITE_NAME: "pagopareceiptpdfhelpdesk" # required to show cloud role name in application insights FUNCTIONS_WORKER_RUNTIME: "java" + RECEIPT_QUEUE_TOPIC: "pagopa-p-weu-receipts-queue-receipt-waiting-4-gen" + COSMOS_RECEIPT_SERVICE_ENDPOINT: "https://pagopa-p-weu-receipts-ds-cosmos-account.documents.azure.com:443/" + COSMOS_BIZ_EVENT_SERVICE_ENDPOINT: "https://pagopa-p-weu-bizevents-ds-cosmos-account.documents.azure.com:443/" + COSMOS_RECEIPT_DB_NAME: "db" + COSMOS_BIZ_EVENT_DB_NAME: "db" + COSMOS_RECEIPT_CONTAINER_NAME: "receipts" + COSMOS_BIZ_EVENT_CONTAINER_NAME: "biz-events" + PDV_TOKENIZER_BASE_PATH: "https://api.tokenizer.pdv.pagopa.it/tokenizer/v1" + PDV_TOKENIZER_INITIAL_INTERVAL: "200" + PDV_TOKENIZER_MULTIPLIER: "2.0" + PDV_TOKENIZER_RANDOMIZATION_FACTOR: "0.6" + PDV_TOKENIZER_MAX_RETRIES: "3" + ENABLE_ECS_CONSOLE: "true" + CONSOLE_LOG_THRESHOLD: "DEBUG" + CONSOLE_LOG_PATTERN: "%d{HH:mm:ss.SSS}[%thread]%-5level%logger{36}-%msg%n" + CONSOLE_LOG_CHARSET: "UTF-8" + OTEL_RESOURCE_ATTRIBUTES: "service.name=pagopareceiptpdfhelpdeskotl,deployment.environment=prod" + OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.elastic-system.svc:4317" + OTEL_LOGS_EXPORTER: none + OTEL_TRACES_SAMPLER: "always_on" + MAX_DATE_DIFF_MILLIS: "360000" + AZURE_FUNCTIONS_MESH_JAVA_OPTS: "-javaagent:/home/site/wwwroot/jmx_prometheus_javaagent-0.19.0.jar=12345:/home/site/wwwroot/config.yaml -Xmx768m -XX:+UseG1GC" + envFieldRef: + APP_NAME: "metadata.labels['app.kubernetes.io/instance']" + APP_VERSION: "metadata.labels['app.kubernetes.io/version']" envSecret: - APPLICATIONINSIGHTS_CONNECTION_STRING: 'ai-d-connection-string' # TODO set in kv + APPLICATIONINSIGHTS_CONNECTION_STRING: "ai-p-connection-string" + COSMOS_RECEIPTS_CONN_STRING: "cosmos-receipt-connection-string" + RECEIPT_QUEUE_CONN_STRING: "receipts-storage-account-connection-string" + COSMOS_BIZ_EVENT_CONN_STRING: "cosmos-biz-event-p-connection-string" + COSMOS_RECEIPT_KEY: "cosmos-receipt-pkey" + COSMOS_BIZ_EVENT_KEY: "cosmos-bizevent-pkey" + OTEL_EXPORTER_OTLP_HEADERS: "elastic-otl-secret-token" + PDV_TOKENIZER_SUBSCRIPTION_KEY: "tokenizer-api-key" keyvault: - name: "pagopa-p--kv" # TODO + name: "pagopa-p-receipts-kv" tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d" nodeSelector: {} - tolerations: [] - affinity: {} + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node_type + operator: In + values: + - user + canaryDelivery: + create: false + ingress: + create: false + canary: + type: header + headerName: X-Canary + headerValue: canary + weightPercent: 0 + service: + create: false + deployment: + create: false + image: + repository: ghcr.io/pagopa/pagopa-receipt-pdf-helpdesk + tag: "1.0.4" + pullPolicy: Always + envConfig: + envSecret: diff --git a/helm/values-uat.yaml b/helm/values-uat.yaml index 1a380e5..9e9a5ce 100644 --- a/helm/values-uat.yaml +++ b/helm/values-uat.yaml @@ -1,36 +1,47 @@ microservice-chart: - namespace: "" # TODO + namespace: "receipts" nameOverride: "" fullnameOverride: "" image: - repository: pagopaucommonacr.azurecr.io/pagopa # TODO - tag: "0.0.1" + repository: ghcr.io/pagopa/pagopa-receipt-pdf-helpdesk + tag: "1.7.1" pullPolicy: Always # https://github.com/Azure/azure-functions-host/blob/dev/src/WebJobs.Script.WebHost/Controllers/HostController.cs livenessProbe: httpGet: - path: /info + path: /health port: 80 initialDelaySeconds: 60 failureThreshold: 6 periodSeconds: 10 readinessProbe: httpGet: - path: /info + path: /health port: 80 initialDelaySeconds: 60 failureThreshold: 6 periodSeconds: 10 deployment: create: true - service: + serviceMonitor: create: true + endpoints: + - interval: 10s #jmx-exporter + targetPort: 12345 + path: /metrics + ports: + - 80 #http + - 12345 #jmx-exporter + service: type: ClusterIP - port: 80 + ports: + - 80 #http + - 12345 #jmx-exporter ingress: create: true - host: "weuuat..internal.uat.platform.pagopa.it" # TODO - path: /pagopa--service/(.*) # TODO + host: "weuuat.receipts.internal.uat.platform.pagopa.it" + path: /pagopa-receipt-pdf-helpdesk/(.*) + servicePort: 80 serviceAccount: create: false annotations: {} @@ -43,15 +54,15 @@ microservice-chart: allowPrivilegeEscalation: false resources: requests: - memory: "512Mi" - cpu: "0.25" + memory: "768Mi" + cpu: "300m" limits: - memory: "512Mi" - cpu: "0.25" + memory: "768Mi" + cpu: "500m" autoscaling: enable: true - minReplica: 3 - maxReplica: 10 + minReplica: 1 + maxReplica: 1 pollingInterval: 10 # seconds cooldownPeriod: 50 # seconds triggers: @@ -60,14 +71,79 @@ microservice-chart: # Required type: Utilization # Allowed types are 'Utilization' or 'AverageValue' value: "75" + - type: memory + metadata: + # Required + type: Utilization # Allowed types are 'Utilization' or 'AverageValue' + value: "70" + fileConfig: {} envConfig: - WEBSITE_SITE_NAME: "pagopa" # required to show cloud role name in application insights # TODO + ENV: "uat" + WEBSITE_SITE_NAME: "pagopareceiptpdfhelpdesk" # required to show cloud role name in application insights FUNCTIONS_WORKER_RUNTIME: "java" + RECEIPT_QUEUE_TOPIC: "pagopa-u-weu-receipts-queue-receipt-waiting-4-gen" + COSMOS_RECEIPT_SERVICE_ENDPOINT: "https://pagopa-u-weu-receipts-ds-cosmos-account.documents.azure.com:443/" + COSMOS_BIZ_EVENT_SERVICE_ENDPOINT: "https://pagopa-u-weu-bizevents-ds-cosmos-account.documents.azure.com:443/" + COSMOS_RECEIPT_DB_NAME: "db" + COSMOS_BIZ_EVENT_DB_NAME: "db" + COSMOS_RECEIPT_CONTAINER_NAME: "receipts" + COSMOS_BIZ_EVENT_CONTAINER_NAME: "biz-events" + PDV_TOKENIZER_BASE_PATH: "https://api.uat.tokenizer.pdv.pagopa.it/tokenizer/v1" + PDV_TOKENIZER_INITIAL_INTERVAL: "200" + PDV_TOKENIZER_MULTIPLIER: "2.0" + PDV_TOKENIZER_RANDOMIZATION_FACTOR: "0.6" + PDV_TOKENIZER_MAX_RETRIES: "3" + ENABLE_ECS_CONSOLE: "true" + CONSOLE_LOG_THRESHOLD: "DEBUG" + CONSOLE_LOG_PATTERN: "%d{HH:mm:ss.SSS}[%thread]%-5level%logger{36}-%msg%n" + CONSOLE_LOG_CHARSET: "UTF-8" + OTEL_RESOURCE_ATTRIBUTES: "service.name=pagopareceiptpdfhelpdeskotl,deployment.environment=uat" + OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.elastic-system.svc:4317" + OTEL_LOGS_EXPORTER: none + OTEL_TRACES_SAMPLER: "always_on" + MAX_DATE_DIFF_MILLIS: "360000" + AZURE_FUNCTIONS_MESH_JAVA_OPTS: "-javaagent:/home/site/wwwroot/jmx_prometheus_javaagent-0.19.0.jar=12345:/home/site/wwwroot/config.yaml -Xmx768m -XX:+UseG1GC" + envFieldRef: + APP_NAME: "metadata.labels['app.kubernetes.io/instance']" + APP_VERSION: "metadata.labels['app.kubernetes.io/version']" envSecret: - APPLICATIONINSIGHTS_CONNECTION_STRING: 'ai-d-connection-string' # TODO set in kv + APPLICATIONINSIGHTS_CONNECTION_STRING: "ai-u-connection-string" + COSMOS_RECEIPTS_CONN_STRING: "cosmos-receipt-connection-string" + RECEIPT_QUEUE_CONN_STRING: "receipts-storage-account-connection-string" + COSMOS_BIZ_EVENT_CONN_STRING: "cosmos-biz-event-u-connection-string" + COSMOS_RECEIPT_KEY: "cosmos-receipt-pkey" + COSMOS_BIZ_EVENT_KEY: "cosmos-bizevent-pkey" + OTEL_EXPORTER_OTLP_HEADERS: "elastic-otl-secret-token" + PDV_TOKENIZER_SUBSCRIPTION_KEY: "tokenizer-api-key" keyvault: - name: "pagopa-u--kv" # TODO + name: "pagopa-u-receipts-kv" tenantId: "7788edaf-0346-4068-9d79-c868aed15b3d" nodeSelector: {} - tolerations: [] - affinity: {} + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node_type + operator: In + values: + - user + canaryDelivery: + create: false + ingress: + create: false + canary: + type: header + headerName: X-Canary + headerValue: canary + weightPercent: 0 + service: + create: false + deployment: + create: false + image: + repository: ghcr.io/pagopa/pagopa-receipt-pdf-helpdesk + tag: "1.0.4" + pullPolicy: Always + envConfig: + envSecret: diff --git a/host.json b/host.json index c627a64..ac6d1b1 100644 --- a/host.json +++ b/host.json @@ -7,6 +7,14 @@ "extensions": { "http": { "routePrefix": "" + }, + "queues": { + "maxPollingInterval": "00:00:02", + "visibilityTimeout": "00:00:30", + "batchSize": 8, + "maxDequeueCount": 5, + "newBatchThreshold": 4, + "messageEncoding": "none" } }, "logging": { @@ -19,8 +27,11 @@ }, "applicationInsights": { "samplingSettings": { - "isEnabled": false + "isEnabled": true, + "maxTelemetryItemsPerSecond": 5, + "includedTypes": "PageView;Trace;Dependency;Request", + "excludedTypes": "Exception;Event;CustomEvent" + } } - } -} + } } diff --git a/integration-test/run_integration_test.sh b/integration-test/run_integration_test.sh new file mode 100644 index 0000000..750b584 --- /dev/null +++ b/integration-test/run_integration_test.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# example: sh ./run_integration_test.sh +set -e + +# run integration tests +cd ./src || exit +yarn install +yarn test:"$1" \ No newline at end of file diff --git a/integration-test/src/README.md b/integration-test/src/README.md new file mode 100644 index 0000000..03de5b3 --- /dev/null +++ b/integration-test/src/README.md @@ -0,0 +1,17 @@ +# Integration Tests +👀 Integration tests are in `integration-test/src/` folder. See there for more information. + +## How run on Docker 🐳 + +To run the integration tests on docker, you can run from this directory the script: + + +``` shell +sh ./run_integration_test.sh +``` + +--- +💻 If you want to test your local branch, +``` shell +sh ./run_integration_test.sh local +``` \ No newline at end of file diff --git a/integration-test/src/config/.env.dev b/integration-test/src/config/.env.dev new file mode 100644 index 0000000..a446cb8 --- /dev/null +++ b/integration-test/src/config/.env.dev @@ -0,0 +1,9 @@ +BIZEVENTS_COSMOS_CONN_STRING= +BIZ_EVENT_COSMOS_DB_NAME=db +BIZ_EVENT_COSMOS_DB_CONTAINER_NAME=biz-events + +RECEIPTS_COSMOS_CONN_STRING= +RECEIPT_COSMOS_DB_NAME=db +RECEIPT_COSMOS_DB_CONTAINER_NAME=receipts + +HELPDESK_URL=https://api.dev.platform.pagopa.it/receipts/helpdesk/v1/recoverFailed \ No newline at end of file diff --git a/integration-test/src/config/.env.local b/integration-test/src/config/.env.local new file mode 100644 index 0000000..f725a6f --- /dev/null +++ b/integration-test/src/config/.env.local @@ -0,0 +1,7 @@ +BIZEVENTS_COSMOS_CONN_STRING= +BIZ_EVENT_COSMOS_DB_NAME=db +BIZ_EVENT_COSMOS_DB_CONTAINER_NAME=biz-events + +RECEIPTS_COSMOS_CONN_STRING= +RECEIPT_COSMOS_DB_NAME=db +RECEIPT_COSMOS_DB_CONTAINER_NAME=receipts \ No newline at end of file diff --git a/integration-test/src/config/.env.uat b/integration-test/src/config/.env.uat new file mode 100644 index 0000000..078e67f --- /dev/null +++ b/integration-test/src/config/.env.uat @@ -0,0 +1,9 @@ +BIZEVENTS_COSMOS_CONN_STRING= +BIZ_EVENT_COSMOS_DB_NAME=db +BIZ_EVENT_COSMOS_DB_CONTAINER_NAME=biz-events + +RECEIPTS_COSMOS_CONN_STRING= +RECEIPT_COSMOS_DB_NAME=db +RECEIPT_COSMOS_DB_CONTAINER_NAME=receipts + +HELPDESK_URL=https://api.uat.platform.pagopa.it/receipts/helpdesk/v1/recoverFailed \ No newline at end of file diff --git a/integration-test/src/config/properties.json b/integration-test/src/config/properties.json new file mode 100644 index 0000000..e69de29 diff --git a/integration-test/src/features/receipt_pdf_datastore.feature b/integration-test/src/features/receipt_pdf_datastore.feature new file mode 100644 index 0000000..1ca3212 --- /dev/null +++ b/integration-test/src/features/receipt_pdf_datastore.feature @@ -0,0 +1,17 @@ +Feature: All about payment events to recover managed by Azure functions receipt-pdf-helpdesk + + Scenario: a biz event stored on biz-events datastore is stored into receipts datastore + Given a random biz event with id "receipt-helpdesk-int-test-id-1" stored on biz-events datastore with status DONE + When biz event has been properly stored into receipt datastore after 10000 ms with eventId "receipt-helpdesk-int-test-id-1" + Then the receipts datastore returns the receipt + And the receipt has eventId "receipt-helpdesk-int-test-id-1" + + Given a random receipt with id "receipt-helpdesk-int-test-id-1" stored with status FAILED + When HTTP recovery request is called + Then response has a 200 Http status + And the receipt has not the status "helpdesk" after 10000 ms + + Given a random receipt with id "receipt-helpdesk-int-test-id-1" stored with status FAILED + When HTTP recovery request is called without eventId + Then response has a 200 Http status + And the receipt has not the status "FAILED" after 10000 ms \ No newline at end of file diff --git a/integration-test/src/package.json b/integration-test/src/package.json new file mode 100644 index 0000000..0a4bae1 --- /dev/null +++ b/integration-test/src/package.json @@ -0,0 +1,20 @@ +{ + "name": "pagopa-receipt-pdf-helpdesk", + "license": "MIT", + "version": "0.0.1", + "scripts": { + "test": "dotenv -e ./config/.env.local yarn cucumber", + "test:local": "dotenv -e ./config/.env.local yarn cucumber", + "test:dev": "dotenv -e ./config/.env.dev yarn cucumber", + "test:uat": "dotenv -e ./config/.env.uat yarn cucumber", + "cucumber": "npx cucumber-js --publish -r step_definitions" + }, + "devDependencies": { + "@azure/cosmos": "^3.17.3", + "@cucumber/cucumber": "^9.1.2", + "axios": "^0.27.2", + "dotenv": "^16.1.4", + "dotenv-cli": "^7.2.1", + "npx": "^10.2.2" + } +} diff --git a/integration-test/src/step_definitions/biz_events_datastore_client.js b/integration-test/src/step_definitions/biz_events_datastore_client.js new file mode 100644 index 0000000..b719488 --- /dev/null +++ b/integration-test/src/step_definitions/biz_events_datastore_client.js @@ -0,0 +1,41 @@ +const { CosmosClient } = require("@azure/cosmos"); +const { createEvent } = require("./common"); + +const cosmos_db_conn_string = process.env.BIZEVENTS_COSMOS_CONN_STRING; +const databaseId = process.env.BIZ_EVENT_COSMOS_DB_NAME; // es. db +const containerId = process.env.BIZ_EVENT_COSMOS_DB_CONTAINER_NAME; // es. biz-events + +const client = new CosmosClient(cosmos_db_conn_string); +const container = client.database(databaseId).container(containerId); + +async function getDocumentByIdFromBizEventsDatastore(id) { + return await container.items + .query({ + query: "SELECT * from c WHERE c.id=@id", + parameters: [{ name: "@id", value: id }] + }) + .fetchAll(); +} + +async function createDocumentInBizEventsDatastore(id) { + let event = createEvent(id); + try { + return await container.items.create(event); + } catch (err) { + console.log(err); + } +} + +async function deleteDocumentFromBizEventsDatastore(id) { + try { + return await container.item(id, id).delete(); + } catch (error) { + if (error.code !== 404) { + console.log(error) + } + } +} + +module.exports = { + getDocumentByIdFromBizEventsDatastore, createDocumentInBizEventsDatastore, deleteDocumentFromBizEventsDatastore +} \ No newline at end of file diff --git a/integration-test/src/step_definitions/common.js b/integration-test/src/step_definitions/common.js new file mode 100644 index 0000000..b60864a --- /dev/null +++ b/integration-test/src/step_definitions/common.js @@ -0,0 +1,166 @@ +const axios = require("axios"); + +const helpdesk_url = process.env.HELPDESK_URL; + +axios.defaults.headers.common['Ocp-Apim-Subscription-Key'] = process.env.SUBKEY || ""; // for all requests +if (process.env.canary) { + axios.defaults.headers.common['X-CANARY'] = 'canary' // for all requests +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function createEvent(id) { + let json_event = { + "id": id, + "version": "2", + "idPaymentManager": "54927408", + "complete": "false", + "receiptId": "9851395f09544a04b288202299193ca6", + "missingInfo": [ + "psp.pspPartitaIVA", + "paymentInfo.primaryCiIncurredFee", + "paymentInfo.idBundle", + "paymentInfo.idCiBundle" + ], + "debtorPosition": { + "modelType": "2", + "noticeNumber": "310391366991197059", + "iuv": "10391366991197059" + }, + "creditor": { + "idPA": "66666666666", + "idBrokerPA": "66666666666", + "idStation": "66666666666_08", + "companyName": "PA paolo", + "officeName": "office" + }, + "psp": { + "idPsp": "60000000001", + "idBrokerPsp": "60000000001", + "idChannel": "60000000001_08", + "psp": "PSP Paolo", + "pspFiscalCode": "CF60000000006", + "channelDescription": "app" + }, + "debtor": { + "fullName": "paGetPaymentName", + "entityUniqueIdentifierType": "G", + "entityUniqueIdentifierValue": "JHNDOE00A01F205N", + "streetName": "paGetPaymentStreet", + "civicNumber": "paGetPayment99", + "postalCode": "20155", + "city": "paGetPaymentCity", + "stateProvinceRegion": "paGetPaymentState", + "country": "IT", + "eMail": "paGetPayment@test.it" + }, + "payer": { + "fullName": "name", + "entityUniqueIdentifierType": "G", + "entityUniqueIdentifierValue": "JHNDOE00A01F205S", + "streetName": "street", + "civicNumber": "civic", + "postalCode": "postal", + "city": "city", + "stateProvinceRegion": "state", + "country": "IT", + "eMail": "prova@test.it" + }, + "paymentInfo": { + "paymentDateTime": "2023-03-17T16:37:36.955813", + "applicationDate": "2021-12-12", + "transferDate": "2021-12-11", + "dueDate": "2021-12-12", + "paymentToken": "9851395f09544a04b288202299193ca6", + "amount": "10.0", + "fee": "2.0", + "totalNotice": "1", + "paymentMethod": "creditCard", + "touchpoint": "app", + "remittanceInformation": "TARI 2021", + "description": "TARI 2021", + "metadata": [ + { + "key": "1", + "value": "22" + } + ] + }, + "transferList": [ + { + "idTransfer": "1", + "fiscalCodePA": "66666666666", + "companyName": "PA paolo", + "amount": "10.0", + "transferCategory": "paGetPaymentTest", + "remittanceInformation": "/RFB/00202200000217527/5.00/TXT/" + } + ], + "transactionDetails": { + "user": { + "fullName": "John Doe", + "type": "F", + "fiscalCode": "JHNDOE00A01F205N", + "notificationEmail": "john.doe@mail.it", + "userId": "1234", + "userStatus": "11", + "userStatusDescription": "REGISTERED_SPID" + }, + "transaction": { + "idTransaction": 123456, + "transactionId": 123456, + "grandTotal": 0, + "amount": 0, + "fee": 0 + } + }, + "timestamp": 1679067463501, + "properties": { + "diagnostic-id": "00-f70ef3167cffad76c6657a67a33ee0d2-61d794a75df0b43b-01", + "serviceIdentifier": "NDP002SIT" + }, + "eventStatus": "DONE", + "eventRetryEnrichmentCount": 0 + } + return json_event +} + +function createReceipt(id, fiscalCode, pdfName) { + let receipt = + { + "eventId": id, + "eventData": { + "debtorFiscalCode": fiscalCode, + "payerFiscalCode": fiscalCode + }, + "status": "IO_NOTIFIED", + "mdAttach": { + "name": pdfName, + "url": pdfName + }, + "id": id + } + return receipt +} +async function recoverFailedEvent(eventId) { + + var data = {} + if (eventId != null) { + data = JSON.stringify({ "eventId": eventId }); + } + + return await axios.put(helpdesk_url, data, {}) + .then(res => { + return res; + }) + .catch(error => { + return error.response; + }); + +} + +module.exports = { + createEvent, sleep, recoverFailedEvent +} \ No newline at end of file diff --git a/integration-test/src/step_definitions/receipt_pdf_datastore_step.js b/integration-test/src/step_definitions/receipt_pdf_datastore_step.js new file mode 100644 index 0000000..395edc8 --- /dev/null +++ b/integration-test/src/step_definitions/receipt_pdf_datastore_step.js @@ -0,0 +1,91 @@ +const assert = require('assert'); +const { After, Given, When, Then, setDefaultTimeout } = require('@cucumber/cucumber'); +const { sleep, recoverFailedEvent } = require("./common"); +const { createDocumentInBizEventsDatastore, deleteDocumentFromBizEventsDatastore } = require("./biz_events_datastore_client"); +const { getDocumentByIdFromReceiptsDatastore, deleteDocumentFromReceiptsDatastoreByEventId, deleteDocumentFromReceiptsDatastore, updateReceiptToFailed } = require("./receipts_datastore_client"); + +// set timeout for Hooks function, it allows to wait for long task +setDefaultTimeout(360 * 1000); + +// initialize variables +this.eventId = null; +this.responseToCheck = null; +this.response = null; +this.receiptId = null; +this.event = null; + +// After each Scenario +After(async function () { + // remove event + if (this.eventId != null) { + await deleteDocumentFromBizEventsDatastore(this.eventId); + } + if (this.eventId != null && this.receiptId != null) { + await deleteDocumentFromReceiptsDatastore(this.receiptId, this.eventId); + } + this.eventId = null; + this.responseToCheck = null; + this.receiptId = null; + this.event = null; +}); + +Given('a random biz event with id {string} stored on biz-events datastore with status DONE', async function (id) { + this.eventId = id; + // prior cancellation to avoid dirty cases + await deleteDocumentFromBizEventsDatastore(this.eventId); + await deleteDocumentFromReceiptsDatastoreByEventId(this.eventId); + + let bizEventStoreResponse = await createDocumentInBizEventsDatastore(this.eventId); + assert.strictEqual(bizEventStoreResponse.statusCode, 201); +}); + +When('biz event has been properly stored into receipt datastore after {int} ms with eventId {string}', async function (time, eventId) { + // boundary time spent by azure function to process event + await sleep(time); + this.responseToCheck = await getDocumentByIdFromReceiptsDatastore(eventId); +}); + +Then('the receipts datastore returns the receipt', async function () { + assert.notStrictEqual(this.responseToCheck.resources.length, 0); + this.receiptId = this.responseToCheck.resources[0].id; + assert.strictEqual(this.responseToCheck.resources.length, 1); +}); + +Then('the receipt has eventId {string}', function (targetId) { + assert.strictEqual(this.responseToCheck.resources[0].eventId, targetId); +}); + +Then('the receipt has not the status {string}', function (targetStatus) { + assert.notStrictEqual(this.responseToCheck.resources[0].status, targetStatus); +}); + +Given('a random receipt with id {string} stored with status FAILED', async function (id) { + this.eventId = id; + // prior cancellation to avoid dirty cases + document = await getDocumentByIdFromReceiptsDatastore(this.eventId); + await updateReceiptToFailed(document.resources[0].id, this.eventId); +}); + +When('HTTP recovery request is called', async function () { + // boundary time spent by azure function to process event + this.response = await recoverFailedEvent(this.eventId); +}); + +Then('the receipt has not the status {string} after {int} ms', async function (targetStatus, time) { + await sleep(time); + this.responseToCheck = await getDocumentByIdFromReceiptsDatastore(this.eventId); + assert.notStrictEqual(this.responseToCheck.resources[0].status, targetStatus); +}); + +When('HTTP recovery request is called without eventId', async function () { + this.response = await recoverFailedEvent(null); +}); + +Then('response has a {int} Http status', function (expectedStatus) { + assert.strictEqual(this.response.status, expectedStatus); +}); + + + + + diff --git a/integration-test/src/step_definitions/receipts_datastore_client.js b/integration-test/src/step_definitions/receipts_datastore_client.js new file mode 100644 index 0000000..314842a --- /dev/null +++ b/integration-test/src/step_definitions/receipts_datastore_client.js @@ -0,0 +1,56 @@ +const { CosmosClient } = require("@azure/cosmos"); +const { createReceipt } = require("./common"); + +const cosmos_db_conn_string = process.env.RECEIPTS_COSMOS_CONN_STRING || ""; +const databaseId = process.env.RECEIPT_COSMOS_DB_NAME; +const receiptContainerId = process.env.RECEIPT_COSMOS_DB_CONTAINER_NAME; + +const client = new CosmosClient(cosmos_db_conn_string); +const receiptContainer = client.database(databaseId).container(receiptContainerId); + +async function getDocumentByIdFromReceiptsDatastore(id) { + return await receiptContainer.items + .query({ + query: "SELECT * from c WHERE c.eventId=@eventId", + parameters: [{ name: "@eventId", value: id }] + }) + .fetchNext(); +} + +async function deleteDocumentFromReceiptsDatastoreByEventId(eventId){ + let documents = await getDocumentByIdFromReceiptsDatastore(eventId); + + documents?.resources?.forEach(el => { + deleteDocumentFromReceiptsDatastore(el.id, eventId); + }) +} + +async function deleteDocumentFromReceiptsDatastore(id, partitionKey) { + try { + return await receiptContainer.item(id, partitionKey).delete(); + } catch (error) { + if (error.code !== 404) { + console.log(error) + } + } +} + +async function updateReceiptToFailed(id, partitionKey) { + + const operations = + [ + { op: 'replace', path: '/status', value: 'FAILED' } + ]; + + try { + return await receiptContainer.item(id, partitionKey).patch(operations); + } catch (error) { + if (error.code !== 404) { + console.log(error) + } + } +} + +module.exports = { + getDocumentByIdFromReceiptsDatastore, deleteDocumentFromReceiptsDatastoreByEventId, deleteDocumentFromReceiptsDatastore, updateReceiptToFailed +} \ No newline at end of file diff --git a/openapi/openapi.json b/openapi/openapi.json new file mode 100644 index 0000000..996ec4f --- /dev/null +++ b/openapi/openapi.json @@ -0,0 +1,295 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Receipts Helpdesk", + "description": "Microservice for exposing REST APIs about receipts helpdesk.", + "termsOfService": "https://www.pagopa.gov.it/", + "version": "1.7.1" + }, + "servers": [ + { + "url": "http://localhost:8080", + "description": "Generated server url" + } + ], + "tags": [], + "paths": { + "/info": { + "get": { + "tags": [ + "Home" + ], + "summary": "health check", + "description": "Return OK if application is started", + "operationId": "healthCheck", + "responses": { + "200": { + "description": "OK", + "headers": { + "X-Request-Id": { + "description": "This header identifies the call", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AppInfo" + } + } + } + }, + "400": { + "description": "Bad Request", + "headers": { + "X-Request-Id": { + "description": "This header identifies the call", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + }, + "401": { + "description": "Unauthorized", + "headers": { + "X-Request-Id": { + "description": "This header identifies the call", + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "Forbidden", + "headers": { + "X-Request-Id": { + "description": "This header identifies the call", + "schema": { + "type": "string" + } + } + } + }, + "429": { + "description": "Too many requests", + "headers": { + "X-Request-Id": { + "description": "This header identifies the call", + "schema": { + "type": "string" + } + } + } + }, + "500": { + "description": "Service unavailable", + "headers": { + "X-Request-Id": { + "description": "This header identifies the call", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + } + }, + "security": [ + { + "ApiKey": [] + } + ] + }, + "parameters": [ + { + "name": "X-Request-Id", + "in": "header", + "description": "This header identifies the call, if not passed it is self-generated. This ID is returned in the response.", + "schema": { + "type": "string" + } + } + ] + }, + "/recoverFailed": { + "put": { + "tags": [ + "Receipts REST APIs" + ], + "summary": "Recover a receipt, or group of, in FAILED or INSERTED status", + "operationId": "recoverFailed", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReceiptFailedRecoveryRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Succesfull Calls.", + "headers": { + "X-Request-Id": { + "description": "This header identifies the call", + "schema": { + "type": "string" + } + } + } + }, + "401": { + "description": "Wrong or missing function key.", + "headers": { + "X-Request-Id": { + "description": "This header identifies the call", + "schema": { + "type": "string" + } + } + } + }, + "422": { + "description": "Unable to process the request.", + "headers": { + "X-Request-Id": { + "description": "This header identifies the call", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + }, + "429": { + "description": "Too many requests.", + "headers": { + "X-Request-Id": { + "description": "This header identifies the call", + "schema": { + "type": "string" + } + } + } + }, + "500": { + "description": "Service unavailable.", + "headers": { + "X-Request-Id": { + "description": "This header identifies the call", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemJson" + } + } + } + } + }, + "security": [ + { + "ApiKey": [] + } + ] + }, + "parameters": [ + { + "name": "X-Request-Id", + "in": "header", + "description": "This header identifies the call, if not passed it is self-generated. This ID is returned in the response.", + "schema": { + "type": "string" + } + } + ] + } + }, + "components": { + "schemas": { + "AppInfo": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "environment": { + "type": "string" + } + } + }, + "ReceiptFailedRecoveryRequest": { + "type": "object", + "properties": { + "eventId": { + "type": "string", + "description": "Id of the event to start recovering (optional)" + } + } + }, + "ProblemJson": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "A short, summary of the problem type. Written in english and readable for engineers (usually not suited for non technical stakeholders and not localized); example: Service Unavailable" + }, + "status": { + "maximum": 600, + "minimum": 100, + "type": "integer", + "description": "The HTTP status code generated by the origin server for this occurrence of the problem.", + "format": "int32", + "example": 200 + }, + "detail": { + "type": "string", + "description": "A human readable explanation specific to this occurrence of the problem.", + "example": "There was an error processing the request" + } + } + } + }, + "securitySchemes": { + "ApiKey": { + "type": "apiKey", + "description": "The API key to access this function app.", + "name": "Ocp-Apim-Subscription-Key", + "in": "header" + } + } + } +} diff --git a/pom.xml b/pom.xml index 1f35088..f9783c5 100644 --- a/pom.xml +++ b/pom.xml @@ -3,16 +3,16 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - it.gov.pagopa.project - example-function + it.gov.pagopa.receipt + receipt-pdf-helpdesk 0.0.1 jar - Azure Custom Fn + pagopa-receipt-pdf-helpdesk UTF-8 - 11 + 17 1.15.0 1.4.2 com.microsoft.azure-20220215182005862 @@ -20,7 +20,7 @@ - + org.modelmapper modelmapper @@ -31,9 +31,34 @@ azure-functions-java-library ${azure.functions.java.library.version} + + com.azure + azure-cosmos + 4.45.1 + + + com.azure + azure-storage-blob + 12.22.2 + + + + + org.apache.httpcomponents + httpclient + 4.5.14 + + + org.apache.httpcomponents + httpmime + 4.5.14 + + + + org.junit.jupiter @@ -42,16 +67,23 @@ test + + uk.org.webcompere + system-stubs-jupiter + 2.1.3 + test + + org.mockito mockito-core - 4.3.1 + 5.4.0 test org.mockito mockito-junit-jupiter - 4.3.1 + 5.4.0 test @@ -144,6 +176,50 @@ provided + + com.azure + azure-storage-queue + 12.17.1 + + + + com.google.api-client + google-api-client + 1.32.1 + + + + ch.qos.logback + logback-classic + 1.4.7 + + + + org.slf4j + slf4j-api + 2.0.9 + + + + co.elastic.logging + logback-ecs-encoder + 1.5.0 + + + + + + org.codehaus.janino + janino + 2.6.1 + + + + io.github.resilience4j + resilience4j-retry + 2.1.0 + + @@ -176,7 +252,7 @@ westus windows - 11 + 17 @@ -268,6 +344,13 @@ + + + + com.diffplug.spotless + spotless-maven-plugin + 2.9.0 + diff --git a/src/main/java/it/gov/pagopa/project/Example.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/Health.java similarity index 50% rename from src/main/java/it/gov/pagopa/project/Example.java rename to src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/Health.java index f063da4..59bfc65 100644 --- a/src/main/java/it/gov/pagopa/project/Example.java +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/Health.java @@ -1,41 +1,32 @@ -package it.gov.pagopa.project; +package it.gov.pagopa.receipt.pdf.helpdesk; import com.microsoft.azure.functions.*; import com.microsoft.azure.functions.annotation.AuthorizationLevel; import com.microsoft.azure.functions.annotation.FunctionName; import com.microsoft.azure.functions.annotation.HttpTrigger; -import javax.ws.rs.core.MediaType; -import java.time.LocalDateTime; import java.util.Optional; -import java.util.logging.Level; -import java.util.logging.Logger; + /** - * Azure Functions with Azure Queue trigger. + * Azure Functions with Azure Http trigger. */ -public class Example { +public class Health { /** * This function will be invoked when a Http Trigger occurs + * + * @return response with HttpStatus.OK */ - @FunctionName("ExampleFunction") + @FunctionName("Health") public HttpResponseMessage run ( - @HttpTrigger( - name = "ExampleTrigger", + @HttpTrigger(name = "HealthTrigger", methods = {HttpMethod.GET}, - route = "example", + route = "health", authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, final ExecutionContext context) { - Logger logger = context.getLogger(); - - String message = String.format("it.gov.pagopa.project.Example function called at: %s", LocalDateTime.now()); - logger.log(Level.INFO, () -> message); - return request.createResponseBuilder(HttpStatus.OK) - .header("Content-Type", MediaType.TEXT_PLAIN) - .body(message) .build(); } -} +} \ No newline at end of file diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/Info.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/Info.java new file mode 100644 index 0000000..31a4b96 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/Info.java @@ -0,0 +1,55 @@ +package it.gov.pagopa.receipt.pdf.helpdesk; + +import com.microsoft.azure.functions.*; +import com.microsoft.azure.functions.annotation.AuthorizationLevel; +import com.microsoft.azure.functions.annotation.FunctionName; +import com.microsoft.azure.functions.annotation.HttpTrigger; +import it.gov.pagopa.receipt.pdf.helpdesk.model.AppInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.util.Optional; +import java.util.Properties; + + +/** + * Azure Functions with Azure Http trigger. + */ +public class Info { + + private final Logger logger = LoggerFactory.getLogger(Info.class); + + /** + * This function will be invoked when a Http Trigger occurs + * + * @return response with HttpStatus.OK + */ + @FunctionName("Info") + public HttpResponseMessage run ( + @HttpTrigger(name = "InfoTrigger", + methods = {HttpMethod.GET}, + route = "info", + authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, + final ExecutionContext context) { + + return request.createResponseBuilder(HttpStatus.OK) + .body(getInfo()) + .build(); + } + public synchronized AppInfo getInfo() { + String version = null; + String name = null; + try (InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("application.properties")) { + Properties properties = new Properties(); + if (inputStream != null) { + properties.load(inputStream); + version = properties.getProperty("version", null); + name = properties.getProperty("name", null); + } + } catch (Exception e) { + logger.error("Impossible to retrieve information from pom.properties file.", e); + } + return AppInfo.builder().version(version).environment("azure-fn").name(name).build(); + } +} \ No newline at end of file diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/RecoverFailedReceipt.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/RecoverFailedReceipt.java new file mode 100644 index 0000000..cbbaa67 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/RecoverFailedReceipt.java @@ -0,0 +1,177 @@ +package it.gov.pagopa.receipt.pdf.helpdesk; + +import com.azure.cosmos.models.FeedResponse; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.microsoft.azure.functions.*; +import com.microsoft.azure.functions.annotation.AuthorizationLevel; +import com.microsoft.azure.functions.annotation.CosmosDBOutput; +import com.microsoft.azure.functions.annotation.FunctionName; +import com.microsoft.azure.functions.annotation.HttpTrigger; +import it.gov.pagopa.receipt.pdf.helpdesk.client.BizEventCosmosClient; +import it.gov.pagopa.receipt.pdf.helpdesk.client.ReceiptCosmosClient; +import it.gov.pagopa.receipt.pdf.helpdesk.client.impl.BizEventCosmosClientImpl; +import it.gov.pagopa.receipt.pdf.helpdesk.client.impl.ReceiptCosmosClientImpl; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.event.BizEvent; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.Receipt; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.enumeration.ReceiptStatusType; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.BizEventNotFoundException; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.PDVTokenizerException; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.ReceiptNotFoundException; +import it.gov.pagopa.receipt.pdf.helpdesk.model.ReceiptFailedRecoveryRequest; +import it.gov.pagopa.receipt.pdf.helpdesk.service.BizEventToReceiptService; +import it.gov.pagopa.receipt.pdf.helpdesk.service.impl.BizEventToReceiptServiceImpl; +import it.gov.pagopa.receipt.pdf.helpdesk.utils.BizEventToReceiptUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; + + +/** + * Azure Functions with Azure Http trigger. + */ +public class RecoverFailedReceipt { + + private final Logger logger = LoggerFactory.getLogger(RecoverFailedReceipt.class); + + private final BizEventToReceiptService bizEventToReceiptService; + private final BizEventCosmosClient bizEventCosmosClient; + private final ReceiptCosmosClient receiptCosmosClient; + + public RecoverFailedReceipt(){ + this.bizEventToReceiptService = new BizEventToReceiptServiceImpl(); + this.receiptCosmosClient = ReceiptCosmosClientImpl.getInstance(); + this.bizEventCosmosClient = BizEventCosmosClientImpl.getInstance(); + } + + RecoverFailedReceipt(BizEventToReceiptService bizEventToReceiptService, + BizEventCosmosClient bizEventCosmosClient, + ReceiptCosmosClient receiptCosmosClient){ + this.bizEventToReceiptService = bizEventToReceiptService; + this.bizEventCosmosClient = bizEventCosmosClient; + this.receiptCosmosClient = receiptCosmosClient; + } + + + /** + * This function will be invoked when a Http Trigger occurs + * + * @return response with HttpStatus.OK + */ + @FunctionName("RecoverFailedReceipt") + public HttpResponseMessage run ( + @HttpTrigger(name = "RecoverFailedReceiptTrigger", + methods = {HttpMethod.PUT}, + route = "recoverFailed", + authLevel = AuthorizationLevel.ANONYMOUS) + HttpRequestMessage> request, + @CosmosDBOutput( + name = "ReceiptDatastore", + databaseName = "db", + collectionName = "receipts", + connectionStringSetting = "COSMOS_RECEIPTS_CONN_STRING") + OutputBinding> documentdb, + final ExecutionContext context) { + + List receiptList = new ArrayList<>(); + + try { + + ReceiptFailedRecoveryRequest receiptFailedRecoveryRequest = request.getBody().get(); + + if (receiptFailedRecoveryRequest.getEventId() != null) { + + getEvent(receiptFailedRecoveryRequest.getEventId(), context, bizEventToReceiptService, receiptList, + bizEventCosmosClient, receiptCosmosClient, null); + + } else { + + String continuationToken = null; + + do { + + Iterable> feedResponseIterator = + receiptCosmosClient.getFailedReceiptDocuments(continuationToken, 100); + + for (FeedResponse page : feedResponseIterator) { + + for (Receipt receipt : page.getResults()) { + try { + getEvent(receipt.getEventId(), context, bizEventToReceiptService, receiptList, + bizEventCosmosClient, receiptCosmosClient, receipt); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + } + + continuationToken = page.getContinuationToken(); + + } + + } while (continuationToken != null); + + } + + + documentdb.setValue(receiptList); + return request.createResponseBuilder(HttpStatus.OK) + .body("OK") + .build(); + + } catch (NoSuchElementException | ReceiptNotFoundException | BizEventNotFoundException exception) { + return request.createResponseBuilder(HttpStatus.BAD_REQUEST) + .build(); + } catch (PDVTokenizerException | JsonProcessingException e) { + return request.createResponseBuilder(HttpStatus.INTERNAL_SERVER_ERROR) + .build(); + } + } + + private void getEvent(String eventId, ExecutionContext context, + BizEventToReceiptService bizEventToReceiptService, + List receiptList, BizEventCosmosClient bizEventCosmosClient, + ReceiptCosmosClient receiptCosmosClient, Receipt receipt) + throws BizEventNotFoundException, ReceiptNotFoundException, PDVTokenizerException, JsonProcessingException { + + BizEvent bizEvent = bizEventCosmosClient.getBizEventDocument( + eventId); + + if (!BizEventToReceiptUtils.isBizEventInvalid(bizEvent, context, logger)) { + + if (receipt == null) { + try { + receipt = receiptCosmosClient.getReceiptDocument( + eventId); + } catch (ReceiptNotFoundException e) { + receipt = BizEventToReceiptUtils.createReceipt(bizEvent, + bizEventToReceiptService, logger); + receipt.setStatus(ReceiptStatusType.FAILED); + } + } + + if (receipt != null && ( + receipt.getStatus().equals(ReceiptStatusType.FAILED) || + receipt.getStatus().equals(ReceiptStatusType.INSERTED) || + receipt.getStatus().equals(ReceiptStatusType.NOT_QUEUE_SENT) + )) { + if (receipt.getEventData() == null || receipt.getEventData().getDebtorFiscalCode() == null) { + BizEventToReceiptUtils.tokenizeReceipt(bizEventToReceiptService, bizEvent, receipt); + } + bizEventToReceiptService.handleSendMessageToQueue(bizEvent, receipt); + if(receipt.getStatus() != ReceiptStatusType.NOT_QUEUE_SENT){ + receipt.setStatus(ReceiptStatusType.INSERTED); + receipt.setInserted_at(System.currentTimeMillis()); + receipt.setReasonErr(null); + receipt.setReasonErrPayer(null); + } + receiptList.add(receipt); + } + + } + + } + +} \ No newline at end of file diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/BizEventCosmosClient.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/BizEventCosmosClient.java new file mode 100644 index 0000000..93285ff --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/BizEventCosmosClient.java @@ -0,0 +1,9 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.client; + +import it.gov.pagopa.receipt.pdf.helpdesk.entity.event.BizEvent; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.BizEventNotFoundException; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.ReceiptNotFoundException; + +public interface BizEventCosmosClient { + BizEvent getBizEventDocument(String eventId) throws ReceiptNotFoundException, BizEventNotFoundException; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/PDVTokenizerClient.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/PDVTokenizerClient.java new file mode 100644 index 0000000..e12df7b --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/PDVTokenizerClient.java @@ -0,0 +1,39 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.client; + +import it.gov.pagopa.receipt.pdf.helpdesk.exception.PDVTokenizerException; +import it.gov.pagopa.receipt.pdf.helpdesk.model.tokenizer.PiiResource; + +import java.net.http.HttpResponse; + +/** + * Client for invoking PDV Tokenizer service + */ +public interface PDVTokenizerClient { + + /** + * Search the token associated to the specified PII + * + * @param piiBody the {@link PiiResource} serialized as String + * @return the {@link HttpResponse} of the PDV Tokenizer service + * @throws PDVTokenizerException if an error occur when invoking the PDV Tokenizer service + */ + HttpResponse searchTokenByPII(String piiBody) throws PDVTokenizerException; + + /** + * Find the PII associated to the specified token + * + * @param token the token + * @return the {@link HttpResponse} of the PDV Tokenizer service + * @throws PDVTokenizerException if an error occur when invoking the PDV Tokenizer service + */ + HttpResponse findPIIByToken(String token) throws PDVTokenizerException; + + /** + * Create a new token for the specified PII + * + * @param piiBody the {@link PiiResource} serialized as String + * @return the {@link HttpResponse} of the PDV Tokenizer service + * @throws PDVTokenizerException if an error occur when invoking the PDV Tokenizer service + */ + HttpResponse createToken(String piiBody) throws PDVTokenizerException; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/ReceiptCosmosClient.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/ReceiptCosmosClient.java new file mode 100644 index 0000000..08445c2 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/ReceiptCosmosClient.java @@ -0,0 +1,15 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.client; + +import com.azure.cosmos.models.CosmosItemResponse; +import com.azure.cosmos.models.FeedResponse; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.Receipt; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.ReceiptNotFoundException; + +public interface ReceiptCosmosClient { + + Receipt getReceiptDocument(String receiptId) throws ReceiptNotFoundException; + + Iterable> getFailedReceiptDocuments(String continuationToken, Integer pageSize); + + CosmosItemResponse saveReceipts(Receipt receipt); +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/ReceiptQueueClient.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/ReceiptQueueClient.java new file mode 100644 index 0000000..a1c3933 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/ReceiptQueueClient.java @@ -0,0 +1,9 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.client; + +import com.azure.core.http.rest.Response; +import com.azure.storage.queue.models.SendMessageResult; + +public interface ReceiptQueueClient { + + Response sendMessageToQueue(String messageText); +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/BizEventCosmosClientImpl.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/BizEventCosmosClientImpl.java new file mode 100644 index 0000000..d29af15 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/BizEventCosmosClientImpl.java @@ -0,0 +1,76 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.client.impl; + +import com.azure.cosmos.CosmosClient; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.cosmos.CosmosContainer; +import com.azure.cosmos.CosmosDatabase; +import com.azure.cosmos.models.CosmosQueryRequestOptions; +import com.azure.cosmos.util.CosmosPagedIterable; +import it.gov.pagopa.receipt.pdf.helpdesk.client.BizEventCosmosClient; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.event.BizEvent; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.BizEventNotFoundException; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.ReceiptNotFoundException; + +/** + * Client for the CosmosDB database + */ +public class BizEventCosmosClientImpl implements BizEventCosmosClient { + + private static BizEventCosmosClientImpl instance; + + private final String databaseId = System.getenv("COSMOS_BIZ_EVENT_DB_NAME"); + private final String containerId = System.getenv("COSMOS_BIZ_EVENT_CONTAINER_NAME"); + + private final CosmosClient cosmosClient; + + private BizEventCosmosClientImpl() { + String azureKey = System.getenv("COSMOS_BIZ_EVENT_KEY"); + String serviceEndpoint = System.getenv("COSMOS_BIZ_EVENT_SERVICE_ENDPOINT"); + + this.cosmosClient = new CosmosClientBuilder() + .endpoint(serviceEndpoint) + .key(azureKey) + .buildClient(); + } + + public BizEventCosmosClientImpl(CosmosClient cosmosClient) { + this.cosmosClient = cosmosClient; + } + + public static BizEventCosmosClientImpl getInstance() { + if (instance == null) { + instance = new BizEventCosmosClientImpl(); + } + + return instance; + } + + /** + * Retrieve receipt document from CosmosDB database + * + * @param eventId Biz-event id + * @return biz-event document + * @throws ReceiptNotFoundException in case no receipt has been found with the given idEvent + */ + @Override + public BizEvent getBizEventDocument(String eventId) throws BizEventNotFoundException { + CosmosDatabase cosmosDatabase = this.cosmosClient.getDatabase(databaseId); + + CosmosContainer cosmosContainer = cosmosDatabase.getContainer(containerId); + + //Build query + String query = "SELECT * FROM c WHERE c.id = " + "'" + eventId + "'"; + + //Query the container + CosmosPagedIterable queryResponse = cosmosContainer + .queryItems(query, new CosmosQueryRequestOptions(), BizEvent.class); + + if (queryResponse.iterator().hasNext()) { + return queryResponse.iterator().next(); + } else { + throw new BizEventNotFoundException("Document not found in the defined container"); + } + + } + +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/PDVTokenizerClientImpl.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/PDVTokenizerClientImpl.java new file mode 100644 index 0000000..0b2918f --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/PDVTokenizerClientImpl.java @@ -0,0 +1,112 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.client.impl; + +import it.gov.pagopa.receipt.pdf.helpdesk.client.PDVTokenizerClient; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.enumeration.ReasonErrorCode; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.PDVTokenizerException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +/** + * {@inheritDoc} + */ +public class PDVTokenizerClientImpl implements PDVTokenizerClient { + + private final Logger logger = LoggerFactory.getLogger(PDVTokenizerClientImpl.class); + + private static final String BASE_PATH = System.getenv().getOrDefault("PDV_TOKENIZER_BASE_PATH", "https://api.uat.tokenizer.pdv.pagopa.it/tokenizer/v1"); + private static final String SUBSCRIPTION_KEY = System.getenv().getOrDefault("PDV_TOKENIZER_SUBSCRIPTION_KEY", ""); + private static final String SUBSCRIPTION_KEY_HEADER = System.getenv().getOrDefault("TOKENIZER_APIM_HEADER_KEY", "x-api-key"); + private static final String SEARCH_TOKEN_ENDPOINT = System.getenv().getOrDefault("PDV_TOKENIZER_SEARCH_TOKEN_ENDPOINT", "/tokens/search"); + private static final String FIND_PII_ENDPOINT = System.getenv().getOrDefault("PDV_TOKENIZER_FIND_PII_ENDPOINT", "/tokens/%s/pii"); + private static final String CREATE_TOKEN_ENDPOINT = System.getenv().getOrDefault("PDV_TOKENIZER_CREATE_TOKEN_ENDPOINT", "/tokens"); + + private final HttpClient client; + + private static PDVTokenizerClientImpl instance; + + public static PDVTokenizerClientImpl getInstance() { + if (instance == null) { + instance = new PDVTokenizerClientImpl(); + } + return instance; + } + + private PDVTokenizerClientImpl() { + this.client = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_2) + .build(); + } + + PDVTokenizerClientImpl(HttpClient client) { + this.client = client; + } + + /** + * {@inheritDoc} + */ + @Override + public HttpResponse searchTokenByPII(String piiBody) throws PDVTokenizerException { + String uri = String.format("%s%s", BASE_PATH, SEARCH_TOKEN_ENDPOINT); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(uri)) + .version(HttpClient.Version.HTTP_2) + .header(SUBSCRIPTION_KEY_HEADER, SUBSCRIPTION_KEY) + .POST(HttpRequest.BodyPublishers.ofString(piiBody)) + .build(); + + return makeCall(request); + } + + /** + * {@inheritDoc} + */ + @Override + public HttpResponse findPIIByToken(String token) throws PDVTokenizerException { + String endpoint = String.format(FIND_PII_ENDPOINT, token); + String uri = String.format("%s%s", BASE_PATH, endpoint); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(uri)) + .version(HttpClient.Version.HTTP_2) + .header(SUBSCRIPTION_KEY_HEADER, SUBSCRIPTION_KEY) + .build(); + + return makeCall(request); + } + + /** + * {@inheritDoc} + */ + @Override + public HttpResponse createToken(String piiBody) throws PDVTokenizerException { + String uri = String.format("%s%s", BASE_PATH, CREATE_TOKEN_ENDPOINT); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(uri)) + .version(HttpClient.Version.HTTP_2) + .header(SUBSCRIPTION_KEY_HEADER, SUBSCRIPTION_KEY) + .PUT(HttpRequest.BodyPublishers.ofString(piiBody)) + .build(); + + return makeCall(request); + } + + private HttpResponse makeCall(HttpRequest request) throws PDVTokenizerException { + try { + return client.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (IOException e) { + throw new PDVTokenizerException("I/O error when invoking PDV Tokenizer", ReasonErrorCode.ERROR_PDV_IO.getCode(), e); + } catch (InterruptedException e) { + logger.warn("This thread was interrupted, restoring the state"); + Thread.currentThread().interrupt(); + throw new PDVTokenizerException("Unexpected error when invoking PDV Tokenizer, the thread was interrupted", ReasonErrorCode.ERROR_PDV_UNEXPECTED.getCode(), e); + } + } +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/ReceiptCosmosClientImpl.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/ReceiptCosmosClientImpl.java new file mode 100644 index 0000000..44d398f --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/ReceiptCosmosClientImpl.java @@ -0,0 +1,117 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.client.impl; + +import com.azure.cosmos.CosmosClient; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.cosmos.CosmosContainer; +import com.azure.cosmos.CosmosDatabase; +import com.azure.cosmos.models.*; +import com.azure.cosmos.util.CosmosPagedIterable; +import it.gov.pagopa.receipt.pdf.helpdesk.client.ReceiptCosmosClient; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.Receipt; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.ReceiptNotFoundException; + +import java.time.OffsetDateTime; + +/** + * Client for the CosmosDB database + */ +public class ReceiptCosmosClientImpl implements ReceiptCosmosClient { + + private static ReceiptCosmosClientImpl instance; + + private final String databaseId = System.getenv("COSMOS_RECEIPT_DB_NAME"); + private final String containerId = System.getenv("COSMOS_RECEIPT_CONTAINER_NAME"); + + private final String millisDiff = System.getenv("MAX_DATE_DIFF_MILLIS"); + + private final CosmosClient cosmosClient; + + private ReceiptCosmosClientImpl() { + String azureKey = System.getenv("COSMOS_RECEIPT_KEY"); + String serviceEndpoint = System.getenv("COSMOS_RECEIPT_SERVICE_ENDPOINT"); + + this.cosmosClient = new CosmosClientBuilder() + .endpoint(serviceEndpoint) + .key(azureKey) + .buildClient(); + } + + public ReceiptCosmosClientImpl(CosmosClient cosmosClient) { + this.cosmosClient = cosmosClient; + } + + public static ReceiptCosmosClientImpl getInstance() { + if (instance == null) { + instance = new ReceiptCosmosClientImpl(); + } + + return instance; + } + + /** + * Retrieve receipt document from CosmosDB database + * + * @param eventId Biz-event id + * @return receipt document + * @throws ReceiptNotFoundException in case no receipt has been found with the given idEvent + */ + public Receipt getReceiptDocument(String eventId) throws ReceiptNotFoundException { + CosmosDatabase cosmosDatabase = this.cosmosClient.getDatabase(databaseId); + + CosmosContainer cosmosContainer = cosmosDatabase.getContainer(containerId); + + //Build query + String query = "SELECT * FROM c WHERE c.eventId = " + "'" + eventId + "'"; + + //Query the container + CosmosPagedIterable queryResponse = cosmosContainer + .queryItems(query, new CosmosQueryRequestOptions(), Receipt.class); + + if (queryResponse.iterator().hasNext()) { + return queryResponse.iterator().next(); + } else { + throw new ReceiptNotFoundException("Document not found in the defined container"); + } + + } + + /** + * Retrieve failed receipt documents from CosmosDB database + * + * @param continuationToken Paged query continuation token + * @return receipt documents + */ + @Override + public Iterable> getFailedReceiptDocuments(String continuationToken, Integer pageSize) { + CosmosDatabase cosmosDatabase = this.cosmosClient.getDatabase(databaseId); + + CosmosContainer cosmosContainer = cosmosDatabase.getContainer(containerId); + + //Build query + String query = "SELECT * FROM c WHERE c.status = 'FAILED' or c.status = 'NOT_QUEUE_SENT' or " + + "( c.status= = 'INSERTED' AND ( " + OffsetDateTime.now().toInstant().toEpochMilli() + + " - c.inserted_at) >= " + millisDiff + " )"; + + //Query the container + return cosmosContainer + .queryItems(query, new CosmosQueryRequestOptions(), Receipt.class) + .iterableByPage(continuationToken,pageSize); + + } + + /** + * Save Receipts on CosmosDB database + * + * @param receipt Receipts to save + * @return receipt documents + */ + @Override + public CosmosItemResponse saveReceipts(Receipt receipt) { + CosmosDatabase cosmosDatabase = this.cosmosClient.getDatabase(databaseId); + + CosmosContainer cosmosContainer = cosmosDatabase.getContainer(containerId); + + return cosmosContainer.createItem(receipt); + } + +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/ReceiptQueueClientImpl.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/ReceiptQueueClientImpl.java new file mode 100644 index 0000000..eef300c --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/ReceiptQueueClientImpl.java @@ -0,0 +1,58 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.client.impl; + +import com.azure.core.http.rest.Response; +import com.azure.storage.queue.QueueClient; +import com.azure.storage.queue.QueueClientBuilder; +import com.azure.storage.queue.models.SendMessageResult; +import it.gov.pagopa.receipt.pdf.helpdesk.client.ReceiptQueueClient; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +/** + * Client for the Queue + */ +public class ReceiptQueueClientImpl implements ReceiptQueueClient { + + private static ReceiptQueueClientImpl instance; + + private final int receiptQueueDelay = Integer.parseInt(System.getenv().getOrDefault("RECEIPT_QUEUE_DELAY", "1")); + + private final QueueClient queueClient; + + private ReceiptQueueClientImpl() { + String receiptQueueConnString = System.getenv("RECEIPT_QUEUE_CONN_STRING"); + String receiptQueueTopic = System.getenv("RECEIPT_QUEUE_TOPIC"); + + this.queueClient = new QueueClientBuilder() + .connectionString(receiptQueueConnString) + .queueName(receiptQueueTopic) + .buildClient(); + } + + public ReceiptQueueClientImpl(QueueClient queueClient) { + this.queueClient = queueClient; + } + + public static ReceiptQueueClientImpl getInstance() { + if (instance == null) { + instance = new ReceiptQueueClientImpl(); + } + + return instance; + } + + /** + * Send string message to the queue + * + * @param messageText Biz-event encoded to base64 string + * @return response from the queue + */ + public Response sendMessageToQueue(String messageText) { + + return this.queueClient.sendMessageWithResponse( + messageText, Duration.of(receiptQueueDelay, ChronoUnit.SECONDS), + null, null, null); + + } +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/AuthRequest.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/AuthRequest.java new file mode 100644 index 0000000..5f6bf3c --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/AuthRequest.java @@ -0,0 +1,20 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.event; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class AuthRequest { + private String authOutcome; + private String guid; + private String correlationId; + private String error; + @JsonProperty(value="auth_code") + private String authCode; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/BizEvent.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/BizEvent.java new file mode 100644 index 0000000..2751da8 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/BizEvent.java @@ -0,0 +1,46 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.event; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.event.enumeration.BizEventStatusType; +import lombok.*; + +import java.util.List; +import java.util.Map; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class BizEvent { + private String id; + private String version; + private String idPaymentManager; + private String complete; + private String receiptId; + private List missingInfo; + private DebtorPosition debtorPosition; + private Creditor creditor; + private Psp psp; + private Debtor debtor; + private Payer payer; + private PaymentInfo paymentInfo; + private List transferList; + private TransactionDetails transactionDetails; + private Long timestamp; + private Map properties; + + // internal management field + @Builder.Default + private BizEventStatusType eventStatus = BizEventStatusType.NA; + @Builder.Default + private Integer eventRetryEnrichmentCount = 0; + @Builder.Default + private Boolean eventTriggeredBySchedule = Boolean.FALSE; + private String eventErrorMessage; + + @Builder.Default + private Boolean attemptedPoisonRetry = Boolean.FALSE; + +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Creditor.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Creditor.java new file mode 100644 index 0000000..f4d32f7 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Creditor.java @@ -0,0 +1,18 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.event; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class Creditor { + private String idPA; + private String idBrokerPA; + private String idStation; + private String companyName; + private String officeName; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Debtor.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Debtor.java new file mode 100644 index 0000000..b8da40b --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Debtor.java @@ -0,0 +1,25 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.event; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class Debtor { + private String fullName; + private String entityUniqueIdentifierType; + private String entityUniqueIdentifierValue; + private String streetName; + private String civicNumber; + private String postalCode; + private String city; + private String stateProvinceRegion; + private String country; + @JsonProperty(value="eMail") + private String eMail; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/DebtorPosition.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/DebtorPosition.java new file mode 100644 index 0000000..e4c24d4 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/DebtorPosition.java @@ -0,0 +1,17 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.event; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class DebtorPosition { + private String modelType; + private String noticeNumber; + private String iuv; + private String iur; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Details.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Details.java new file mode 100644 index 0000000..34af148 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Details.java @@ -0,0 +1,16 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.event; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class Details { + private String blurredNumber; + private String holder; + private String circuit; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Info.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Info.java new file mode 100644 index 0000000..c1b6f20 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Info.java @@ -0,0 +1,22 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.event; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class Info { + private String type; + private String blurredNumber; + private String holder; + private String expireMonth; + private String expireYear; + private String brand; + private String issuerAbi; + private String issuerName; + private String label; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/MBD.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/MBD.java new file mode 100644 index 0000000..fb13c96 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/MBD.java @@ -0,0 +1,25 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.event; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class MBD { + @JsonProperty(value="IUBD") + private String iubd; + @JsonProperty(value="oraAcquisto") + private String purchaseTime; + @JsonProperty(value="importo") + private String amount; + @JsonProperty(value="tipoBollo") + private String stampType; + @JsonProperty(value="MBDAttachment") + private String mbdAttachment; //MBD base64 + +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/MapEntry.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/MapEntry.java new file mode 100644 index 0000000..c1d1dee --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/MapEntry.java @@ -0,0 +1,17 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.event; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class MapEntry { + private String key; + private String value; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Payer.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Payer.java new file mode 100644 index 0000000..6e0504c --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Payer.java @@ -0,0 +1,25 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.event; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class Payer { + private String fullName; + private String entityUniqueIdentifierType; + private String entityUniqueIdentifierValue; + private String streetName; + private String civicNumber; + private String postalCode; + private String city; + private String stateProvinceRegion; + private String country; + @JsonProperty(value="eMail") + private String eMail; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/PaymentInfo.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/PaymentInfo.java new file mode 100644 index 0000000..4ca963d --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/PaymentInfo.java @@ -0,0 +1,34 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.event; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class PaymentInfo { + private String paymentDateTime; + private String applicationDate; + private String transferDate; + private String dueDate; + private String paymentToken; + private String amount; + private String fee; + private String primaryCiIncurredFee; + private String idBundle; + private String idCiBundle; + private String totalNotice; + private String paymentMethod; + private String touchpoint; + private String remittanceInformation; + private String description; + private List metadata; + @JsonProperty(value="IUR") + private String IUR; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Psp.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Psp.java new file mode 100644 index 0000000..da2b211 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Psp.java @@ -0,0 +1,20 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.event; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class Psp { + private String idPsp; + private String idBrokerPsp; + private String idChannel; + private String psp; + private String pspPartitaIVA; + private String pspFiscalCode; + private String channelDescription; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Transaction.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Transaction.java new file mode 100644 index 0000000..357a783 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Transaction.java @@ -0,0 +1,25 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.event; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class Transaction { + private long idTransaction; + private long grandTotal; + private long amount; + private long fee; + private String transactionStatus; + private String accountingStatus; + private String rrn; + private String authorizationCode; + private String creationDate; + private String numAut; + private String accountCode; + private TransactionPsp psp; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/TransactionDetails.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/TransactionDetails.java new file mode 100644 index 0000000..ff2ddd2 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/TransactionDetails.java @@ -0,0 +1,17 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.event; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class TransactionDetails { + private String origin; + private User user; + private Transaction transaction; + private WalletItem wallet; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/TransactionPsp.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/TransactionPsp.java new file mode 100644 index 0000000..1efa7bc --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/TransactionPsp.java @@ -0,0 +1,16 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.event; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class TransactionPsp { + private String idChannel; + private String businessName; + private String serviceName; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Transfer.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Transfer.java new file mode 100644 index 0000000..7734703 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/Transfer.java @@ -0,0 +1,27 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.event; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.*; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class Transfer { + private String idTransfer; + private String fiscalCodePA; + private String companyName; + private String amount; + private String transferCategory; + private String remittanceInformation; +// @JsonProperty(value="IBAN") - + private String IBAN; +// @JsonProperty(value="MBD") - +// private MBD mbd; - + private String MBDAttachment; + private List metadata; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/User.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/User.java new file mode 100644 index 0000000..820c1c2 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/User.java @@ -0,0 +1,21 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.event; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.event.enumeration.UserType; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class User { + private String fullName; + private UserType type; + private String fiscalCode; + private String notificationEmail; + private String userId; + private String userStatus; + private String userStatusDescription; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/WalletItem.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/WalletItem.java new file mode 100644 index 0000000..0119d0a --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/WalletItem.java @@ -0,0 +1,25 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.event; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.event.enumeration.WalletType; +import lombok.*; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class WalletItem { + private String idWallet; + private WalletType walletType; + private List enableableFunctions; + private boolean pagoPa; + private String onboardingChannel; + private boolean favourite; + private String createDate; + private Info info; + private AuthRequest authRequest; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/enumeration/BizEventStatusType.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/enumeration/BizEventStatusType.java new file mode 100644 index 0000000..2831d43 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/enumeration/BizEventStatusType.java @@ -0,0 +1,5 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.event.enumeration; + +public enum BizEventStatusType { + NA, RETRY, FAILED, DONE +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/enumeration/UserType.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/enumeration/UserType.java new file mode 100644 index 0000000..61e9962 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/enumeration/UserType.java @@ -0,0 +1,14 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.event.enumeration; + +import com.google.api.client.util.NullValue; +import com.google.api.client.util.Value; + + +public enum UserType { + @NullValue + UNKNOWN, + @Value("F") + F, + @Value("G") + G +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/enumeration/WalletType.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/enumeration/WalletType.java new file mode 100644 index 0000000..9e7b307 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/event/enumeration/WalletType.java @@ -0,0 +1,5 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.event.enumeration; + +public enum WalletType { + CARD, PAYPAL, BANCOMATPAY +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/CartItem.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/CartItem.java new file mode 100644 index 0000000..a9387b8 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/CartItem.java @@ -0,0 +1,13 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class CartItem { + private String subject; + private String payeeName; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/EventData.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/EventData.java new file mode 100644 index 0000000..0fd5512 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/EventData.java @@ -0,0 +1,18 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +public class EventData { + private String payerFiscalCode; + private String debtorFiscalCode; + private String transactionCreationDate; + private String amount; + private List cart; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/IOMessageData.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/IOMessageData.java new file mode 100644 index 0000000..2e47de3 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/IOMessageData.java @@ -0,0 +1,13 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class IOMessageData { + private String idMessageDebtor; + private String idMessagePayer; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/ReasonError.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/ReasonError.java new file mode 100644 index 0000000..a81e9bd --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/ReasonError.java @@ -0,0 +1,16 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ReasonError { + private int code; + private String message; + +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/Receipt.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/Receipt.java new file mode 100644 index 0000000..c38cf4d --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/Receipt.java @@ -0,0 +1,28 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt; + +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.enumeration.ReceiptStatusType; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class Receipt { + + private String eventId; + private String id; + private String version; + private EventData eventData; + private IOMessageData ioMessageData; + private ReceiptStatusType status; + private ReceiptMetadata mdAttach; + private ReceiptMetadata mdAttachPayer; + private int numRetry; + private ReasonError reasonErr; + private ReasonError reasonErrPayer; + private long inserted_at; + private long generated_at; + private long notified_at; + +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/ReceiptMetadata.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/ReceiptMetadata.java new file mode 100644 index 0000000..2554e1c --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/ReceiptMetadata.java @@ -0,0 +1,14 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class ReceiptMetadata { + + private String name; + private String url; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/enumeration/ReasonErrorCode.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/enumeration/ReasonErrorCode.java new file mode 100644 index 0000000..bd84a6e --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/enumeration/ReasonErrorCode.java @@ -0,0 +1,20 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.enumeration; + +public enum ReasonErrorCode { + ERROR_QUEUE(902), + ERROR_COSMOS(904), + ERROR_PDV_IO(800), + ERROR_PDV_UNEXPECTED(801), + ERROR_PDV_MAPPING(802); + ; + + private final int code; + + ReasonErrorCode(int code){ + this.code = code; + } + + public int getCode(){ + return this.code; + } +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/enumeration/ReceiptErrorStatusType.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/enumeration/ReceiptErrorStatusType.java new file mode 100644 index 0000000..dd74502 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/enumeration/ReceiptErrorStatusType.java @@ -0,0 +1,5 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.enumeration; + +public enum ReceiptErrorStatusType { + TO_REVIEW, REVIEWED, NOT_TO_RETRY, REQUEUED +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/enumeration/ReceiptStatusType.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/enumeration/ReceiptStatusType.java new file mode 100644 index 0000000..9281c9e --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/entity/receipt/enumeration/ReceiptStatusType.java @@ -0,0 +1,5 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.enumeration; + +public enum ReceiptStatusType { + NOT_QUEUE_SENT, INSERTED, RETRY, GENERATED, SIGNED, FAILED, IO_NOTIFIED, IO_ERROR_TO_NOTIFY, IO_NOTIFIER_RETRY, UNABLE_TO_SEND, NOT_TO_NOTIFY +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/exception/BizEventNotFoundException.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/exception/BizEventNotFoundException.java new file mode 100644 index 0000000..418265a --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/exception/BizEventNotFoundException.java @@ -0,0 +1,26 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.exception; + +/** Thrown in case no receipt is found in the CosmosDB container */ +public class BizEventNotFoundException extends Exception{ + + /** + * Constructs new exception with provided message and cause + * + * @param message Detail message + */ + public BizEventNotFoundException(String message) { + super(message); + } + + /** + * Constructs new exception with provided message and cause + * + * @param message Detail message + * @param cause Exception thrown + */ + public BizEventNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} + + diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/exception/PDVTokenizerException.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/exception/PDVTokenizerException.java new file mode 100644 index 0000000..de35d3a --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/exception/PDVTokenizerException.java @@ -0,0 +1,35 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.exception; + +import lombok.Getter; + +/** + * Thrown in case an error occur when invoking PDV Tokenizer service + */ +@Getter +public class PDVTokenizerException extends Exception { + + private final int statusCode; + + /** + * Constructs new exception with provided message + * + * @param message Detail message + * @param statusCode status code + */ + public PDVTokenizerException(String message, int statusCode) { + super(message); + this.statusCode = statusCode; + } + + /** + * Constructs new exception with provided message + * + * @param message Detail message + * @param statusCode status code + * @param cause Exception causing the constructed one + */ + public PDVTokenizerException(String message, int statusCode, Throwable cause) { + super(message, cause); + this.statusCode = statusCode; + } +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/exception/PDVTokenizerUnexpectedException.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/exception/PDVTokenizerUnexpectedException.java new file mode 100644 index 0000000..5508a85 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/exception/PDVTokenizerUnexpectedException.java @@ -0,0 +1,19 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.exception; + +import lombok.Getter; + +/** + * Thrown in case an unexpected error occur when invoking PDV Tokenizer service + */ +@Getter +public class PDVTokenizerUnexpectedException extends RuntimeException { + + /** + * Constructs new exception with provided cause + * + * @param cause Exception causing the constructed one + */ + public PDVTokenizerUnexpectedException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/exception/ReceiptNotFoundException.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/exception/ReceiptNotFoundException.java new file mode 100644 index 0000000..b1c3839 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/exception/ReceiptNotFoundException.java @@ -0,0 +1,26 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.exception; + +/** Thrown in case no receipt is found in the CosmosDB container */ +public class ReceiptNotFoundException extends Exception{ + + /** + * Constructs new exception with provided message and cause + * + * @param message Detail message + */ + public ReceiptNotFoundException(String message) { + super(message); + } + + /** + * Constructs new exception with provided message and cause + * + * @param message Detail message + * @param cause Exception thrown + */ + public ReceiptNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} + + diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/exception/UnableToQueueException.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/exception/UnableToQueueException.java new file mode 100644 index 0000000..2b66d3d --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/exception/UnableToQueueException.java @@ -0,0 +1,24 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.exception; + +public class UnableToQueueException extends Exception { + + /** + * Constructs new exception with provided message and cause + * + * @param message Detail message + */ + public UnableToQueueException(String message) { + super(message); + } + + /** + * Constructs new exception with provided message and cause + * + * @param message Detail message + * @param cause Exception thrown + */ + public UnableToQueueException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/AppInfo.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/AppInfo.java new file mode 100644 index 0000000..588621c --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/AppInfo.java @@ -0,0 +1,18 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.*; + +@Data +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class AppInfo { + + private String name; + private String version; + private String environment; +} \ No newline at end of file diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/ReceiptFailedRecoveryRequest.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/ReceiptFailedRecoveryRequest.java new file mode 100644 index 0000000..0b8ff1f --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/ReceiptFailedRecoveryRequest.java @@ -0,0 +1,10 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.model; + +import lombok.Data; + +@Data +public class ReceiptFailedRecoveryRequest { + + private String eventId; + +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/tokenizer/ErrorMessage.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/tokenizer/ErrorMessage.java new file mode 100644 index 0000000..5369749 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/tokenizer/ErrorMessage.java @@ -0,0 +1,18 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.model.tokenizer; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Model class for the error response of the PDV Tokenizer for status 403, 404, 429 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ErrorMessage { + + private String message; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/tokenizer/ErrorResponse.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/tokenizer/ErrorResponse.java new file mode 100644 index 0000000..0b9dd9b --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/tokenizer/ErrorResponse.java @@ -0,0 +1,26 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.model.tokenizer; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * Model class for the error response of the PDV Tokenizer for status 400, 500 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ErrorResponse { + + private String detail; + private String instance; + private List invalidParams; + private int status; + private String title; + private String type; + +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/tokenizer/InvalidParam.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/tokenizer/InvalidParam.java new file mode 100644 index 0000000..8b394a9 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/tokenizer/InvalidParam.java @@ -0,0 +1,19 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.model.tokenizer; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Model class for the details of invalid param error + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InvalidParam { + + private String name; + private String reason; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/tokenizer/PiiResource.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/tokenizer/PiiResource.java new file mode 100644 index 0000000..94ce4e6 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/tokenizer/PiiResource.java @@ -0,0 +1,18 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.model.tokenizer; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Model class that hold Personal Identifiable Information + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PiiResource { + + private String pii; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/tokenizer/TokenResource.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/tokenizer/TokenResource.java new file mode 100644 index 0000000..26248af --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/model/tokenizer/TokenResource.java @@ -0,0 +1,18 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.model.tokenizer; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Model class that hold the token related to a PII + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TokenResource { + + private String token; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/service/BizEventToReceiptService.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/service/BizEventToReceiptService.java new file mode 100644 index 0000000..ad494ef --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/service/BizEventToReceiptService.java @@ -0,0 +1,39 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import it.gov.pagopa.receipt.pdf.helpdesk.client.ReceiptCosmosClient; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.event.BizEvent; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.EventData; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.Receipt; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.PDVTokenizerException; + +public interface BizEventToReceiptService { + + /** + * Handles sending biz-events as message to queue and updates receipt's status + * + * @param bizEvent Biz-event from CosmosDB + * @param receipt Receipt to update + */ + void handleSendMessageToQueue(BizEvent bizEvent, Receipt receipt); + + + /** + * Retrieve conditionally the transaction creation date from biz-event + * + * @param bizEvent Biz-event from CosmosDB + * @return transaction date + */ + String getTransactionCreationDate(BizEvent bizEvent); + + /** + * Calls PDVTokenizerService to tokenize the fiscal codes for both Debtor & Payer (if present) + * + * @param bizEvent BizEvent where fiscalCodes are stored + * @param receipt Receipt to update in case of errors + * @param eventData Event data to update with tokenized fiscalCodes + * @throws JsonProcessingException if an error occur when parsing input or output + * @throws PDVTokenizerException if an error occur when invoking the PDV Tokenizer + */ + void tokenizeFiscalCodes(BizEvent bizEvent, Receipt receipt, EventData eventData) throws JsonProcessingException, PDVTokenizerException; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/service/PDVTokenizerService.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/service/PDVTokenizerService.java new file mode 100644 index 0000000..21bc32c --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/service/PDVTokenizerService.java @@ -0,0 +1,41 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import it.gov.pagopa.receipt.pdf.helpdesk.client.PDVTokenizerClient; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.PDVTokenizerException; + +/** + * Service that handle the input and output for the {@link PDVTokenizerClient} + */ +public interface PDVTokenizerService { + + /** + * Search the token associated to the specified fiscal code by calling {@link PDVTokenizerClient#searchTokenByPII(String)} + * + * @param fiscalCode the fiscal code + * @return the token associated to the fiscal code + * @throws JsonProcessingException if an error occur when parsing input or output + * @throws PDVTokenizerException if an error occur when invoking the PDV Tokenizer + */ + String getToken(String fiscalCode) throws JsonProcessingException, PDVTokenizerException; + + /** + * Search the fiscal code associated to the specified token by calling {@link PDVTokenizerClient#findPIIByToken(String)} + * + * @param token the token + * @return the fiscal code associated to the provided token + * @throws JsonProcessingException if an error occur when parsing input or output + * @throws PDVTokenizerException if an error occur when invoking the PDV Tokenizer + */ + String getFiscalCode(String token) throws PDVTokenizerException, JsonProcessingException; + + /** + * Generate a token for the specified fiscal code by calling {@link PDVTokenizerClient#createToken(String)} + * + * @param fiscalCode the fiscal code + * @return the generated token + * @throws JsonProcessingException if an error occur when parsing input or output + * @throws PDVTokenizerException if an error occur when invoking the PDV Tokenizer + */ + String generateTokenForFiscalCode(String fiscalCode) throws PDVTokenizerException, JsonProcessingException; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/service/PDVTokenizerServiceRetryWrapper.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/service/PDVTokenizerServiceRetryWrapper.java new file mode 100644 index 0000000..0ccb32a --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/service/PDVTokenizerServiceRetryWrapper.java @@ -0,0 +1,40 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.PDVTokenizerException; + +/** + * Service that wrap the {@link PDVTokenizerService} for adding retry logic for tokenizer responses with 429 status code + */ +public interface PDVTokenizerServiceRetryWrapper { + + /** + * Call {@link PDVTokenizerService#getToken(String)} with retry on failure + * + * @param fiscalCode the fiscal code + * @return the token associated to the fiscal code + * @throws JsonProcessingException if an error occur when parsing input or output + * @throws PDVTokenizerException if an error occur when invoking the PDV Tokenizer + */ + String getTokenWithRetry(String fiscalCode) throws JsonProcessingException, PDVTokenizerException; + + /** + * Call {@link PDVTokenizerService#getFiscalCode(String)} with retry on failure + * + * @param token the token + * @return the fiscal code associated to the provided token + * @throws JsonProcessingException if an error occur when parsing input or output + * @throws PDVTokenizerException if an error occur when invoking the PDV Tokenizer + */ + String getFiscalCodeWithRetry(String token) throws PDVTokenizerException, JsonProcessingException; + + /** + * Call {@link PDVTokenizerService#generateTokenForFiscalCode(String)} with retry on failure + * + * @param fiscalCode the fiscal code + * @return the generated token + * @throws JsonProcessingException if an error occur when parsing input or output + * @throws PDVTokenizerException if an error occur when invoking the PDV Tokenizer + */ + String generateTokenForFiscalCodeWithRetry(String fiscalCode) throws PDVTokenizerException, JsonProcessingException; +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/service/impl/BizEventToReceiptServiceImpl.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/service/impl/BizEventToReceiptServiceImpl.java new file mode 100644 index 0000000..956f880 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/service/impl/BizEventToReceiptServiceImpl.java @@ -0,0 +1,140 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.service.impl; + +import com.azure.core.http.rest.Response; +import com.azure.cosmos.models.CosmosItemResponse; +import com.azure.storage.queue.models.SendMessageResult; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.microsoft.azure.functions.HttpStatus; +import it.gov.pagopa.receipt.pdf.helpdesk.client.ReceiptCosmosClient; +import it.gov.pagopa.receipt.pdf.helpdesk.client.ReceiptQueueClient; +import it.gov.pagopa.receipt.pdf.helpdesk.client.impl.ReceiptCosmosClientImpl; +import it.gov.pagopa.receipt.pdf.helpdesk.client.impl.ReceiptQueueClientImpl; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.event.BizEvent; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.EventData; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.ReasonError; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.Receipt; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.enumeration.ReasonErrorCode; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.enumeration.ReceiptStatusType; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.PDVTokenizerException; +import it.gov.pagopa.receipt.pdf.helpdesk.service.BizEventToReceiptService; +import it.gov.pagopa.receipt.pdf.helpdesk.service.PDVTokenizerServiceRetryWrapper; +import it.gov.pagopa.receipt.pdf.helpdesk.utils.ObjectMapperUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Objects; + +public class BizEventToReceiptServiceImpl implements BizEventToReceiptService { + + private final Logger logger = LoggerFactory.getLogger(BizEventToReceiptServiceImpl.class); + + private final PDVTokenizerServiceRetryWrapper pdvTokenizerService; + private final ReceiptQueueClient queueClient; + + public BizEventToReceiptServiceImpl() { + this.pdvTokenizerService = new PDVTokenizerServiceRetryWrapperImpl(); + this.queueClient = ReceiptQueueClientImpl.getInstance(); + } + + public BizEventToReceiptServiceImpl(PDVTokenizerServiceRetryWrapper pdvTokenizerService, ReceiptQueueClient queueClient) { + this.pdvTokenizerService = pdvTokenizerService; + this.queueClient = queueClient; + } + + /** + * {@inheritDoc} + */ + @Override + public void handleSendMessageToQueue(BizEvent bizEvent, Receipt receipt) { + //Encode biz-event to base64 string + String messageText = Base64.getMimeEncoder().encodeToString( + Objects.requireNonNull(ObjectMapperUtils.writeValueAsString(bizEvent)).getBytes(StandardCharsets.UTF_8) + ); + + //Add message to the queue + int statusCode; + try { + Response sendMessageResult = queueClient.sendMessageToQueue(messageText); + + statusCode = sendMessageResult.getStatusCode(); + } catch (Exception e) { + statusCode = ReasonErrorCode.ERROR_QUEUE.getCode(); + logger.error(String.format("Sending BizEvent with id %s to queue failed", bizEvent.getId()), e); + } + + if (statusCode != HttpStatus.CREATED.value()) { + String errorString = String.format( + "[BizEventToReceiptService] Error sending message to queue for receipt with eventId %s", + receipt.getEventId()); + handleError(receipt, ReceiptStatusType.NOT_QUEUE_SENT, errorString, statusCode); + //Error info + logger.error(errorString); + } + } + + /** + * Handles errors for queue and cosmos and updates receipt's status accordingly + * + * @param receipt Receipt to update + */ + private void handleError(Receipt receipt, ReceiptStatusType statusType, String errorMessage, int errorCode) { + receipt.setStatus(statusType); + ReasonError reasonError = new ReasonError(errorCode, errorMessage); + receipt.setReasonErr(reasonError); + } + + /** + * {@inheritDoc} + */ + @Override + public String getTransactionCreationDate(BizEvent bizEvent) { + if (bizEvent.getTransactionDetails() != null && bizEvent.getTransactionDetails().getTransaction() != null) { + return bizEvent.getTransactionDetails().getTransaction().getCreationDate(); + + } else if (bizEvent.getPaymentInfo() != null) { + return bizEvent.getPaymentInfo().getPaymentDateTime(); + } + + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public void tokenizeFiscalCodes(BizEvent bizEvent, Receipt receipt, EventData eventData) throws JsonProcessingException, PDVTokenizerException { + try { + if (bizEvent.getDebtor() != null && bizEvent.getDebtor().getEntityUniqueIdentifierValue() != null) { + eventData.setDebtorFiscalCode( + pdvTokenizerService.generateTokenForFiscalCodeWithRetry(bizEvent.getDebtor().getEntityUniqueIdentifierValue()) + ); + } + if (bizEvent.getPayer() != null && bizEvent.getPayer().getEntityUniqueIdentifierValue() != null) { + eventData.setPayerFiscalCode( + pdvTokenizerService.generateTokenForFiscalCodeWithRetry(bizEvent.getPayer().getEntityUniqueIdentifierValue()) + ); + } + } catch (PDVTokenizerException e) { + handleTokenizerException(receipt, e.getMessage(), e.getStatusCode()); + throw e; + } catch (JsonProcessingException e) { + handleTokenizerException(receipt, e.getMessage(), ReasonErrorCode.ERROR_PDV_MAPPING.getCode()); + throw e; + } + } + + /** + * Handles errors for PDV tokenizer and updates receipt's status accordingly + * + * @param receipt Receipt to update + * @param errorMessage Message to save + * @param statusCode StatusCode to save + */ + private void handleTokenizerException(Receipt receipt, String errorMessage, int statusCode) { + receipt.setStatus(ReceiptStatusType.FAILED); + ReasonError reasonError = new ReasonError(statusCode, errorMessage); + receipt.setReasonErr(reasonError); + } +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/service/impl/PDVTokenizerServiceImpl.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/service/impl/PDVTokenizerServiceImpl.java new file mode 100644 index 0000000..53c3a49 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/service/impl/PDVTokenizerServiceImpl.java @@ -0,0 +1,112 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.service.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import it.gov.pagopa.receipt.pdf.helpdesk.client.PDVTokenizerClient; +import it.gov.pagopa.receipt.pdf.helpdesk.client.impl.PDVTokenizerClientImpl; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.PDVTokenizerException; +import it.gov.pagopa.receipt.pdf.helpdesk.model.tokenizer.ErrorMessage; +import it.gov.pagopa.receipt.pdf.helpdesk.model.tokenizer.ErrorResponse; +import it.gov.pagopa.receipt.pdf.helpdesk.model.tokenizer.PiiResource; +import it.gov.pagopa.receipt.pdf.helpdesk.model.tokenizer.TokenResource; +import it.gov.pagopa.receipt.pdf.helpdesk.service.PDVTokenizerService; +import it.gov.pagopa.receipt.pdf.helpdesk.utils.ObjectMapperUtils; +import org.apache.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.http.HttpResponse; + +/** + * {@inheritDoc} + */ +public class PDVTokenizerServiceImpl implements PDVTokenizerService { + + private final Logger logger = LoggerFactory.getLogger(PDVTokenizerServiceImpl.class); + + private final PDVTokenizerClient pdvTokenizerClient; + + public PDVTokenizerServiceImpl() { + this.pdvTokenizerClient = PDVTokenizerClientImpl.getInstance(); + } + + PDVTokenizerServiceImpl(PDVTokenizerClient pdvTokenizerClient) { + this.pdvTokenizerClient = pdvTokenizerClient; + } + + /** + * {@inheritDoc} + */ + @Override + public String getToken(String fiscalCode) throws JsonProcessingException, PDVTokenizerException { + logger.debug("PDV Tokenizer getToken called"); + PiiResource piiResource = PiiResource.builder().pii(fiscalCode).build(); + String tokenizerBody = ObjectMapperUtils.writeValueAsString(piiResource); + + HttpResponse httpResponse = pdvTokenizerClient.searchTokenByPII(tokenizerBody); + + handleErrorResponse(httpResponse, "getToken"); + TokenResource tokenResource = ObjectMapperUtils.mapString(httpResponse.body(), TokenResource.class); + logger.debug("PDV Tokenizer getToken invocation completed"); + return tokenResource.getToken(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getFiscalCode(String token) throws PDVTokenizerException, JsonProcessingException { + logger.debug("PDV Tokenizer getFiscalCode called"); + HttpResponse httpResponse = pdvTokenizerClient.findPIIByToken(token); + + handleErrorResponse(httpResponse, "getFiscalCode"); + PiiResource piiResource = ObjectMapperUtils.mapString(httpResponse.body(), PiiResource.class); + logger.debug("PDV Tokenizer getFiscalCode invocation completed"); + return piiResource.getPii(); + } + + /** + * {@inheritDoc} + */ + @Override + public String generateTokenForFiscalCode(String fiscalCode) throws PDVTokenizerException, JsonProcessingException { + logger.debug("PDV Tokenizer generateTokenForFiscalCode called"); + PiiResource piiResource = PiiResource.builder().pii(fiscalCode).build(); + String tokenizerBody = ObjectMapperUtils.writeValueAsString(piiResource); + + HttpResponse httpResponse = pdvTokenizerClient.createToken(tokenizerBody); + + if (httpResponse.statusCode() == HttpStatus.SC_BAD_REQUEST + || httpResponse.statusCode() == HttpStatus.SC_INTERNAL_SERVER_ERROR) { + ErrorResponse response = ObjectMapperUtils.mapString(httpResponse.body(), ErrorResponse.class); + String errMsg = String.format("PDV Tokenizer generateTokenForFiscalCode invocation failed with status %s and message: %s. Error description: %s (%s)", + response.getStatus(), response.getTitle(), response.getDetail(), response.getType()); + throw new PDVTokenizerException(errMsg, response.getStatus()); + } + if (httpResponse.statusCode() != HttpStatus.SC_OK) { + ErrorMessage response = ObjectMapperUtils.mapString(httpResponse.body(), ErrorMessage.class); + String errMsg = String.format("PDV Tokenizer generateTokenForFiscalCode invocation failed with status %s and message: %s.", + httpResponse.statusCode(), response.getMessage()); + throw new PDVTokenizerException(errMsg, httpResponse.statusCode()); + } + TokenResource tokenResource = ObjectMapperUtils.mapString(httpResponse.body(), TokenResource.class); + logger.debug("PDV Tokenizer generateTokenForFiscalCode invocation completed"); + return tokenResource.getToken(); + } + + private void handleErrorResponse(HttpResponse httpResponse, String serviceName) throws JsonProcessingException, PDVTokenizerException { + if (httpResponse.statusCode() == HttpStatus.SC_BAD_REQUEST + || httpResponse.statusCode() == HttpStatus.SC_INTERNAL_SERVER_ERROR + || httpResponse.statusCode() == HttpStatus.SC_NOT_FOUND) { + ErrorResponse response = ObjectMapperUtils.mapString(httpResponse.body(), ErrorResponse.class); + String errMsg = String.format("PDV Tokenizer %s invocation failed with status %s and message: %s. Error description: %s (%s)", + serviceName, response.getStatus(), response.getTitle(), response.getDetail(), response.getType()); + throw new PDVTokenizerException(errMsg, response.getStatus()); + } + if (httpResponse.statusCode() != HttpStatus.SC_OK) { + ErrorMessage response = ObjectMapperUtils.mapString(httpResponse.body(), ErrorMessage.class); + String errMsg = String.format("PDV Tokenizer %s invocation failed with status %s and message: %s.", + serviceName, httpResponse.statusCode(), response.getMessage()); + throw new PDVTokenizerException(errMsg, httpResponse.statusCode()); + } + } +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/service/impl/PDVTokenizerServiceRetryWrapperImpl.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/service/impl/PDVTokenizerServiceRetryWrapperImpl.java new file mode 100644 index 0000000..b2154de --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/service/impl/PDVTokenizerServiceRetryWrapperImpl.java @@ -0,0 +1,85 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.service.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.github.resilience4j.core.IntervalFunction; +import io.github.resilience4j.core.functions.CheckedFunction; +import io.github.resilience4j.retry.Retry; +import io.github.resilience4j.retry.RetryConfig; +import io.github.resilience4j.retry.RetryRegistry; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.PDVTokenizerException; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.PDVTokenizerUnexpectedException; +import it.gov.pagopa.receipt.pdf.helpdesk.service.PDVTokenizerService; +import it.gov.pagopa.receipt.pdf.helpdesk.service.PDVTokenizerServiceRetryWrapper; + +/** + * {@inheritDoc} + */ +public class PDVTokenizerServiceRetryWrapperImpl implements PDVTokenizerServiceRetryWrapper { + + private static final Long INITIAL_INTERVAL = Long.parseLong(System.getenv().getOrDefault("PDV_TOKENIZER_INITIAL_INTERVAL", "200")); + private static final Double MULTIPLIER = Double.parseDouble(System.getenv().getOrDefault("PDV_TOKENIZER_MULTIPLIER", "2.0")); + private static final Double RANDOMIZATION_FACTOR = Double.parseDouble(System.getenv().getOrDefault("PDV_TOKENIZER_RANDOMIZATION_FACTOR", "0.6")); + private static final Integer MAX_RETRIES = Integer.parseInt(System.getenv().getOrDefault("PDV_TOKENIZER_MAX_RETRIES", "3")); + + private final PDVTokenizerService pdvTokenizerService; + private final Retry retry; + + PDVTokenizerServiceRetryWrapperImpl(PDVTokenizerService pdvTokenizerService, Retry retry) { + this.pdvTokenizerService = pdvTokenizerService; + this.retry = retry; + } + + public PDVTokenizerServiceRetryWrapperImpl() { + RetryConfig config = RetryConfig.custom() + .maxAttempts(MAX_RETRIES) + .intervalFunction(IntervalFunction.ofExponentialRandomBackoff(INITIAL_INTERVAL, MULTIPLIER, RANDOMIZATION_FACTOR)) + .retryOnException(e -> (e instanceof PDVTokenizerException tokenizerException) && tokenizerException.getStatusCode() == 429) + .build(); + + RetryRegistry registry = RetryRegistry.of(config); + + this.pdvTokenizerService = new PDVTokenizerServiceImpl(); + this.retry = registry.retry("tokenizerRetry"); + } + + /** + * {@inheritDoc} + */ + @Override + public String getTokenWithRetry(String fiscalCode) throws JsonProcessingException, PDVTokenizerException { + CheckedFunction function = Retry.decorateCheckedFunction(retry, pdvTokenizerService::getToken); + return runFunction(fiscalCode, function); + } + + /** + * {@inheritDoc} + */ + @Override + public String getFiscalCodeWithRetry(String token) throws PDVTokenizerException, JsonProcessingException { + CheckedFunction function = Retry.decorateCheckedFunction(retry, pdvTokenizerService::getFiscalCode); + return runFunction(token, function); + } + + /** + * {@inheritDoc} + */ + @Override + public String generateTokenForFiscalCodeWithRetry(String fiscalCode) throws PDVTokenizerException, JsonProcessingException { + CheckedFunction function = Retry.decorateCheckedFunction(retry, pdvTokenizerService::generateTokenForFiscalCode); + return runFunction(fiscalCode, function); + } + + private String runFunction(String fiscalCode, CheckedFunction function) throws PDVTokenizerException, JsonProcessingException { + try { + return function.apply(fiscalCode); + } catch (Throwable e) { + if (e instanceof PDVTokenizerException tokenizerException) { + throw tokenizerException; + } + if (e instanceof JsonProcessingException jsonProcessingException) { + throw jsonProcessingException; + } + throw new PDVTokenizerUnexpectedException(e); + } + } +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/utils/BizEventToReceiptUtils.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/utils/BizEventToReceiptUtils.java new file mode 100644 index 0000000..e320e84 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/utils/BizEventToReceiptUtils.java @@ -0,0 +1,184 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.utils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.microsoft.azure.functions.ExecutionContext; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.event.BizEvent; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.event.Transfer; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.event.enumeration.BizEventStatusType; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.CartItem; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.EventData; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.Receipt; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.enumeration.ReceiptStatusType; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.PDVTokenizerException; +import it.gov.pagopa.receipt.pdf.helpdesk.service.BizEventToReceiptService; +import org.slf4j.Logger; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class BizEventToReceiptUtils { + + private static final String REMITTANCE_INFORMATION_REGEX = "/TXT/(.*)"; + + /** + * Creates a new instance of Receipt, using the tokenizer service to mask the PII, based on + * the provided BizEvent + * + * @param bizEvent instance of BizEvent + * @param service implementation of the BizEventToReceipt service to use + * @return generated instance of Receipt + */ + public static Receipt createReceipt(BizEvent bizEvent, BizEventToReceiptService service, Logger logger) { + Receipt receipt = new Receipt(); + + // Insert biz-event data into receipt + receipt.setId(bizEvent.getId() + UUID.randomUUID()); + receipt.setEventId(bizEvent.getId()); + + EventData eventData = new EventData(); + + try { + service.tokenizeFiscalCodes(bizEvent, receipt, eventData); + } catch (Exception e) { + logger.error("Error tokenizing receipt with bizEventId {}", bizEvent.getId(), e); + receipt.setStatus(ReceiptStatusType.FAILED); + return receipt; + } + + eventData.setTransactionCreationDate( + service.getTransactionCreationDate(bizEvent)); + eventData.setAmount(bizEvent.getPaymentInfo() != null ? + bizEvent.getPaymentInfo().getAmount() : null); + + CartItem item = new CartItem(); + item.setPayeeName(bizEvent.getCreditor() != null ? bizEvent.getCreditor().getCompanyName() : null); + item.setSubject(getItemSubject(bizEvent)); + List cartItems = Collections.singletonList(item); + eventData.setCart(cartItems); + + receipt.setEventData(eventData); + return receipt; + } + + /** + * Checks if the instance of Biz Event is in status DONE and contains all the required information to process + * in the receipt generation + * + * @param bizEvent BizEvent to validate + * @param context Function context + * @param logger Function logger + * @return boolean to determine if the proposed event is invalid + */ + public static boolean isBizEventInvalid(BizEvent bizEvent, ExecutionContext context, Logger logger) { + + if (bizEvent == null) { + logger.debug("[{}] event is null", context.getFunctionName()); + return true; + } + + if (!bizEvent.getEventStatus().equals(BizEventStatusType.DONE)) { + logger.debug("[{}] event with id {} discarded because in status {}", + context.getFunctionName(), bizEvent.getId(), bizEvent.getEventStatus()); + return true; + } + + if (bizEvent.getDebtor().getEntityUniqueIdentifierValue() == null || + bizEvent.getDebtor().getEntityUniqueIdentifierValue().equals("ANONIMO")) { + logger.debug("[{}] event with id {} discarded because debtor identifier is missing or ANONIMO", + context.getFunctionName(), bizEvent.getId()); + return true; + } + + if (bizEvent.getPaymentInfo() != null) { + String totalNotice = bizEvent.getPaymentInfo().getTotalNotice(); + + if (totalNotice != null) { + int intTotalNotice; + + try { + intTotalNotice = Integer.parseInt(totalNotice); + + } catch (NumberFormatException e) { + logger.error("[{}] event with id {} discarded because has an invalid total notice value: {}", + context.getFunctionName(), bizEvent.getId(), + totalNotice, + e); + return true; + } + + if (intTotalNotice > 1) { + logger.debug("[{}] event with id {} discarded because is part of a payment cart ({} total notice)", + context.getFunctionName(), bizEvent.getId(), + intTotalNotice); + return true; + } + } + } + + return false; + } + + public static void tokenizeReceipt(BizEventToReceiptService service, BizEvent bizEvent, Receipt receipt) + throws PDVTokenizerException, JsonProcessingException { + if (receipt.getEventData() == null) { + EventData eventData = new EventData(); + receipt.setEventData(eventData); + eventData.setTransactionCreationDate( + service.getTransactionCreationDate(bizEvent)); + eventData.setAmount(bizEvent.getPaymentInfo() != null ? + bizEvent.getPaymentInfo().getAmount() : null); + + CartItem item = new CartItem(); + item.setPayeeName(bizEvent.getCreditor() != null ? bizEvent.getCreditor().getCompanyName() : null); + item.setSubject(getItemSubject(bizEvent)); + List cartItems = Collections.singletonList(item); + eventData.setCart(cartItems); + } + service.tokenizeFiscalCodes(bizEvent, receipt, receipt.getEventData()); + } + + /** + * Retrieve RemittanceInformation from BizEvent + * + * @param bizEvent BizEvent from which retrieve the data + * @return the remittance information + */ + private static String getItemSubject(BizEvent bizEvent) { + if (bizEvent.getPaymentInfo() != null && bizEvent.getPaymentInfo().getRemittanceInformation() != null) { + return bizEvent.getPaymentInfo().getRemittanceInformation(); + } + List transferList = bizEvent.getTransferList(); + if (transferList != null && !transferList.isEmpty()) { + double amount = 0; + String remittanceInformation = null; + for (Transfer transfer : transferList) { + double transferAmount; + try { + transferAmount = Double.parseDouble(transfer.getAmount()); + } catch (Exception ignored) { + continue; + } + if (amount < transferAmount) { + amount = transferAmount; + remittanceInformation = transfer.getRemittanceInformation(); + } + } + return formatRemittanceInformation(remittanceInformation); + } + return null; + } + + private static String formatRemittanceInformation(String remittanceInformation) { + if (remittanceInformation != null) { + Pattern pattern = Pattern.compile(REMITTANCE_INFORMATION_REGEX); + Matcher matcher = pattern.matcher(remittanceInformation); + if (matcher.find()) { + return matcher.group(1); + } + } + return remittanceInformation; + } +} diff --git a/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/utils/ObjectMapperUtils.java b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/utils/ObjectMapperUtils.java new file mode 100644 index 0000000..9a37073 --- /dev/null +++ b/src/main/java/it/gov/pagopa/receipt/pdf/helpdesk/utils/ObjectMapperUtils.java @@ -0,0 +1,57 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.utils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.modelmapper.ModelMapper; +import org.modelmapper.convention.MatchingStrategies; + +public class ObjectMapperUtils { + + private static final ModelMapper modelMapper; + private static final ObjectMapper objectMapper; + + /** + * Model mapper property setting are specified in the following block. + * Default property matching strategy is set to Strict see {@link MatchingStrategies} + * Custom mappings are added using {@link ModelMapper#addMappings(PropertyMap)} + */ + static { + modelMapper = new ModelMapper(); + modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT); + objectMapper = new ObjectMapper(); + } + + /** + * Hide from public usage. + */ + private ObjectMapperUtils() { + } + + /** + * Encodes an object to a string + * + * @param value Object to be encoded + * @return encoded string + */ + public static String writeValueAsString(Object value) { + try { + return objectMapper.writeValueAsString(value); + } catch (JsonProcessingException e) { + return null; + } + } + + /** + * Maps string to object of defined Class + * + * @param string String to map + * @param outClass Class to be mapped to + * @param Defined Class + * @return object of the defined Class + */ + public static T mapString(final String string, Class outClass) throws JsonProcessingException { + return objectMapper.readValue(string, outClass); + } + + +} diff --git a/src/main/resources-filtered/application.properties b/src/main/resources-filtered/application.properties new file mode 100644 index 0000000..8a22575 --- /dev/null +++ b/src/main/resources-filtered/application.properties @@ -0,0 +1,2 @@ +version=${project.version} +name=${project.name} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 8b13789..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..1558371 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,74 @@ + + + + + + + + ${CONSOLE_LOG_THRESHOLD} + + + ${CONSOLE_LOG_PATTERN} + ${CONSOLE_LOG_CHARSET} + + + + + + + ${name} + ${version} + ${ENV} + + + > + + + true + 20000 + 0 + + + + + + + + + + + ${FILE_LOG_THRESHOLD} + + + ${FILE_LOG_PATTERN} + ${FILE_LOG_CHARSET} + + ${LOG_FILE} + + ${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz} + ${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false} + ${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB} + ${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0} + ${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-7} + + + + + true + 20000 + 0 + + + + + + + + + + + + diff --git a/src/test/java/it/gov/pagopa/project/ExampleTest.java b/src/test/java/it/gov/pagopa/project/ExampleTest.java deleted file mode 100644 index f2ca314..0000000 --- a/src/test/java/it/gov/pagopa/project/ExampleTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package it.gov.pagopa.project; - -import com.microsoft.azure.functions.ExecutionContext; -import com.microsoft.azure.functions.HttpRequestMessage; -import com.microsoft.azure.functions.HttpResponseMessage; -import com.microsoft.azure.functions.HttpStatus; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.Spy; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Optional; -import java.util.logging.Logger; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class ExampleTest { - - @Spy - Example function; - - @Mock - ExecutionContext context; - - @Test - void runOk() { - // test precondition - Logger logger = Logger.getLogger("example-test-logger"); - when(context.getLogger()).thenReturn(logger); - - final HttpResponseMessage.Builder builder = mock(HttpResponseMessage.Builder.class); - HttpRequestMessage> request = mock(HttpRequestMessage.class); - - doReturn(builder).when(request).createResponseBuilder(any(HttpStatus.class)); - doReturn(builder).when(builder).header(anyString(), anyString()); - doReturn(builder).when(builder).body(anyString()); - - HttpResponseMessage responseMock = mock(HttpResponseMessage.class); - doReturn(HttpStatus.OK).when(responseMock).getStatus(); - doReturn(responseMock).when(builder).build(); - - // test execution - HttpResponseMessage response = function.run(request, context); - - // test assertion - assertEquals(HttpStatus.OK, response.getStatus()); - } -} diff --git a/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/HealthTest.java b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/HealthTest.java new file mode 100644 index 0000000..4972a63 --- /dev/null +++ b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/HealthTest.java @@ -0,0 +1,49 @@ +package it.gov.pagopa.receipt.pdf.helpdesk; + +import com.microsoft.azure.functions.ExecutionContext; +import com.microsoft.azure.functions.HttpRequestMessage; +import com.microsoft.azure.functions.HttpResponseMessage; +import com.microsoft.azure.functions.HttpStatus; +import it.gov.pagopa.receipt.pdf.helpdesk.util.HttpResponseMessageMock; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +class HealthTest { + + @Mock + ExecutionContext executionContextMock; + + @Spy + Health sut; + + @Test + void runOK() { + @SuppressWarnings("unchecked") + HttpRequestMessage> request = mock(HttpRequestMessage.class); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage response = sut.run(request, executionContextMock); + + // test assertion + assertNotNull(response); + assertEquals(HttpStatus.OK, response.getStatus()); + } +} diff --git a/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/InfoTest.java b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/InfoTest.java new file mode 100644 index 0000000..ec16ec2 --- /dev/null +++ b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/InfoTest.java @@ -0,0 +1,57 @@ +package it.gov.pagopa.receipt.pdf.helpdesk; + +import com.microsoft.azure.functions.ExecutionContext; +import com.microsoft.azure.functions.HttpRequestMessage; +import com.microsoft.azure.functions.HttpResponseMessage; +import com.microsoft.azure.functions.HttpStatus; +import it.gov.pagopa.receipt.pdf.helpdesk.model.AppInfo; +import it.gov.pagopa.receipt.pdf.helpdesk.util.HttpResponseMessageMock; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +class InfoTest { + + @Mock + ExecutionContext executionContextMock; + + @Spy + Info sut; + + @Test + void runOK() { + @SuppressWarnings("unchecked") + HttpRequestMessage> request = mock(HttpRequestMessage.class); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage response = sut.run(request, executionContextMock); + + // test assertion + assertNotNull(response); + assertEquals(HttpStatus.OK, response.getStatus()); + assertNotNull(response.getBody()); + AppInfo responseBody = (AppInfo) response.getBody(); + assertNotNull(responseBody.getName()); + assertNotNull(responseBody.getVersion()); + assertNotNull(responseBody.getEnvironment()); + assertEquals("pagopa-receipt-pdf-helpdesk", responseBody.getName()); + assertEquals("azure-fn", responseBody.getEnvironment()); + } +} diff --git a/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/RecoverFailedReceiptTest.java b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/RecoverFailedReceiptTest.java new file mode 100644 index 0000000..3079216 --- /dev/null +++ b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/RecoverFailedReceiptTest.java @@ -0,0 +1,700 @@ +package it.gov.pagopa.receipt.pdf.helpdesk; + +import com.azure.core.http.rest.Response; +import com.azure.cosmos.models.ModelBridgeInternal; +import com.azure.storage.queue.models.SendMessageResult; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.microsoft.azure.functions.*; +import it.gov.pagopa.receipt.pdf.helpdesk.client.ReceiptCosmosClient; +import it.gov.pagopa.receipt.pdf.helpdesk.client.ReceiptQueueClient; +import it.gov.pagopa.receipt.pdf.helpdesk.client.impl.BizEventCosmosClientImpl; +import it.gov.pagopa.receipt.pdf.helpdesk.client.impl.ReceiptCosmosClientImpl; +import it.gov.pagopa.receipt.pdf.helpdesk.client.impl.ReceiptQueueClientImpl; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.event.*; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.event.enumeration.BizEventStatusType; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.CartItem; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.EventData; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.Receipt; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.enumeration.ReceiptStatusType; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.BizEventNotFoundException; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.PDVTokenizerException; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.ReceiptNotFoundException; +import it.gov.pagopa.receipt.pdf.helpdesk.model.ReceiptFailedRecoveryRequest; +import it.gov.pagopa.receipt.pdf.helpdesk.service.PDVTokenizerServiceRetryWrapper; +import it.gov.pagopa.receipt.pdf.helpdesk.service.impl.BizEventToReceiptServiceImpl; +import it.gov.pagopa.receipt.pdf.helpdesk.util.HttpResponseMessageMock; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; + +import java.lang.reflect.Field; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static com.microsoft.azure.functions.HttpStatus.BAD_REQUEST; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class RecoverFailedReceiptTest { + + private final String PAYER_FISCAL_CODE = "a valid payer CF"; + private final String DEBTOR_FISCAL_CODE = "a valid debtor CF"; + private final String TOKENIZED_DEBTOR_FISCAL_CODE = "tokenizedDebtorFiscalCode"; + private final String TOKENIZED_PAYER_FISCAL_CODE = "tokenizedPayerFiscalCode"; + private final String EVENT_ID = "a valid id"; + + public static final String HTTP_MESSAGE_ERROR = "an error occured"; + + + private RecoverFailedReceipt function; + + @Mock + private ExecutionContext context; + @Mock + private PDVTokenizerServiceRetryWrapper pdvTokenizerServiceMock; + @Mock + private ReceiptCosmosClient receiptCosmosClient; + @Mock + private ReceiptQueueClient queueClient; + @Mock + private BizEventCosmosClientImpl bizEventCosmosClientMock; + + @Captor + private ArgumentCaptor> receiptCaptor; + + @Test + void requestOnValidBizEventShouldCreateRequest() throws PDVTokenizerException, JsonProcessingException, + ReceiptNotFoundException, BizEventNotFoundException { + when(pdvTokenizerServiceMock.generateTokenForFiscalCodeWithRetry(DEBTOR_FISCAL_CODE)) + .thenReturn(TOKENIZED_DEBTOR_FISCAL_CODE); + when(pdvTokenizerServiceMock.generateTokenForFiscalCodeWithRetry(PAYER_FISCAL_CODE)) + .thenReturn(TOKENIZED_PAYER_FISCAL_CODE); + + Response response = mock(Response.class); + when(response.getStatusCode()).thenReturn(HttpStatus.CREATED.value()); + when(queueClient.sendMessageToQueue(anyString())).thenReturn(response); + + BizEventToReceiptServiceImpl receiptService = new BizEventToReceiptServiceImpl(pdvTokenizerServiceMock, queueClient); + + when(bizEventCosmosClientMock.getBizEventDocument(Mockito.eq("1"))) + .thenReturn(generateValidBizEvent("1")); + + when(receiptCosmosClient.getReceiptDocument(Mockito.eq("1"))).thenThrow(ReceiptNotFoundException.class); + + function = new RecoverFailedReceipt(receiptService, bizEventCosmosClientMock, receiptCosmosClient); + + @SuppressWarnings("unchecked") + OutputBinding> documentdb = (OutputBinding>) spy(OutputBinding.class); + + ReceiptFailedRecoveryRequest receiptFailedRecoveryRequest = new ReceiptFailedRecoveryRequest(); + receiptFailedRecoveryRequest.setEventId("1"); + + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(receiptFailedRecoveryRequest)); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + assertDoesNotThrow(() -> function.run(request, documentdb, context)); + + verify(documentdb).setValue(receiptCaptor.capture()); + Receipt captured = receiptCaptor.getValue().get(0); + assertEquals(ReceiptStatusType.INSERTED, captured.getStatus()); + assertEquals(EVENT_ID, captured.getEventId()); + assertEquals(TOKENIZED_PAYER_FISCAL_CODE, captured.getEventData().getPayerFiscalCode()); + assertEquals(TOKENIZED_DEBTOR_FISCAL_CODE, captured.getEventData().getDebtorFiscalCode()); + assertNotNull(captured.getEventData().getCart()); + assertEquals(1, captured.getEventData().getCart().size()); + } + + @Test + void requestOnValidBizEventAndFailedReceiptShouldResend() throws + ReceiptNotFoundException, BizEventNotFoundException { + when(receiptCosmosClient.getReceiptDocument(Mockito.eq("1"))).thenReturn(createFailedReceipt("1")); + + Response response = mock(Response.class); + when(response.getStatusCode()).thenReturn(HttpStatus.CREATED.value()); + when(queueClient.sendMessageToQueue(anyString())).thenReturn(response); + BizEventToReceiptServiceImpl receiptService = new BizEventToReceiptServiceImpl(pdvTokenizerServiceMock, queueClient); + + when(bizEventCosmosClientMock.getBizEventDocument(Mockito.eq("1"))) + .thenReturn(generateValidBizEvent("1")); + + function = new RecoverFailedReceipt(receiptService, bizEventCosmosClientMock, receiptCosmosClient); + + @SuppressWarnings("unchecked") + OutputBinding> documentdb = (OutputBinding>) spy(OutputBinding.class); + + ReceiptFailedRecoveryRequest receiptFailedRecoveryRequest = new ReceiptFailedRecoveryRequest(); + receiptFailedRecoveryRequest.setEventId("1"); + + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(receiptFailedRecoveryRequest)); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + assertDoesNotThrow(() -> function.run(request, documentdb, context)); + + verify(documentdb).setValue(receiptCaptor.capture()); + Receipt captured = receiptCaptor.getValue().get(0); + assertEquals(ReceiptStatusType.INSERTED, captured.getStatus()); + assertEquals("1", captured.getEventId()); + assertEquals(TOKENIZED_PAYER_FISCAL_CODE, captured.getEventData().getPayerFiscalCode()); + assertEquals(TOKENIZED_DEBTOR_FISCAL_CODE, captured.getEventData().getDebtorFiscalCode()); + assertNotNull(captured.getEventData().getCart()); + assertEquals(1, captured.getEventData().getCart().size()); + } + + + @Test + void requestOnValidBizEventAndFailedReceiptListShouldResend() throws BizEventNotFoundException { + ReceiptCosmosClientImpl receiptCosmosClient = mock(ReceiptCosmosClientImpl.class); + when(receiptCosmosClient.getFailedReceiptDocuments(Mockito.any(),Mockito.any())).thenReturn( + Collections.singletonList(ModelBridgeInternal + .createFeedResponse(Collections.singletonList(createFailedReceipt("1")), + Collections.emptyMap()))); + + Response response = mock(Response.class); + when(response.getStatusCode()).thenReturn(HttpStatus.CREATED.value()); + when(queueClient.sendMessageToQueue(anyString())).thenReturn(response); + + BizEventToReceiptServiceImpl receiptService = new BizEventToReceiptServiceImpl(pdvTokenizerServiceMock, queueClient); + + when(bizEventCosmosClientMock.getBizEventDocument(Mockito.eq("1"))) + .thenReturn(generateValidBizEvent("1")); + + function = new RecoverFailedReceipt(receiptService, bizEventCosmosClientMock, receiptCosmosClient); + + @SuppressWarnings("unchecked") + OutputBinding> documentdb = (OutputBinding>) spy(OutputBinding.class); + + ReceiptFailedRecoveryRequest receiptFailedRecoveryRequest = new ReceiptFailedRecoveryRequest(); + + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(receiptFailedRecoveryRequest)); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + assertDoesNotThrow(() -> function.run(request, documentdb, context)); + + verify(documentdb).setValue(receiptCaptor.capture()); + Receipt captured = receiptCaptor.getValue().get(0); + assertEquals(ReceiptStatusType.INSERTED, captured.getStatus()); + assertEquals("1", captured.getEventId()); + assertEquals(TOKENIZED_PAYER_FISCAL_CODE, captured.getEventData().getPayerFiscalCode()); + assertEquals(TOKENIZED_DEBTOR_FISCAL_CODE, captured.getEventData().getDebtorFiscalCode()); + assertNotNull(captured.getEventData().getCart()); + assertEquals(1, captured.getEventData().getCart().size()); + } + + @Test + void requestOnValidBizEventAndFailedReceiptWithMissingFiscalCodeTokenShouldUpdateWithToken() throws PDVTokenizerException, JsonProcessingException, + ReceiptNotFoundException, BizEventNotFoundException { + when(pdvTokenizerServiceMock.generateTokenForFiscalCodeWithRetry(DEBTOR_FISCAL_CODE)) + .thenReturn(TOKENIZED_DEBTOR_FISCAL_CODE); + when(pdvTokenizerServiceMock.generateTokenForFiscalCodeWithRetry(PAYER_FISCAL_CODE)) + .thenReturn(TOKENIZED_PAYER_FISCAL_CODE); + + Response response = mock(Response.class); + when(response.getStatusCode()).thenReturn(HttpStatus.CREATED.value()); + when(queueClient.sendMessageToQueue(anyString())).thenReturn(response); + + BizEventToReceiptServiceImpl receiptService = new BizEventToReceiptServiceImpl(pdvTokenizerServiceMock, queueClient); + + Receipt receipt = createFailedReceipt("1"); + receipt.getEventData().setPayerFiscalCode(null); + receipt.getEventData().setDebtorFiscalCode(null); + when(receiptCosmosClient.getReceiptDocument(Mockito.eq("1"))).thenReturn(receipt); + + when(bizEventCosmosClientMock.getBizEventDocument(Mockito.eq("1"))) + .thenReturn(generateValidBizEvent("1")); + + function = new RecoverFailedReceipt(receiptService, bizEventCosmosClientMock, receiptCosmosClient); + + @SuppressWarnings("unchecked") + OutputBinding> documentdb = (OutputBinding>) spy(OutputBinding.class); + + ReceiptFailedRecoveryRequest receiptFailedRecoveryRequest = new ReceiptFailedRecoveryRequest(); + receiptFailedRecoveryRequest.setEventId("1"); + + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(receiptFailedRecoveryRequest)); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + assertDoesNotThrow(() -> function.run(request, documentdb, context)); + + verify(documentdb).setValue(receiptCaptor.capture()); + Receipt captured = receiptCaptor.getValue().get(0); + assertEquals(ReceiptStatusType.INSERTED, captured.getStatus()); + assertEquals("1", captured.getEventId()); + assertEquals(TOKENIZED_PAYER_FISCAL_CODE, captured.getEventData().getPayerFiscalCode()); + assertEquals(TOKENIZED_DEBTOR_FISCAL_CODE, captured.getEventData().getDebtorFiscalCode()); + assertNotNull(captured.getEventData().getCart()); + assertEquals(1, captured.getEventData().getCart().size()); + } + + @Test + void requestOnValidBizEventAndFailedReceiptWithoutEventDataShouldUpdateWithToken() throws PDVTokenizerException, JsonProcessingException, + ReceiptNotFoundException, BizEventNotFoundException { + when(pdvTokenizerServiceMock.generateTokenForFiscalCodeWithRetry(DEBTOR_FISCAL_CODE)) + .thenReturn(TOKENIZED_DEBTOR_FISCAL_CODE); + when(pdvTokenizerServiceMock.generateTokenForFiscalCodeWithRetry(PAYER_FISCAL_CODE)) + .thenReturn(TOKENIZED_PAYER_FISCAL_CODE); + + Response response = mock(Response.class); + when(response.getStatusCode()).thenReturn(HttpStatus.CREATED.value()); + when(queueClient.sendMessageToQueue(anyString())).thenReturn(response); + + BizEventToReceiptServiceImpl receiptService = new BizEventToReceiptServiceImpl(pdvTokenizerServiceMock, queueClient); + + Receipt receipt = createFailedReceipt("1"); + receipt.setEventData(null); + when(receiptCosmosClient.getReceiptDocument(Mockito.eq("1"))).thenReturn(receipt); + + when(bizEventCosmosClientMock.getBizEventDocument(Mockito.eq("1"))) + .thenReturn(generateValidBizEvent("1")); + + function = new RecoverFailedReceipt(receiptService, bizEventCosmosClientMock, receiptCosmosClient); + + @SuppressWarnings("unchecked") + OutputBinding> documentdb = (OutputBinding>) spy(OutputBinding.class); + + ReceiptFailedRecoveryRequest receiptFailedRecoveryRequest = new ReceiptFailedRecoveryRequest(); + receiptFailedRecoveryRequest.setEventId("1"); + + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(receiptFailedRecoveryRequest)); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + assertDoesNotThrow(() -> function.run(request, documentdb, context)); + + verify(documentdb).setValue(receiptCaptor.capture()); + Receipt captured = receiptCaptor.getValue().get(0); + assertEquals(ReceiptStatusType.INSERTED, captured.getStatus()); + assertEquals("1", captured.getEventId()); + assertEquals(TOKENIZED_PAYER_FISCAL_CODE, captured.getEventData().getPayerFiscalCode()); + assertEquals(TOKENIZED_DEBTOR_FISCAL_CODE, captured.getEventData().getDebtorFiscalCode()); + assertNotNull(captured.getEventData().getCart()); + assertEquals(1, captured.getEventData().getCart().size()); + } + + @Test + void requestWithMissingBizEventOnRequestIdShouldReturnBadRequest() throws BizEventNotFoundException { + + BizEventToReceiptServiceImpl receiptService = new BizEventToReceiptServiceImpl(pdvTokenizerServiceMock, queueClient); + + when(bizEventCosmosClientMock.getBizEventDocument(Mockito.eq("1"))) + .thenThrow(BizEventNotFoundException.class); + + function = new RecoverFailedReceipt(receiptService, bizEventCosmosClientMock, receiptCosmosClient); + + @SuppressWarnings("unchecked") + OutputBinding> documentdb = (OutputBinding>) spy(OutputBinding.class); + + ReceiptFailedRecoveryRequest receiptFailedRecoveryRequest = new ReceiptFailedRecoveryRequest(); + receiptFailedRecoveryRequest.setEventId("1"); + + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(receiptFailedRecoveryRequest)); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage httpResponseMessage = assertDoesNotThrow(() -> function.run(request, documentdb, context)); + + assertNotNull(httpResponseMessage); + assertEquals(BAD_REQUEST.value(), httpResponseMessage.getStatus().value()); + + } + + @Test + void runDiscardedWithEventNotDONE() throws BizEventNotFoundException { + + + BizEventToReceiptServiceImpl receiptService = new BizEventToReceiptServiceImpl(pdvTokenizerServiceMock, queueClient); + + when(bizEventCosmosClientMock.getBizEventDocument(Mockito.eq("1"))) + .thenReturn(generateNotDoneBizEvent()); + + function = new RecoverFailedReceipt(receiptService, bizEventCosmosClientMock, receiptCosmosClient); + + @SuppressWarnings("unchecked") + OutputBinding> documentdb = (OutputBinding>) spy(OutputBinding.class); + + ReceiptFailedRecoveryRequest receiptFailedRecoveryRequest = new ReceiptFailedRecoveryRequest(); + receiptFailedRecoveryRequest.setEventId("1"); + + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(receiptFailedRecoveryRequest)); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage httpResponseMessage = assertDoesNotThrow(() -> function.run(request, documentdb, context)); + + assertNotNull(httpResponseMessage); + verifyNoInteractions(receiptCosmosClient); + verifyNoInteractions(queueClient); + + } + + @Test + void generateAnonymDebtorBizEvent() throws BizEventNotFoundException { + + + BizEventToReceiptServiceImpl receiptService = new BizEventToReceiptServiceImpl(pdvTokenizerServiceMock, queueClient); + + when(bizEventCosmosClientMock.getBizEventDocument(Mockito.eq("1"))) + .thenReturn(generateAnonymDebtorBizEvent("1")); + + function = new RecoverFailedReceipt(receiptService, bizEventCosmosClientMock, receiptCosmosClient); + + @SuppressWarnings("unchecked") + OutputBinding> documentdb = (OutputBinding>) spy(OutputBinding.class); + + ReceiptFailedRecoveryRequest receiptFailedRecoveryRequest = new ReceiptFailedRecoveryRequest(); + receiptFailedRecoveryRequest.setEventId("1"); + + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(receiptFailedRecoveryRequest)); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage httpResponseMessage = assertDoesNotThrow(() -> function.run(request, documentdb, context)); + + assertNotNull(httpResponseMessage); + verifyNoInteractions(receiptCosmosClient); + verifyNoInteractions(queueClient); + + } + + @Test + void runDiscardedWithEventNull() throws BizEventNotFoundException { + + BizEventToReceiptServiceImpl receiptService = new BizEventToReceiptServiceImpl(pdvTokenizerServiceMock, queueClient); + + when(bizEventCosmosClientMock.getBizEventDocument(Mockito.eq("1"))) + .thenReturn(null); + + function = new RecoverFailedReceipt(receiptService, bizEventCosmosClientMock, receiptCosmosClient); + + @SuppressWarnings("unchecked") + OutputBinding> documentdb = (OutputBinding>) spy(OutputBinding.class); + + ReceiptFailedRecoveryRequest receiptFailedRecoveryRequest = new ReceiptFailedRecoveryRequest(); + receiptFailedRecoveryRequest.setEventId("1"); + + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(receiptFailedRecoveryRequest)); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage httpResponseMessage = assertDoesNotThrow(() -> function.run(request, documentdb, context)); + + assertNotNull(httpResponseMessage); + verifyNoInteractions(receiptCosmosClient); + verifyNoInteractions(queueClient); + + } + + @Test + void runDiscardedWithCartEvent() throws BizEventNotFoundException { + + BizEventToReceiptServiceImpl receiptService = new BizEventToReceiptServiceImpl(pdvTokenizerServiceMock, queueClient); + + when(bizEventCosmosClientMock.getBizEventDocument(Mockito.eq("1"))) + .thenReturn(generateValidBizEvent("2")); + + function = new RecoverFailedReceipt(receiptService, bizEventCosmosClientMock, receiptCosmosClient); + + @SuppressWarnings("unchecked") + OutputBinding> documentdb = (OutputBinding>) spy(OutputBinding.class); + + ReceiptFailedRecoveryRequest receiptFailedRecoveryRequest = new ReceiptFailedRecoveryRequest(); + receiptFailedRecoveryRequest.setEventId("1"); + + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(receiptFailedRecoveryRequest)); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage httpResponseMessage = assertDoesNotThrow(() -> function.run(request, documentdb, context)); + + assertNotNull(httpResponseMessage); + verifyNoInteractions(receiptCosmosClient); + verifyNoInteractions(queueClient); + + } + + @Test + void runDiscardedWithCartEventWithInvalidTotalNotice() throws BizEventNotFoundException { + + BizEventToReceiptServiceImpl receiptService = new BizEventToReceiptServiceImpl(pdvTokenizerServiceMock, queueClient); + + when(bizEventCosmosClientMock.getBizEventDocument(Mockito.eq("1"))) + .thenReturn(generateValidBizEvent("invalid string")); + + function = new RecoverFailedReceipt(receiptService, bizEventCosmosClientMock, receiptCosmosClient); + + @SuppressWarnings("unchecked") + OutputBinding> documentdb = (OutputBinding>) spy(OutputBinding.class); + + ReceiptFailedRecoveryRequest receiptFailedRecoveryRequest = new ReceiptFailedRecoveryRequest(); + receiptFailedRecoveryRequest.setEventId("1"); + + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(receiptFailedRecoveryRequest)); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage httpResponseMessage = assertDoesNotThrow(() -> function.run(request, documentdb, context)); + + assertNotNull(httpResponseMessage); + verifyNoInteractions(receiptCosmosClient); + verifyNoInteractions(queueClient); + + } + + @Test + void errorTokenizingFiscalCodes() throws PDVTokenizerException, JsonProcessingException, BizEventNotFoundException { + lenient().when(pdvTokenizerServiceMock.generateTokenForFiscalCodeWithRetry(DEBTOR_FISCAL_CODE)) + .thenThrow(new PDVTokenizerException(HTTP_MESSAGE_ERROR, org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR)); + + when(bizEventCosmosClientMock.getBizEventDocument(Mockito.eq("1"))) + .thenReturn(generateValidBizEvent("1")); + + BizEventToReceiptServiceImpl receiptService = new BizEventToReceiptServiceImpl(pdvTokenizerServiceMock, queueClient); + function = new RecoverFailedReceipt(receiptService, bizEventCosmosClientMock, receiptCosmosClient); + + @SuppressWarnings("unchecked") + OutputBinding> documentdb = (OutputBinding>) spy(OutputBinding.class); + + ReceiptFailedRecoveryRequest receiptFailedRecoveryRequest = new ReceiptFailedRecoveryRequest(); + receiptFailedRecoveryRequest.setEventId("1"); + + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(receiptFailedRecoveryRequest)); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + HttpResponseMessage httpResponseMessage = assertDoesNotThrow(() -> function.run(request, documentdb, context)); + + assertNotNull(httpResponseMessage); + verifyNoInteractions(queueClient); + + } + + @Test + void errorAddingMessageToQueue() throws PDVTokenizerException, JsonProcessingException, BizEventNotFoundException, ReceiptNotFoundException { + + when(pdvTokenizerServiceMock.generateTokenForFiscalCodeWithRetry(DEBTOR_FISCAL_CODE)) + .thenReturn(TOKENIZED_DEBTOR_FISCAL_CODE); + when(pdvTokenizerServiceMock.generateTokenForFiscalCodeWithRetry(PAYER_FISCAL_CODE)) + .thenReturn(TOKENIZED_PAYER_FISCAL_CODE); + + Response response = mock(Response.class); + when(response.getStatusCode()).thenReturn(HttpStatus.FORBIDDEN.value()); + when(queueClient.sendMessageToQueue(anyString())).thenReturn(response); + + BizEventToReceiptServiceImpl receiptService = new BizEventToReceiptServiceImpl(pdvTokenizerServiceMock, queueClient); + + Receipt receipt = createFailedReceipt("1"); + receipt.setEventData(null); + when(receiptCosmosClient.getReceiptDocument(Mockito.eq("1"))).thenReturn(receipt); + + when(bizEventCosmosClientMock.getBizEventDocument(Mockito.eq("1"))) + .thenReturn(generateValidBizEvent("1")); + + function = new RecoverFailedReceipt(receiptService, bizEventCosmosClientMock, receiptCosmosClient); + + @SuppressWarnings("unchecked") + OutputBinding> documentdb = (OutputBinding>) spy(OutputBinding.class); + + ReceiptFailedRecoveryRequest receiptFailedRecoveryRequest = new ReceiptFailedRecoveryRequest(); + receiptFailedRecoveryRequest.setEventId("1"); + + HttpRequestMessage> request = mock(HttpRequestMessage.class); + when(request.getBody()).thenReturn(Optional.of(receiptFailedRecoveryRequest)); + + doAnswer((Answer) invocation -> { + HttpStatus status = (HttpStatus) invocation.getArguments()[0]; + return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status); + }).when(request).createResponseBuilder(any(HttpStatus.class)); + + // test execution + assertDoesNotThrow(() -> function.run(request, documentdb, context)); + + + } + + + private static void setMock(ReceiptQueueClientImpl mock) { + try { + Field instance = ReceiptQueueClientImpl.class.getDeclaredField("instance"); + instance.setAccessible(true); + instance.set(instance, mock); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static void setMock(ReceiptCosmosClientImpl mock) { + try { + Field instance = ReceiptCosmosClientImpl.class.getDeclaredField("instance"); + instance.setAccessible(true); + instance.set(instance, mock); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static void setMock(BizEventCosmosClientImpl mock) { + try { + Field instance = BizEventCosmosClientImpl.class.getDeclaredField("instance"); + instance.setAccessible(true); + instance.set(instance, mock); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private BizEvent generateValidBizEvent(String totalNotice){ + BizEvent item = new BizEvent(); + + Payer payer = new Payer(); + payer.setEntityUniqueIdentifierValue(PAYER_FISCAL_CODE); + Debtor debtor = new Debtor(); + debtor.setEntityUniqueIdentifierValue(DEBTOR_FISCAL_CODE); + + TransactionDetails transactionDetails = new TransactionDetails(); + Transaction transaction = new Transaction(); + transaction.setCreationDate(String.valueOf(LocalDateTime.now())); + transactionDetails.setTransaction(transaction); + + PaymentInfo paymentInfo = new PaymentInfo(); + paymentInfo.setTotalNotice(totalNotice); + + item.setEventStatus(BizEventStatusType.DONE); + item.setId(EVENT_ID); + item.setPayer(payer); + item.setDebtor(debtor); + item.setTransactionDetails(transactionDetails); + item.setPaymentInfo(paymentInfo); + + return item; + } + + private Receipt createFailedReceipt(String eventId) { + Receipt receipt = new Receipt(); + + receipt.setId(eventId); + receipt.setEventId(eventId); + + receipt.setVersion("1"); + + receipt.setStatus(ReceiptStatusType.FAILED); + EventData eventData = new EventData(); + eventData.setDebtorFiscalCode(TOKENIZED_DEBTOR_FISCAL_CODE); + eventData.setPayerFiscalCode(TOKENIZED_PAYER_FISCAL_CODE); + receipt.setEventData(eventData); + + CartItem item = new CartItem(); + List cartItems = Collections.singletonList(item); + eventData.setCart(cartItems); + + return receipt; + } + + private BizEvent generateAnonymDebtorBizEvent(String totalNotice){ + BizEvent item = new BizEvent(); + + Payer payer = new Payer(); + payer.setEntityUniqueIdentifierValue(PAYER_FISCAL_CODE); + Debtor debtor = new Debtor(); + debtor.setEntityUniqueIdentifierValue("ANONIMO"); + + TransactionDetails transactionDetails = new TransactionDetails(); + Transaction transaction = new Transaction(); + transaction.setCreationDate(String.valueOf(LocalDateTime.now())); + transactionDetails.setTransaction(transaction); + + PaymentInfo paymentInfo = new PaymentInfo(); + paymentInfo.setTotalNotice(totalNotice); + + item.setEventStatus(BizEventStatusType.DONE); + item.setId(EVENT_ID); + item.setPayer(payer); + item.setDebtor(debtor); + item.setTransactionDetails(transactionDetails); + item.setPaymentInfo(paymentInfo); + + return item; + } + + + private BizEvent generateNotDoneBizEvent(){ + BizEvent item = new BizEvent(); + + item.setEventStatus(BizEventStatusType.NA); + + return item; + } + +} \ No newline at end of file diff --git a/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/BizEventCosmosClientImplTest.java b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/BizEventCosmosClientImplTest.java new file mode 100644 index 0000000..e9206f7 --- /dev/null +++ b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/BizEventCosmosClientImplTest.java @@ -0,0 +1,92 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.client.impl; + +import com.azure.cosmos.CosmosClient; +import com.azure.cosmos.CosmosContainer; +import com.azure.cosmos.CosmosDatabase; +import com.azure.cosmos.util.CosmosPagedIterable; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.event.BizEvent; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.Receipt; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.BizEventNotFoundException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Iterator; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static uk.org.webcompere.systemstubs.SystemStubs.withEnvironmentVariables; + +class BizEventCosmosClientImplTest { + + @Test + void testSingletonConnectionError() throws Exception { + String mockKey = "mockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeyMK=="; + withEnvironmentVariables( + "COSMOS_BIZ_EVENT_KEY", mockKey, + "COSMOS_BIZ_EVENT_SERVICE_ENDPOINT", "" + ).execute(() -> Assertions.assertThrows(IllegalArgumentException.class, BizEventCosmosClientImpl::getInstance) + ); + } + + @Test + void runOk() throws BizEventNotFoundException { + String BIZ_EVENT_ID = "a valid event id"; + + CosmosClient mockClient = mock(CosmosClient.class); + + CosmosDatabase mockDatabase = mock(CosmosDatabase.class); + CosmosContainer mockContainer = mock(CosmosContainer.class); + + CosmosPagedIterable mockIterable = mock(CosmosPagedIterable.class); + + Iterator mockIterator = mock(Iterator.class); + BizEvent bizEvent = new BizEvent(); + bizEvent.setId(BIZ_EVENT_ID); + + when(mockIterator.hasNext()).thenReturn(true); + when(mockIterator.next()).thenReturn(bizEvent); + + when(mockIterable.iterator()).thenReturn(mockIterator); + + when(mockContainer.queryItems(anyString(), any(), eq(BizEvent.class))).thenReturn( + mockIterable + ); + when(mockDatabase.getContainer(any())).thenReturn(mockContainer); + when(mockClient.getDatabase(any())).thenReturn(mockDatabase); + + BizEventCosmosClientImpl client = new BizEventCosmosClientImpl(mockClient); + + Assertions.assertDoesNotThrow(() -> client.getBizEventDocument(BIZ_EVENT_ID)); + + BizEvent bizEventResponse = client.getBizEventDocument(BIZ_EVENT_ID); + Assertions.assertEquals(BIZ_EVENT_ID, bizEventResponse.getId()); + } + + @Test + void runKo() { + CosmosClient mockClient = mock(CosmosClient.class); + + CosmosDatabase mockDatabase = mock(CosmosDatabase.class); + CosmosContainer mockContainer = mock(CosmosContainer.class); + + CosmosPagedIterable mockIterable = mock(CosmosPagedIterable.class); + + Iterator mockIterator = mock(Iterator.class); + + when(mockIterator.hasNext()).thenReturn(false); + + when(mockIterable.iterator()).thenReturn(mockIterator); + + when(mockContainer.queryItems(anyString(), any(), eq(BizEvent.class))).thenReturn( + mockIterable + ); + when(mockDatabase.getContainer(any())).thenReturn(mockContainer); + when(mockClient.getDatabase(any())).thenReturn(mockDatabase); + + BizEventCosmosClientImpl client = new BizEventCosmosClientImpl(mockClient); + + Assertions.assertThrows(BizEventNotFoundException.class, () -> client.getBizEventDocument("an invalid receipt id")); + } + +} \ No newline at end of file diff --git a/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/PDVTokenizerClientImplTest.java b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/PDVTokenizerClientImplTest.java new file mode 100644 index 0000000..3c1e5b9 --- /dev/null +++ b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/PDVTokenizerClientImplTest.java @@ -0,0 +1,99 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.client.impl; + +import it.gov.pagopa.receipt.pdf.helpdesk.client.PDVTokenizerClient; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.PDVTokenizerException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.http.HttpClient; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +class PDVTokenizerClientImplTest { + + private HttpClient clientMock; + private PDVTokenizerClient sut; + + @BeforeEach + void setUp() { + clientMock = mock(HttpClient.class); + sut = spy(new PDVTokenizerClientImpl(clientMock)); + } + + @Test + void searchTokenByPIISuccess() throws PDVTokenizerException, IOException, InterruptedException { + sut.searchTokenByPII(anyString()); + + verify(clientMock).send(any(), any()); + } + + @Test + void findPIIByTokenSuccess() throws PDVTokenizerException, IOException, InterruptedException { + sut.findPIIByToken(anyString()); + + verify(clientMock).send(any(), any()); + } + + @Test + void createTokenSuccess() throws PDVTokenizerException, IOException, InterruptedException { + sut.createToken(anyString()); + + verify(clientMock).send(any(), any()); + } + + @Test + void searchTokenByPIIFailThrowsIOException() throws IOException, InterruptedException { + doThrow(IOException.class).when(clientMock).send(any(), any()); + + assertThrows(PDVTokenizerException.class, () -> sut.searchTokenByPII(anyString())); + + verify(clientMock).send(any(), any()); + } + + @Test + void searchTokenByPIIFailThrowsInterruptedException() throws IOException, InterruptedException { + doThrow(InterruptedException.class).when(clientMock).send(any(), any()); + + assertThrows(PDVTokenizerException.class, () -> sut.searchTokenByPII(anyString())); + + verify(clientMock).send(any(), any()); + } + + @Test + void findPIIByTokenFailThrowsIOException() throws IOException, InterruptedException { + doThrow(IOException.class).when(clientMock).send(any(), any()); + + assertThrows(PDVTokenizerException.class, () -> sut.findPIIByToken(anyString())); + + verify(clientMock).send(any(), any()); + } + + @Test + void findPIIByTokenFailThrowsInterruptedException() throws IOException, InterruptedException { + doThrow(InterruptedException.class).when(clientMock).send(any(), any()); + + assertThrows(PDVTokenizerException.class, () -> sut.findPIIByToken(anyString())); + + verify(clientMock).send(any(), any()); + } + + @Test + void createTokenFailThrowsIOException() throws IOException, InterruptedException { + doThrow(IOException.class).when(clientMock).send(any(), any()); + + assertThrows(PDVTokenizerException.class, () -> sut.createToken(anyString())); + + verify(clientMock).send(any(), any()); + } + + @Test + void createTokenFailThrowsInterruptedException() throws IOException, InterruptedException { + doThrow(InterruptedException.class).when(clientMock).send(any(), any()); + + assertThrows(PDVTokenizerException.class, () -> sut.createToken(anyString())); + + verify(clientMock).send(any(), any()); + } +} \ No newline at end of file diff --git a/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/ReceiptCosmosClientImplTest.java b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/ReceiptCosmosClientImplTest.java new file mode 100644 index 0000000..ea640db --- /dev/null +++ b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/ReceiptCosmosClientImplTest.java @@ -0,0 +1,123 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.client.impl; + +import com.azure.cosmos.CosmosClient; +import com.azure.cosmos.CosmosContainer; +import com.azure.cosmos.CosmosDatabase; +import com.azure.cosmos.util.CosmosPagedIterable; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.Receipt; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.ReceiptNotFoundException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Iterator; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static uk.org.webcompere.systemstubs.SystemStubs.withEnvironmentVariables; + +class ReceiptCosmosClientImplTest { + + @Test + void testSingletonConnectionError() throws Exception { + String mockKey = "mockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeyMK=="; + withEnvironmentVariables( + "COSMOS_RECEIPT_KEY", mockKey, + "COSMOS_RECEIPT_SERVICE_ENDPOINT", "" + ).execute(() -> Assertions.assertThrows(IllegalArgumentException.class, ReceiptCosmosClientImpl::getInstance) + ); + } + + @Test + void runOk() throws ReceiptNotFoundException { + String RECEIPT_ID = "a valid receipt id"; + + CosmosClient mockClient = mock(CosmosClient.class); + + CosmosDatabase mockDatabase = mock(CosmosDatabase.class); + CosmosContainer mockContainer = mock(CosmosContainer.class); + + CosmosPagedIterable mockIterable = mock(CosmosPagedIterable.class); + + Iterator mockIterator = mock(Iterator.class); + Receipt receipt = new Receipt(); + receipt.setId(RECEIPT_ID); + + when(mockIterator.hasNext()).thenReturn(true); + when(mockIterator.next()).thenReturn(receipt); + + when(mockIterable.iterator()).thenReturn(mockIterator); + + when(mockContainer.queryItems(anyString(), any(), eq(Receipt.class))).thenReturn( + mockIterable + ); + when(mockDatabase.getContainer(any())).thenReturn(mockContainer); + when(mockClient.getDatabase(any())).thenReturn(mockDatabase); + + ReceiptCosmosClientImpl client = new ReceiptCosmosClientImpl(mockClient); + + Assertions.assertDoesNotThrow(() -> client.getReceiptDocument(RECEIPT_ID)); + + Receipt receiptResponse = client.getReceiptDocument(RECEIPT_ID); + Assertions.assertEquals(RECEIPT_ID, receiptResponse.getId()); + } + + @Test + void runKo() { + CosmosClient mockClient = mock(CosmosClient.class); + + CosmosDatabase mockDatabase = mock(CosmosDatabase.class); + CosmosContainer mockContainer = mock(CosmosContainer.class); + + CosmosPagedIterable mockIterable = mock(CosmosPagedIterable.class); + + Iterator mockIterator = mock(Iterator.class); + + when(mockIterator.hasNext()).thenReturn(false); + + when(mockIterable.iterator()).thenReturn(mockIterator); + + when(mockContainer.queryItems(anyString(), any(), eq(Receipt.class))).thenReturn( + mockIterable + ); + when(mockDatabase.getContainer(any())).thenReturn(mockContainer); + when(mockClient.getDatabase(any())).thenReturn(mockDatabase); + + ReceiptCosmosClientImpl client = new ReceiptCosmosClientImpl(mockClient); + + Assertions.assertThrows(ReceiptNotFoundException.class, () -> client.getReceiptDocument("an invalid receipt id")); + } + + @Test +void runOk_FailedQueryClient() throws ReceiptNotFoundException { + String RECEIPT_ID = "a valid receipt id"; + + CosmosClient mockClient = mock(CosmosClient.class); + + CosmosDatabase mockDatabase = mock(CosmosDatabase.class); + CosmosContainer mockContainer = mock(CosmosContainer.class); + + CosmosPagedIterable mockIterable = mock(CosmosPagedIterable.class); + + Iterator mockIterator = mock(Iterator.class); + Receipt receipt = new Receipt(); + receipt.setId(RECEIPT_ID); + + when(mockIterator.hasNext()).thenReturn(true); + when(mockIterator.next()).thenReturn(receipt); + + when(mockIterable.iterator()).thenReturn(mockIterator); + + when(mockContainer.queryItems(anyString(), any(), eq(Receipt.class))).thenReturn( + mockIterable + ); + when(mockDatabase.getContainer(any())).thenReturn(mockContainer); + when(mockClient.getDatabase(any())).thenReturn(mockDatabase); + + ReceiptCosmosClientImpl client = new ReceiptCosmosClientImpl(mockClient); + + Assertions.assertDoesNotThrow(() -> client.getFailedReceiptDocuments(null, 100)); + + } + +} \ No newline at end of file diff --git a/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/ReceiptQueueClientImplTest.java b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/ReceiptQueueClientImplTest.java new file mode 100644 index 0000000..69e616c --- /dev/null +++ b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/client/impl/ReceiptQueueClientImplTest.java @@ -0,0 +1,64 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.client.impl; + +import com.azure.core.http.rest.Response; +import com.azure.storage.queue.QueueClient; +import com.azure.storage.queue.models.SendMessageResult; +import com.microsoft.azure.functions.HttpStatus; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static uk.org.webcompere.systemstubs.SystemStubs.withEnvironmentVariables; + +class ReceiptQueueClientImplTest { + + @Test + void testSingletonConnectionError() throws Exception { + @SuppressWarnings("secrets:S6338") + String mockKey = "mockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeymockKeyMK=="; + withEnvironmentVariables( + "RECEIPT_QUEUE_CONN_STRING", "DefaultEndpointsProtocol=https;AccountName=samplequeue;AccountKey="+mockKey+";EndpointSuffix=core.windows.net", + "RECEIPT_QUEUE_TOPIC", "validTopic" + ).execute(() -> Assertions.assertDoesNotThrow(ReceiptQueueClientImpl::getInstance) + ); + } + + @Test + void runOk() { + String MESSAGE_TEXT = "a valid message text"; + + Response response = mock(Response.class); + QueueClient mockClient = mock(QueueClient.class); + + when(response.getStatusCode()).thenReturn(HttpStatus.CREATED.value()); + when(mockClient.sendMessageWithResponse(eq(MESSAGE_TEXT), any(), eq(null), eq(null), eq(null))) + .thenReturn(response); + + ReceiptQueueClientImpl client = new ReceiptQueueClientImpl(mockClient); + + Response clientResponse = client.sendMessageToQueue(MESSAGE_TEXT); + + Assertions.assertEquals(HttpStatus.CREATED.value(), clientResponse.getStatusCode()); + } + + @Test + void runKo() { + String MESSAGE_TEXT = "an invalid message text"; + + Response response = mock(Response.class); + QueueClient mockClient = mock(QueueClient.class); + + when(response.getStatusCode()).thenReturn(HttpStatus.NO_CONTENT.value()); + when(mockClient.sendMessageWithResponse(eq(MESSAGE_TEXT), any(), eq(null), eq(null), eq(null))) + .thenReturn(response); + + ReceiptQueueClientImpl client = new ReceiptQueueClientImpl(mockClient); + + Response clientResponse = client.sendMessageToQueue(MESSAGE_TEXT); + + Assertions.assertEquals(HttpStatus.NO_CONTENT.value(), clientResponse.getStatusCode()); + } +} \ No newline at end of file diff --git a/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/service/impl/PDVTokenizerServiceImplTest.java b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/service/impl/PDVTokenizerServiceImplTest.java new file mode 100644 index 0000000..fb29183 --- /dev/null +++ b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/service/impl/PDVTokenizerServiceImplTest.java @@ -0,0 +1,226 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.service.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import it.gov.pagopa.receipt.pdf.helpdesk.client.PDVTokenizerClient; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.PDVTokenizerException; +import it.gov.pagopa.receipt.pdf.helpdesk.model.tokenizer.*; +import it.gov.pagopa.receipt.pdf.helpdesk.service.PDVTokenizerService; +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +class PDVTokenizerServiceImplTest { + + private static final String TOKEN = "token"; + private static final String FISCAL_CODE = "fiscalCode"; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + private HttpResponse httpResponseMock; + private PDVTokenizerClient pdvTokenizerClientMock; + + private PDVTokenizerService sut; + + @BeforeEach + void setUp() { + httpResponseMock = mock(HttpResponse.class); + pdvTokenizerClientMock = mock(PDVTokenizerClient.class); + sut = spy(new PDVTokenizerServiceImpl(pdvTokenizerClientMock)); + } + + @Test + void getTokenSuccess() throws JsonProcessingException, PDVTokenizerException { + TokenResource tokenResource = TokenResource.builder().token(TOKEN).build(); + String responseBody = objectMapper.writeValueAsString(tokenResource); + + doReturn(HttpStatus.SC_OK).when(httpResponseMock).statusCode(); + doReturn(responseBody).when(httpResponseMock).body(); + doReturn(httpResponseMock).when(pdvTokenizerClientMock).searchTokenByPII(anyString()); + + String token = sut.getToken(FISCAL_CODE); + + assertNotNull(token); + assertEquals(TOKEN, token); + + verify(pdvTokenizerClientMock).searchTokenByPII(anyString()); + } + + @Test + void getFiscalCodeSuccess() throws JsonProcessingException, PDVTokenizerException { + PiiResource piiResource = PiiResource.builder().pii(FISCAL_CODE).build(); + String responseBody = objectMapper.writeValueAsString(piiResource); + + doReturn(HttpStatus.SC_OK).when(httpResponseMock).statusCode(); + doReturn(responseBody).when(httpResponseMock).body(); + doReturn(httpResponseMock).when(pdvTokenizerClientMock).findPIIByToken(anyString()); + + String fiscalCode = sut.getFiscalCode(TOKEN); + + assertNotNull(fiscalCode); + assertEquals(FISCAL_CODE, fiscalCode); + + verify(pdvTokenizerClientMock).findPIIByToken(anyString()); + } + + @Test + void generateTokenForFiscalCodeSuccess() throws JsonProcessingException, PDVTokenizerException { + TokenResource tokenResource = TokenResource.builder().token(TOKEN).build(); + String responseBody = objectMapper.writeValueAsString(tokenResource); + + doReturn(HttpStatus.SC_OK).when(httpResponseMock).statusCode(); + doReturn(responseBody).when(httpResponseMock).body(); + doReturn(httpResponseMock).when(pdvTokenizerClientMock).createToken(anyString()); + + String token = sut.generateTokenForFiscalCode(FISCAL_CODE); + + assertNotNull(token); + assertEquals(TOKEN, token); + + verify(pdvTokenizerClientMock).createToken(anyString()); + } + + @Test + void getTokenFailClientThrowsPDVTokenizerException() throws PDVTokenizerException { + doThrow(PDVTokenizerException.class).when(pdvTokenizerClientMock).searchTokenByPII(anyString()); + + assertThrows(PDVTokenizerException.class, () -> sut.getToken(FISCAL_CODE)); + + verify(pdvTokenizerClientMock).searchTokenByPII(anyString()); + } + + @Test + void getTokenFailResponse400() throws PDVTokenizerException, JsonProcessingException { + ErrorResponse errorResponse = buildErrorResponse(); + String responseBody = objectMapper.writeValueAsString(errorResponse); + + doReturn(HttpStatus.SC_BAD_REQUEST).when(httpResponseMock).statusCode(); + doReturn(responseBody).when(httpResponseMock).body(); + doReturn(httpResponseMock).when(pdvTokenizerClientMock).searchTokenByPII(anyString()); + + PDVTokenizerException e = assertThrows(PDVTokenizerException.class, () -> sut.getToken(FISCAL_CODE)); + + assertEquals(HttpStatus.SC_BAD_REQUEST, e.getStatusCode()); + + verify(pdvTokenizerClientMock).searchTokenByPII(anyString()); + } + + @Test + void getTokenFailResponse429() throws PDVTokenizerException, JsonProcessingException { + ErrorMessage errorResponse = ErrorMessage.builder().message("Too Many Requests").build(); + String responseBody = objectMapper.writeValueAsString(errorResponse); + + doReturn(429).when(httpResponseMock).statusCode(); + doReturn(responseBody).when(httpResponseMock).body(); + doReturn(httpResponseMock).when(pdvTokenizerClientMock).searchTokenByPII(anyString()); + + PDVTokenizerException e = assertThrows(PDVTokenizerException.class, () -> sut.getToken(FISCAL_CODE)); + + assertEquals(429, e.getStatusCode()); + + verify(pdvTokenizerClientMock).searchTokenByPII(anyString()); + } + + @Test + void getFiscalCodeFailClientThrowsPDVTokenizerException() throws PDVTokenizerException { + doThrow(PDVTokenizerException.class).when(pdvTokenizerClientMock).findPIIByToken(anyString()); + + assertThrows(PDVTokenizerException.class, () -> sut.getFiscalCode(TOKEN)); + + verify(pdvTokenizerClientMock).findPIIByToken(anyString()); + } + + @Test + void getFiscalCodeFailResponse400() throws PDVTokenizerException, JsonProcessingException { + ErrorResponse errorResponse = buildErrorResponse(); + String responseBody = objectMapper.writeValueAsString(errorResponse); + + doReturn(HttpStatus.SC_BAD_REQUEST).when(httpResponseMock).statusCode(); + doReturn(responseBody).when(httpResponseMock).body(); + doReturn(httpResponseMock).when(pdvTokenizerClientMock).findPIIByToken(anyString()); + + PDVTokenizerException e = assertThrows(PDVTokenizerException.class, () -> sut.getFiscalCode(TOKEN)); + + assertEquals(HttpStatus.SC_BAD_REQUEST, e.getStatusCode()); + + verify(pdvTokenizerClientMock).findPIIByToken(anyString()); + } + + @Test + void getFiscalCodeFailResponse429() throws PDVTokenizerException, JsonProcessingException { + ErrorMessage errorResponse = ErrorMessage.builder().message("Too Many Requests").build(); + String responseBody = objectMapper.writeValueAsString(errorResponse); + + doReturn(429).when(httpResponseMock).statusCode(); + doReturn(responseBody).when(httpResponseMock).body(); + doReturn(httpResponseMock).when(pdvTokenizerClientMock).findPIIByToken(anyString()); + + PDVTokenizerException e = assertThrows(PDVTokenizerException.class, () -> sut.getFiscalCode(TOKEN)); + + assertEquals(429, e.getStatusCode()); + + verify(pdvTokenizerClientMock).findPIIByToken(anyString()); + } + + @Test + void generateTokenForFiscalCodeFailClientThrowsPDVTokenizerException() throws PDVTokenizerException { + doThrow(PDVTokenizerException.class).when(pdvTokenizerClientMock).createToken(anyString()); + + assertThrows(PDVTokenizerException.class, () -> sut.generateTokenForFiscalCode(FISCAL_CODE)); + + verify(pdvTokenizerClientMock).createToken(anyString()); + } + + @Test + void generateTokenForFiscalCodeFailResponse400() throws PDVTokenizerException, JsonProcessingException { + ErrorResponse errorResponse = buildErrorResponse(); + String responseBody = objectMapper.writeValueAsString(errorResponse); + + doReturn(HttpStatus.SC_BAD_REQUEST).when(httpResponseMock).statusCode(); + doReturn(responseBody).when(httpResponseMock).body(); + doReturn(httpResponseMock).when(pdvTokenizerClientMock).createToken(anyString()); + + PDVTokenizerException e = assertThrows(PDVTokenizerException.class, () -> sut.generateTokenForFiscalCode(FISCAL_CODE)); + + assertEquals(HttpStatus.SC_BAD_REQUEST, e.getStatusCode()); + + verify(pdvTokenizerClientMock).createToken(anyString()); + } + + @Test + void generateTokenForFiscalCodeFailResponse429() throws PDVTokenizerException, JsonProcessingException { + ErrorMessage errorResponse = ErrorMessage.builder().message("Too Many Requests").build(); + String responseBody = objectMapper.writeValueAsString(errorResponse); + + doReturn(429).when(httpResponseMock).statusCode(); + doReturn(responseBody).when(httpResponseMock).body(); + doReturn(httpResponseMock).when(pdvTokenizerClientMock).createToken(anyString()); + + PDVTokenizerException e = assertThrows(PDVTokenizerException.class, () -> sut.generateTokenForFiscalCode(FISCAL_CODE)); + + assertEquals(429, e.getStatusCode()); + + verify(pdvTokenizerClientMock).createToken(anyString()); + } + + private ErrorResponse buildErrorResponse() { + return ErrorResponse.builder() + .title("Error") + .detail("Error detail") + .status(HttpStatus.SC_BAD_REQUEST) + .invalidParams(Collections.singletonList(InvalidParam.builder() + .name("param name") + .reason("reason") + .build())) + .instance("instance") + .type("type") + .build(); + } +} \ No newline at end of file diff --git a/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/service/impl/PDVTokenizerServiceRetryWrapperImplTest.java b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/service/impl/PDVTokenizerServiceRetryWrapperImplTest.java new file mode 100644 index 0000000..103efdf --- /dev/null +++ b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/service/impl/PDVTokenizerServiceRetryWrapperImplTest.java @@ -0,0 +1,214 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.service.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.github.resilience4j.retry.Retry; +import io.github.resilience4j.retry.RetryConfig; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.PDVTokenizerException; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.PDVTokenizerUnexpectedException; +import it.gov.pagopa.receipt.pdf.helpdesk.service.PDVTokenizerService; +import it.gov.pagopa.receipt.pdf.helpdesk.service.PDVTokenizerServiceRetryWrapper; +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +class PDVTokenizerServiceRetryWrapperImplTest { + + private static final String FISCAL_CODE = "fiscalCode"; + private static final String TOKEN = "token"; + private static final int MAX_ATTEMPTS = 3; + + private PDVTokenizerService pdvTokenizerServiceMock; + + private PDVTokenizerServiceRetryWrapper sut; + + @BeforeEach + void setUp() { + pdvTokenizerServiceMock = mock(PDVTokenizerService.class); + + RetryConfig config = RetryConfig.custom() + .maxAttempts(MAX_ATTEMPTS) + .retryOnException(e -> (e instanceof PDVTokenizerException tokenizerException) && tokenizerException.getStatusCode() == 429) + .build(); + Retry retry = Retry.of("id", config); + + sut = spy(new PDVTokenizerServiceRetryWrapperImpl(pdvTokenizerServiceMock, retry)); + } + + @Test + void getTokenRetryForPDVTokenizerExceptionWithStatus429() throws PDVTokenizerException, JsonProcessingException { + String errMsg = "Error"; + doThrow(new PDVTokenizerException(errMsg, 429)).when(pdvTokenizerServiceMock).getToken(anyString()); + + PDVTokenizerException e = assertThrows(PDVTokenizerException.class, () -> sut.getTokenWithRetry(FISCAL_CODE)); + + assertNotNull(e); + assertEquals(429, e.getStatusCode()); + assertEquals(errMsg, e.getMessage()); + + verify(pdvTokenizerServiceMock, times(MAX_ATTEMPTS)).getToken(anyString()); + } + + @Test + void getTokenNotRetryForPDVTokenizerExceptionWithoutStatus429() throws PDVTokenizerException, JsonProcessingException { + String errMsg = "Error"; + doThrow(new PDVTokenizerException(errMsg, HttpStatus.SC_INTERNAL_SERVER_ERROR)).when(pdvTokenizerServiceMock).getToken(anyString()); + + PDVTokenizerException e = assertThrows(PDVTokenizerException.class, () -> sut.getTokenWithRetry(FISCAL_CODE)); + + assertNotNull(e); + assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getStatusCode()); + assertEquals(errMsg, e.getMessage()); + + verify(pdvTokenizerServiceMock).getToken(anyString()); + } + + @Test + void getTokenNotRetryForJsonProcessingException() throws PDVTokenizerException, JsonProcessingException { + doThrow(JsonProcessingException.class).when(pdvTokenizerServiceMock).getToken(anyString()); + + JsonProcessingException e = assertThrows(JsonProcessingException.class, () -> sut.getTokenWithRetry(FISCAL_CODE)); + + assertNotNull(e); + verify(pdvTokenizerServiceMock).getToken(anyString()); + } + + @Test + void getTokenNotRetryForPDVTokenizerUnexpectedException() throws PDVTokenizerException, JsonProcessingException { + doThrow(RuntimeException.class).when(pdvTokenizerServiceMock).getToken(anyString()); + + PDVTokenizerUnexpectedException e = assertThrows(PDVTokenizerUnexpectedException.class, () -> sut.getTokenWithRetry(FISCAL_CODE)); + + assertNotNull(e); + verify(pdvTokenizerServiceMock).getToken(anyString()); + } + + @Test + void getTokenSuccessNotRetry() throws PDVTokenizerException, JsonProcessingException { + doReturn(TOKEN).when(pdvTokenizerServiceMock).getToken(anyString()); + + String token = sut.getTokenWithRetry(FISCAL_CODE); + + assertEquals(TOKEN, token); + verify(pdvTokenizerServiceMock).getToken(anyString()); + } + + @Test + void getFiscalCodeRetryForPDVTokenizerExceptionWithStatus429() throws PDVTokenizerException, JsonProcessingException { + String errMsg = "Error"; + doThrow(new PDVTokenizerException(errMsg, 429)).when(pdvTokenizerServiceMock).getFiscalCode(anyString()); + + PDVTokenizerException e = assertThrows(PDVTokenizerException.class, () -> sut.getFiscalCodeWithRetry(TOKEN)); + + assertNotNull(e); + assertEquals(429, e.getStatusCode()); + assertEquals(errMsg, e.getMessage()); + + verify(pdvTokenizerServiceMock, times(MAX_ATTEMPTS)).getFiscalCode(anyString()); + } + + @Test + void getFiscalCodeNotRetryForPDVTokenizerExceptionWithoutStatus429() throws PDVTokenizerException, JsonProcessingException { + String errMsg = "Error"; + doThrow(new PDVTokenizerException(errMsg, HttpStatus.SC_INTERNAL_SERVER_ERROR)).when(pdvTokenizerServiceMock).getFiscalCode(anyString()); + + PDVTokenizerException e = assertThrows(PDVTokenizerException.class, () -> sut.getFiscalCodeWithRetry(TOKEN)); + + assertNotNull(e); + assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getStatusCode()); + assertEquals(errMsg, e.getMessage()); + + verify(pdvTokenizerServiceMock).getFiscalCode(anyString()); + } + + @Test + void getFiscalCodeNotRetryForJsonProcessingException() throws PDVTokenizerException, JsonProcessingException { + doThrow(JsonProcessingException.class).when(pdvTokenizerServiceMock).getFiscalCode(anyString()); + + JsonProcessingException e = assertThrows(JsonProcessingException.class, () -> sut.getFiscalCodeWithRetry(TOKEN)); + + assertNotNull(e); + verify(pdvTokenizerServiceMock).getFiscalCode(anyString()); + } + + @Test + void getFiscalCodeNotRetryForPDVTokenizerUnexpectedException() throws PDVTokenizerException, JsonProcessingException { + doThrow(RuntimeException.class).when(pdvTokenizerServiceMock).getFiscalCode(anyString()); + + PDVTokenizerUnexpectedException e = assertThrows(PDVTokenizerUnexpectedException.class, () -> sut.getFiscalCodeWithRetry(TOKEN)); + + assertNotNull(e); + verify(pdvTokenizerServiceMock).getFiscalCode(anyString()); + } + + @Test + void getFiscalCodeSuccessNotRetry() throws PDVTokenizerException, JsonProcessingException { + doReturn(FISCAL_CODE).when(pdvTokenizerServiceMock).getFiscalCode(anyString()); + + String token = sut.getFiscalCodeWithRetry(TOKEN); + + assertEquals(FISCAL_CODE, token); + verify(pdvTokenizerServiceMock).getFiscalCode(anyString()); + } + + @Test + void generateTokenForFiscalCodeRetryForPDVTokenizerExceptionWithStatus429() throws PDVTokenizerException, JsonProcessingException { + String errMsg = "Error"; + doThrow(new PDVTokenizerException(errMsg, 429)).when(pdvTokenizerServiceMock).generateTokenForFiscalCode(anyString()); + + PDVTokenizerException e = assertThrows(PDVTokenizerException.class, () -> sut.generateTokenForFiscalCodeWithRetry(FISCAL_CODE)); + + assertNotNull(e); + assertEquals(429, e.getStatusCode()); + assertEquals(errMsg, e.getMessage()); + + verify(pdvTokenizerServiceMock, times(MAX_ATTEMPTS)).generateTokenForFiscalCode(anyString()); + } + + @Test + void generateTokenForFiscalCodeNotRetryForPDVTokenizerExceptionWithoutStatus429() throws PDVTokenizerException, JsonProcessingException { + String errMsg = "Error"; + doThrow(new PDVTokenizerException(errMsg, HttpStatus.SC_INTERNAL_SERVER_ERROR)).when(pdvTokenizerServiceMock).generateTokenForFiscalCode(anyString()); + + PDVTokenizerException e = assertThrows(PDVTokenizerException.class, () -> sut.generateTokenForFiscalCodeWithRetry(FISCAL_CODE)); + + assertNotNull(e); + assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getStatusCode()); + assertEquals(errMsg, e.getMessage()); + + verify(pdvTokenizerServiceMock).generateTokenForFiscalCode(anyString()); + } + + @Test + void generateTokenForFiscalCodeNotRetryForJsonProcessingException() throws PDVTokenizerException, JsonProcessingException { + doThrow(JsonProcessingException.class).when(pdvTokenizerServiceMock).generateTokenForFiscalCode(anyString()); + + JsonProcessingException e = assertThrows(JsonProcessingException.class, () -> sut.generateTokenForFiscalCodeWithRetry(FISCAL_CODE)); + + assertNotNull(e); + verify(pdvTokenizerServiceMock).generateTokenForFiscalCode(anyString()); + } + + @Test + void generateTokenForFiscalCodeNotRetryForPDVTokenizerUnexpectedException() throws PDVTokenizerException, JsonProcessingException { + doThrow(RuntimeException.class).when(pdvTokenizerServiceMock).generateTokenForFiscalCode(anyString()); + + PDVTokenizerUnexpectedException e = assertThrows(PDVTokenizerUnexpectedException.class, () -> sut.generateTokenForFiscalCodeWithRetry(FISCAL_CODE)); + + assertNotNull(e); + verify(pdvTokenizerServiceMock).generateTokenForFiscalCode(anyString()); + } + + @Test + void generateTokenForFiscalCodeSuccessNotRetry() throws PDVTokenizerException, JsonProcessingException { + doReturn(TOKEN).when(pdvTokenizerServiceMock).generateTokenForFiscalCode(anyString()); + + String token = sut.generateTokenForFiscalCodeWithRetry(FISCAL_CODE); + + assertEquals(TOKEN, token); + verify(pdvTokenizerServiceMock).generateTokenForFiscalCode(anyString()); + } +} \ No newline at end of file diff --git a/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/util/HttpResponseMessageMock.java b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/util/HttpResponseMessageMock.java new file mode 100644 index 0000000..4879dc5 --- /dev/null +++ b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/util/HttpResponseMessageMock.java @@ -0,0 +1,84 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.util; + + +import com.microsoft.azure.functions.HttpResponseMessage; +import com.microsoft.azure.functions.HttpStatus; +import com.microsoft.azure.functions.HttpStatusType; + +import java.util.HashMap; +import java.util.Map; + +/** + * The mock for HttpResponseMessage, can be used in unit tests to verify if the + * returned response by HTTP trigger function is correct or not. + */ +public class HttpResponseMessageMock implements HttpResponseMessage { + private int httpStatusCode; + private HttpStatusType httpStatus; + private Object body; + private Map headers; + + public HttpResponseMessageMock(HttpStatusType status, Map headers, Object body) { + this.httpStatus = status; + this.httpStatusCode = status.value(); + this.headers = headers; + this.body = body; + } + + @Override + public HttpStatusType getStatus() { + return this.httpStatus; + } + + @Override + public int getStatusCode() { + return httpStatusCode; + } + + @Override + public String getHeader(String key) { + return this.headers.get(key); + } + + @Override + public Object getBody() { + return this.body; + } + + public static class HttpResponseMessageBuilderMock implements Builder { + private Object body; + private int httpStatusCode; + private Map headers = new HashMap<>(); + private HttpStatusType httpStatus; + + public Builder status(HttpStatus status) { + this.httpStatusCode = status.value(); + this.httpStatus = status; + return this; + } + + @Override + public Builder status(HttpStatusType httpStatusType) { + this.httpStatusCode = httpStatusType.value(); + this.httpStatus = httpStatusType; + return this; + } + + @Override + public Builder header(String key, String value) { + this.headers.put(key, value); + return this; + } + + @Override + public Builder body(Object body) { + this.body = body; + return this; + } + + @Override + public HttpResponseMessage build() { + return new HttpResponseMessageMock(this.httpStatus, this.headers, this.body); + } + } +} \ No newline at end of file diff --git a/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/utils/BizEventToReceiptUtilsTest.java b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/utils/BizEventToReceiptUtilsTest.java new file mode 100644 index 0000000..61d2c29 --- /dev/null +++ b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/utils/BizEventToReceiptUtilsTest.java @@ -0,0 +1,153 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.utils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.microsoft.azure.functions.HttpStatus; +import it.gov.pagopa.receipt.pdf.helpdesk.client.impl.ReceiptCosmosClientImpl; +import it.gov.pagopa.receipt.pdf.helpdesk.client.impl.ReceiptQueueClientImpl; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.event.*; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.event.enumeration.BizEventStatusType; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.Receipt; +import it.gov.pagopa.receipt.pdf.helpdesk.entity.receipt.enumeration.ReceiptStatusType; +import it.gov.pagopa.receipt.pdf.helpdesk.exception.PDVTokenizerException; +import it.gov.pagopa.receipt.pdf.helpdesk.service.PDVTokenizerServiceRetryWrapper; +import it.gov.pagopa.receipt.pdf.helpdesk.service.impl.BizEventToReceiptServiceImpl; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class BizEventToReceiptUtilsTest { + private final String EVENT_ID = "a valid id"; + private final String PAYER_FISCAL_CODE = "a valid payer CF"; + private final String DEBTOR_FISCAL_CODE = "a valid debtor CF"; + private final String TOKENIZED_DEBTOR_FISCAL_CODE = "tokenizedDebtorFiscalCode"; + private final String TOKENIZED_PAYER_FISCAL_CODE = "tokenizedPayerFiscalCode"; + public static final String REMITTANCE_INFORMATION_PAYMENT_INFO = "TARI 2021"; + public static final String REMITTANCE_INFORMATION_TRANSFER_LIST = "EXAMPLE/TXT/TARI 2021/EXAMPLE"; + public static final String REMITTANCE_INFORMATION_TRANSFER_LIST_FORMATTED = "TARI 2021/EXAMPLE"; + public static final String TRANSFER_AMOUNT_HIGHEST = "10000.00"; + public static final String TRANSFER_AMOUNT_MEDIUM = "20.00"; + public static final String TRANSFER_AMOUNT_LOWEST = "10.00"; + + @Mock + private PDVTokenizerServiceRetryWrapper pdvTokenizerServiceMock; + private final Logger logger = LoggerFactory.getLogger(BizEventToReceiptUtilsTest.class); + + @Test + void createReceiptSuccessWithPaymentInfo() throws PDVTokenizerException, JsonProcessingException { + when(pdvTokenizerServiceMock.generateTokenForFiscalCodeWithRetry(DEBTOR_FISCAL_CODE)).thenReturn(TOKENIZED_DEBTOR_FISCAL_CODE); + when(pdvTokenizerServiceMock.generateTokenForFiscalCodeWithRetry(PAYER_FISCAL_CODE)).thenReturn(TOKENIZED_PAYER_FISCAL_CODE); + + BizEventToReceiptServiceImpl receiptService = new BizEventToReceiptServiceImpl(pdvTokenizerServiceMock, mock(ReceiptQueueClientImpl.class)); + + Receipt receipt = BizEventToReceiptUtils.createReceipt(generateValidBizEvent(false,false), receiptService, logger); + + assertEquals(EVENT_ID, receipt.getEventId()); + assertNotNull(receipt.getId()); + assertEquals(TOKENIZED_DEBTOR_FISCAL_CODE, receipt.getEventData().getDebtorFiscalCode()); + assertEquals(TOKENIZED_PAYER_FISCAL_CODE, receipt.getEventData().getPayerFiscalCode()); + assertEquals(REMITTANCE_INFORMATION_PAYMENT_INFO, receipt.getEventData().getCart().get(0).getSubject()); + } + + @Test + void createReceiptSuccessWithoutPaymentInfoButWithTransferList() throws PDVTokenizerException, JsonProcessingException { + when(pdvTokenizerServiceMock.generateTokenForFiscalCodeWithRetry(DEBTOR_FISCAL_CODE)).thenReturn(TOKENIZED_DEBTOR_FISCAL_CODE); + when(pdvTokenizerServiceMock.generateTokenForFiscalCodeWithRetry(PAYER_FISCAL_CODE)).thenReturn(TOKENIZED_PAYER_FISCAL_CODE); + + BizEventToReceiptServiceImpl receiptService = new BizEventToReceiptServiceImpl(pdvTokenizerServiceMock, mock(ReceiptQueueClientImpl.class)); + + Receipt receipt = BizEventToReceiptUtils.createReceipt(generateValidBizEvent(false,true), receiptService, logger); + + assertEquals(EVENT_ID, receipt.getEventId()); + assertNotNull(receipt.getId()); + assertEquals(TOKENIZED_DEBTOR_FISCAL_CODE, receipt.getEventData().getDebtorFiscalCode()); + assertEquals(TOKENIZED_PAYER_FISCAL_CODE, receipt.getEventData().getPayerFiscalCode()); + assertEquals(REMITTANCE_INFORMATION_TRANSFER_LIST_FORMATTED, receipt.getEventData().getCart().get(0).getSubject()); + } + + @Test + void createReceiptSuccessWithoutRemittanceInformation() throws PDVTokenizerException, JsonProcessingException { + when(pdvTokenizerServiceMock.generateTokenForFiscalCodeWithRetry(DEBTOR_FISCAL_CODE)).thenReturn(TOKENIZED_DEBTOR_FISCAL_CODE); + when(pdvTokenizerServiceMock.generateTokenForFiscalCodeWithRetry(PAYER_FISCAL_CODE)).thenReturn(TOKENIZED_PAYER_FISCAL_CODE); + + BizEventToReceiptServiceImpl receiptService = new BizEventToReceiptServiceImpl(pdvTokenizerServiceMock, mock(ReceiptQueueClientImpl.class)); + + Receipt receipt = BizEventToReceiptUtils.createReceipt(generateValidBizEvent(true,false), receiptService, logger); + + assertEquals(EVENT_ID, receipt.getEventId()); + assertNotNull(receipt.getId()); + assertEquals(TOKENIZED_DEBTOR_FISCAL_CODE, receipt.getEventData().getDebtorFiscalCode()); + assertEquals(TOKENIZED_PAYER_FISCAL_CODE, receipt.getEventData().getPayerFiscalCode()); + assertNull(receipt.getEventData().getCart().get(0).getSubject()); + } + + @Test + void createReceiptSuccessWithTokenizerFailed() throws PDVTokenizerException, JsonProcessingException { + when(pdvTokenizerServiceMock.generateTokenForFiscalCodeWithRetry(DEBTOR_FISCAL_CODE)).thenThrow(new PDVTokenizerException("exception", HttpStatus.I_AM_A_TEAPOT.value())); + + BizEventToReceiptServiceImpl receiptService = new BizEventToReceiptServiceImpl(pdvTokenizerServiceMock, mock(ReceiptQueueClientImpl.class)); + + Receipt receipt = BizEventToReceiptUtils.createReceipt(generateValidBizEvent(false,false), receiptService, logger); + + assertEquals(EVENT_ID, receipt.getEventId()); + assertNotNull(receipt.getId()); + assertNull(receipt.getEventData()); + assertEquals(ReceiptStatusType.FAILED, receipt.getStatus()); + } + + private BizEvent generateValidBizEvent( boolean withoutRemittanceInformation, boolean withTransferList){ + BizEvent item = new BizEvent(); + + Payer payer = new Payer(); + payer.setEntityUniqueIdentifierValue(PAYER_FISCAL_CODE); + Debtor debtor = new Debtor(); + debtor.setEntityUniqueIdentifierValue(DEBTOR_FISCAL_CODE); + + TransactionDetails transactionDetails = new TransactionDetails(); + Transaction transaction = new Transaction(); + transaction.setCreationDate(String.valueOf(LocalDateTime.now())); + transactionDetails.setTransaction(transaction); + + PaymentInfo paymentInfo = new PaymentInfo(); + paymentInfo.setTotalNotice("1"); + if(!withoutRemittanceInformation){ + if(withTransferList){ + List transferList = List.of( + Transfer.builder() + .amount(TRANSFER_AMOUNT_LOWEST) + .remittanceInformation("not to show") + .build(), + Transfer.builder() + .amount(TRANSFER_AMOUNT_MEDIUM) + .remittanceInformation("not to show") + .build(), + Transfer.builder() + .amount(TRANSFER_AMOUNT_HIGHEST) + .remittanceInformation(REMITTANCE_INFORMATION_TRANSFER_LIST) + .build() + ); + item.setTransferList(transferList); + } else { + paymentInfo.setRemittanceInformation(REMITTANCE_INFORMATION_PAYMENT_INFO); + } + } + item.setEventStatus(BizEventStatusType.DONE); + item.setId(EVENT_ID); + item.setPayer(payer); + item.setDebtor(debtor); + item.setTransactionDetails(transactionDetails); + item.setPaymentInfo(paymentInfo); + + return item; + } +} \ No newline at end of file diff --git a/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/utils/ObjectMapperUtilsTest.java b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/utils/ObjectMapperUtilsTest.java new file mode 100644 index 0000000..da8e9af --- /dev/null +++ b/src/test/java/it/gov/pagopa/receipt/pdf/helpdesk/utils/ObjectMapperUtilsTest.java @@ -0,0 +1,18 @@ +package it.gov.pagopa.receipt.pdf.helpdesk.utils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; + +class ObjectMapperUtilsTest { + + @Test + void returnNullAfterException() { + + Assertions.assertNull(ObjectMapperUtils.writeValueAsString(InputStream.nullInputStream())); + Assertions.assertThrows(JsonProcessingException.class, () -> ObjectMapperUtils.mapString("", InputStream.class)); + + } +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..1eeeeff --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,2 @@ +version=${project.version} +name=pagopa-receipt-pdf-helpdesk \ No newline at end of file diff --git a/src/test/resources/resources/META-INF/maven/it.gov.pagopa.receipt.pdf.helpdesk/pagopa-receipt-pdf-helpdesk/pom.properties b/src/test/resources/resources/META-INF/maven/it.gov.pagopa.receipt.pdf.helpdesk/pagopa-receipt-pdf-helpdesk/pom.properties new file mode 100644 index 0000000..1cf49bc --- /dev/null +++ b/src/test/resources/resources/META-INF/maven/it.gov.pagopa.receipt.pdf.helpdesk/pagopa-receipt-pdf-helpdesk/pom.properties @@ -0,0 +1,5 @@ +#Generated by Maven +#Mon Jul 31 17:21:09 CEST 2023 +groupId=it.gov.pagopa.receipt +name=pagopa-receipt-pdf-helpdesk +version=x.y.z \ No newline at end of file