├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── images ├── github-start.png ├── smart-accounting-deployment-diag-1.png ├── smart-accounting-deployment-diag-2.png ├── smart-accounting1.PNG ├── smart-accounting10.PNG ├── smart-accounting11.PNG ├── smart-accounting2.PNG ├── smart-accounting3.PNG ├── smart-accounting4.PNG ├── smart-accounting5.png ├── smart-accounting6.PNG ├── smart-accounting7.PNG ├── smart-accounting8.PNG └── smart-accounting9.PNG ├── sample-files ├── Invoice01.PNG └── Invoice02.PNG └── src ├── smart-accounting-backend-services ├── azure-pipelines │ ├── azure-pipelines-build-template.yml │ ├── azure-pipelines-deployment-template.yml │ └── azure-pipelines.yml ├── kubernetes │ ├── create-ingress.yml │ ├── document-analyzer-microservice.yml │ ├── file-processor-microservice.yml │ ├── notification-microservice.yml │ └── processed-document-microservice.yml └── src │ ├── .dockerignore │ ├── BuildingBlocks │ ├── SmartAccounting.Common │ │ ├── CommonResponse │ │ │ ├── OperationError.cs │ │ │ └── OperationResponse.cs │ │ ├── ExceptionMiddleware │ │ │ ├── ApiExceptionMiddleware.cs │ │ │ └── ApiRequestProcessingError.cs │ │ └── SmartAccounting.Common.csproj │ ├── SmartAccounting.EventBus │ │ ├── AzureServiceBusEventBus.cs │ │ ├── Configuration │ │ │ └── IEventBusConfiguration.cs │ │ ├── InMemoryEventBusSubscriptionsManager.cs │ │ ├── IntegrationEvent.cs │ │ ├── Interfaces │ │ │ ├── IEventBus.cs │ │ │ ├── IEventBusSubscriptionsManager.cs │ │ │ └── IIntegrationEventHandler.cs │ │ ├── SmartAccounting.EventBus.csproj │ │ └── SubscriptionInfo.cs │ ├── SmartAccounting.EventLog │ │ ├── EventLogContext.cs │ │ ├── EventLogService.cs │ │ ├── EventState.cs │ │ ├── IntegrationEventLogEntry.cs │ │ └── SmartAccounting.EventLog.csproj │ └── SmartAccounting.Logging │ │ ├── Configuration │ │ └── ApplicationInsightsServiceConfiguration.cs │ │ ├── LoggingServicesInitializer.cs │ │ └── SmartAccounting.Logging.csproj │ ├── DocumentAnalyzer │ └── SmartAccounting.DocumentAnalyzer.API │ │ ├── Application │ │ ├── IntegrationEvents │ │ │ ├── DocumentSuccessfullyAnalyzedIntegrationEvent.cs │ │ │ ├── EventHandlers │ │ │ │ └── FileSuccessfullyUploadedEventHandler.cs │ │ │ └── FileSuccessfullyUploadedIntegrationEvent.cs │ │ ├── Model │ │ │ └── UserInvoice.cs │ │ └── Repositories │ │ │ └── IUserInvoiceRepository.cs │ │ ├── Configuration │ │ ├── CosmosDbServiceConfiguration.cs │ │ ├── EventBusConfiguration.cs │ │ ├── FormRecognizerServiceConfiguration.cs │ │ ├── SqlDbServiceConfiguration.cs │ │ └── StorageServiceConfiguration.cs │ │ ├── Core │ │ └── DependencyInjection │ │ │ ├── ConfigurationServiceCollectionExtensions.cs │ │ │ ├── DataServiceCollectionExtensions.cs │ │ │ ├── FileScanningServiceCollectionExtensions.cs │ │ │ ├── IntegrationServiceCollectionExtensions.cs │ │ │ └── StorageServiceCollectionExtensions.cs │ │ ├── Dockerfile │ │ ├── Infrastructure │ │ ├── IntegrationEvents │ │ │ └── DocumentAnalyzerEventService.cs │ │ ├── Repositories │ │ │ └── UserInvoiceRepository.cs │ │ └── Services │ │ │ ├── FileContentAnalyzerService.cs │ │ │ ├── FormRecognizerInvoiceScanner.cs │ │ │ └── StorageService.cs │ │ ├── Program.cs │ │ ├── SmartAccounting.DocumentAnalyzer.API.csproj │ │ ├── Startup.cs │ │ └── appsettings.json │ ├── FileProcessor │ └── SmartAccounting.FileProcessor.API │ │ ├── Application │ │ ├── Commands │ │ │ └── UploadFileCommand.cs │ │ ├── DTO │ │ │ └── FileToProcessDto.cs │ │ ├── ErrorHandling │ │ │ └── OperationErrorDictionary.cs │ │ ├── IntegrationEvents │ │ │ └── FileSuccessfullyUploadedIntegrationEvent.cs │ │ └── Model │ │ │ └── UserInvoice.cs │ │ ├── BackgroundServices │ │ ├── Channels │ │ │ └── FileProcessingChannel.cs │ │ └── FileProcessingBackgroundService.cs │ │ ├── Configuration │ │ ├── AzureAdB2cServiceConfiguration.cs │ │ ├── CosmosDbServiceConfiguration.cs │ │ ├── EventBusConfiguration.cs │ │ ├── SqlDbServiceConfiguration.cs │ │ ├── StorageServiceConfiguration.cs │ │ └── SwaggerConfiguration.cs │ │ ├── Controllers │ │ └── FileController.cs │ │ ├── Core │ │ └── DependencyInjection │ │ │ ├── AuthenticationServiceCollectionExtensions.cs │ │ │ ├── ConfigurationServiceCollectionExtensions.cs │ │ │ ├── IntegrationServiceCollectionExtensions.cs │ │ │ ├── StorageServiceCollectionExtensions.cs │ │ │ └── SwaggerCollectionExtensions.cs │ │ ├── Dockerfile │ │ ├── Infrastructure │ │ ├── AuthorizationPolicies │ │ │ ├── ScopesHandler.cs │ │ │ └── ScopesRequirement.cs │ │ ├── Identity │ │ │ └── IdentityService.cs │ │ ├── IntegrationEvents │ │ │ └── FileProcessorIntegrationEventService.cs │ │ └── Services │ │ │ └── StorageService.cs │ │ ├── Migrations │ │ ├── 20210618095506_initial-migration.Designer.cs │ │ ├── 20210618095506_initial-migration.cs │ │ └── EventLogContextModelSnapshot.cs │ │ ├── Program.cs │ │ ├── SmartAccounting.FileProcessor.API.csproj │ │ ├── Startup.cs │ │ └── appsettings.json │ ├── Notification │ └── SmartAccounting.Notification.API │ │ ├── Application │ │ ├── IntegrationEvents │ │ │ ├── DocumentSuccessfullyAnalyzedIntegrationEvent.cs │ │ │ └── EventHandlers │ │ │ │ └── DocumentSuccessfullyAnalyzedEventHandler.cs │ │ └── Model │ │ │ └── DocumentProcessedNotification.cs │ │ ├── CommunicationHubs │ │ └── DocumentNotificationHub.cs │ │ ├── Configuration │ │ ├── AzureAdB2cServiceConfiguration.cs │ │ ├── EventBusConfiguration.cs │ │ ├── SignalRServiceConfiguration.cs │ │ └── SqlDbServiceConfiguration.cs │ │ ├── Core │ │ └── DependencyInjection │ │ │ ├── AuthenticationServiceCollectionExtensions.cs │ │ │ ├── ConfigurationServiceCollectionExtensions.cs │ │ │ ├── IntegrationServiceCollectionExtensions.cs │ │ │ └── RealTimeNotificationServiceCollectionExtensions.cs │ │ ├── Dockerfile │ │ ├── Infrastructure │ │ ├── AuthorizationPolicies │ │ │ ├── ScopesHandler.cs │ │ │ └── ScopesRequirement.cs │ │ └── Services │ │ │ └── RealTimeNotificationService.cs │ │ ├── Program.cs │ │ ├── SmartAccounting.Notification.API.csproj │ │ ├── Startup.cs │ │ └── appsettings.json │ ├── ProcessedDocument │ └── SmartAccounting.ProcessedDocument.API │ │ ├── Application │ │ ├── DTO │ │ │ └── UserInvoiceDto.cs │ │ ├── Model │ │ │ └── UserInvoice.cs │ │ ├── Queries │ │ │ ├── GetUserInvoiceQuery.cs │ │ │ └── GetUserInvoicesQuery.cs │ │ └── Repositories │ │ │ └── UserInvoiceRepository.cs │ │ ├── Configuration │ │ ├── CosmosDbServiceConfiguration.cs │ │ ├── StorageServiceConfiguration.cs │ │ └── SwaggerConfiguration.cs │ │ ├── Controllers │ │ └── DocumentController.cs │ │ ├── Core │ │ └── DependencyInjection │ │ │ ├── AuthenticationServiceCollectionExtensions.cs │ │ │ ├── ConfigurationServiceCollectionExtensions.cs │ │ │ ├── DataServiceCollectionExtensions.cs │ │ │ ├── StorageServiceCollectionExtensions.cs │ │ │ └── SwaggerCollectionExtensions.cs │ │ ├── Dockerfile │ │ ├── Infrastructure │ │ ├── AuthorizationPolicies │ │ │ ├── ScopesHandler.cs │ │ │ └── ScopesRequirement.cs │ │ ├── Identity │ │ │ └── IdentityService.cs │ │ ├── Repositories │ │ │ └── UserInvoiceRepository.cs │ │ └── Services │ │ │ └── StorageService.cs │ │ ├── Program.cs │ │ ├── SmartAccounting.ProcessedDocument.API.csproj │ │ ├── Startup.cs │ │ └── appsettings.json │ ├── SmartAccounting.sln │ ├── docker-compose.dcproj │ ├── docker-compose.override.yml │ └── docker-compose.yml ├── smart-accounting-infrastructure ├── aks-config │ ├── aks-namespace.yml │ ├── certificates.yml │ ├── cluster-issuer.yml │ └── secret-provider-class.yml └── azure-services-config │ └── smart-accounting-infrastructure.bicep └── smart-accounting-web-app ├── azure-pipelines ├── azure-pipelines-build-template.yml ├── azure-pipelines-deployment-template.yml └── azure-pipelines.yml └── src ├── SmartAccounting.WebPortal ├── App.razor ├── Application │ ├── Infrastructure │ │ ├── ApiService.cs │ │ ├── FileProcessorApiService.cs │ │ ├── ProcessedDocumentApiService.cs │ │ └── SignalRCommunicationService.cs │ └── Model │ │ ├── DocumentProcessedNotification.cs │ │ ├── Invoice.cs │ │ └── InvoiceUpload.cs ├── AuthorizationPolicies │ ├── ClaimAuthorizationHandler.cs │ └── ClaimRequirement.cs ├── Pages │ ├── Error.cshtml │ ├── Error.cshtml.cs │ ├── Index.razor │ ├── MyInvoices.razor │ ├── Upload.razor │ └── _Host.cshtml ├── Program.cs ├── Shared │ ├── LoginDisplay.razor │ ├── MainLayout.razor │ ├── MainLayout.razor.css │ ├── NavMenu.razor │ ├── NavMenu.razor.css │ └── SurveyPrompt.razor ├── SmartAccounting.WebPortal.csproj ├── Startup.cs ├── _Imports.razor ├── appsettings.json └── wwwroot │ ├── css │ ├── bootstrap │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ ├── open-iconic │ │ ├── FONT-LICENSE │ │ ├── ICON-LICENSE │ │ ├── README.md │ │ └── font │ │ │ ├── css │ │ │ └── open-iconic-bootstrap.min.css │ │ │ └── fonts │ │ │ ├── open-iconic.eot │ │ │ ├── open-iconic.otf │ │ │ ├── open-iconic.svg │ │ │ ├── open-iconic.ttf │ │ │ └── open-iconic.woff │ └── site.css │ ├── favicon.ico │ └── images │ └── logo.png └── SmartAccounting.sln /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Daniel-Krzyczkowski] 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Daniel Krzyczkowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /images/github-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/images/github-start.png -------------------------------------------------------------------------------- /images/smart-accounting-deployment-diag-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/images/smart-accounting-deployment-diag-1.png -------------------------------------------------------------------------------- /images/smart-accounting-deployment-diag-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/images/smart-accounting-deployment-diag-2.png -------------------------------------------------------------------------------- /images/smart-accounting1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/images/smart-accounting1.PNG -------------------------------------------------------------------------------- /images/smart-accounting10.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/images/smart-accounting10.PNG -------------------------------------------------------------------------------- /images/smart-accounting11.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/images/smart-accounting11.PNG -------------------------------------------------------------------------------- /images/smart-accounting2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/images/smart-accounting2.PNG -------------------------------------------------------------------------------- /images/smart-accounting3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/images/smart-accounting3.PNG -------------------------------------------------------------------------------- /images/smart-accounting4.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/images/smart-accounting4.PNG -------------------------------------------------------------------------------- /images/smart-accounting5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/images/smart-accounting5.png -------------------------------------------------------------------------------- /images/smart-accounting6.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/images/smart-accounting6.PNG -------------------------------------------------------------------------------- /images/smart-accounting7.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/images/smart-accounting7.PNG -------------------------------------------------------------------------------- /images/smart-accounting8.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/images/smart-accounting8.PNG -------------------------------------------------------------------------------- /images/smart-accounting9.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/images/smart-accounting9.PNG -------------------------------------------------------------------------------- /sample-files/Invoice01.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/sample-files/Invoice01.PNG -------------------------------------------------------------------------------- /sample-files/Invoice02.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/sample-files/Invoice02.PNG -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/azure-pipelines/azure-pipelines-build-template.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | - job: BuildAndPush 3 | displayName: "Build and push Docker containers" 4 | pool: 5 | vmImage: ubuntu-latest 6 | steps: 7 | - task: DockerCompose@0 8 | displayName: 'Build Docker images' 9 | inputs: 10 | containerregistrytype: 'Azure Container Registry' 11 | azureSubscription: ${{parameters.azureConnectionName}} 12 | azureContainerRegistry: ${{parameters.azureContainerRegistry}} 13 | dockerComposeFile: '**/docker-compose.yml' 14 | action: 'Build services' 15 | additionalImageTags: '$(Build.BuildId)' 16 | includeLatestTag: true 17 | 18 | - task: DockerCompose@0 19 | displayName: 'Push Docker images to Azure Container Registry' 20 | inputs: 21 | containerregistrytype: 'Azure Container Registry' 22 | azureSubscription: ${{parameters.azureConnectionName}} 23 | azureContainerRegistry: ${{parameters.azureContainerRegistry}} 24 | dockerComposeFile: '**/docker-compose.yml' 25 | action: 'Push services' 26 | additionalImageTags: '$(Build.BuildId)' 27 | includeLatestTag: true 28 | 29 | - task: PublishPipelineArtifact@1 30 | displayName: 'Publish AKS deployment manifests' 31 | inputs: 32 | artifactName: 'manifests' 33 | path: '$(System.DefaultWorkingDirectory)/kubernetes' -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/azure-pipelines/azure-pipelines-deployment-template.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | - deployment: Deploy 3 | displayName: 'Deploy docker containers to AKS cluster' 4 | pool: 5 | vmImage: ubuntu-latest 6 | environment: ${{parameters.environment}} 7 | strategy: 8 | runOnce: 9 | deploy: 10 | steps: 11 | - task: DownloadPipelineArtifact@2 12 | displayName: 'Download AKS deployment manifests' 13 | inputs: 14 | artifactName: 'manifests' 15 | downloadPath: '$(System.ArtifactsDirectory)/manifests' 16 | 17 | - task: KubernetesManifest@0 18 | displayName: 'Deploy services to AKS cluster' 19 | inputs: 20 | action: 'deploy' 21 | kubernetesServiceConnection: ${{parameters.kubernetesConnectionName}} 22 | namespace: ${{parameters.kubernetesNamespace}} 23 | manifests: | 24 | $(System.ArtifactsDirectory)/manifests/document-analyzer-microservice.yml 25 | $(System.ArtifactsDirectory)/manifests/file-processor-microservice.yml 26 | $(System.ArtifactsDirectory)/manifests/notification-microservice.yml 27 | $(System.ArtifactsDirectory)/manifests/processed-document-microservice.yml 28 | 29 | - task: Kubernetes@1 30 | displayName: Update ingress routes 31 | inputs: 32 | connectionType: Azure Resource Manager 33 | azureSubscriptionEndpoint: ${{parameters.azureConnectionName}} 34 | azureResourceGroup: ${{parameters.azureResourceGroup}} 35 | kubernetesCluster: ${{parameters.kubernetesClusterName}} 36 | command: apply 37 | arguments: -f $(System.ArtifactsDirectory)/manifests/create-ingress.yml -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/azure-pipelines/azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - develop 3 | 4 | stages: 5 | 6 | - stage: Build 7 | displayName: 'Build and push Docker containers' 8 | variables: 9 | - group: 'smart-accounting-backend-dev-env-vg' 10 | jobs: 11 | - template: azure-pipelines-build-template.yml 12 | parameters: 13 | azureConnectionName: '$(azureConnectionName)' 14 | azureContainerRegistry: '$(azureContainerRegistry)' 15 | 16 | - stage: DeployDEV 17 | displayName: 'Deploy docker containers to AKS cluster' 18 | condition: succeeded() 19 | dependsOn: Build 20 | variables: 21 | - group: 'smart-accounting-backend-dev-env-vg' 22 | jobs: 23 | - template: azure-pipelines-deployment-template.yml 24 | parameters: 25 | azureConnectionName: '$(azureConnectionName)' 26 | kubernetesConnectionName: '$(kubernetesConnectionName)' 27 | azureContainerRegistry: '$(azureContainerRegistry)' 28 | azureResourceGroup: '$(azureResourceGroup)' 29 | kubernetesNamespace: '$(kubernetesNamespace)' 30 | kubernetesClusterName: '$(kubernetesClusterName)' 31 | environment: '$(environment)' -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/kubernetes/create-ingress.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: dev-smart-accounting-ingress 5 | namespace: dev-smart-accounting-services 6 | annotations: 7 | kubernetes.io/ingress.class: nginx 8 | nginx.ingress.kubernetes.io/rewrite-target: /$2 9 | cert-manager.io/cluster-issuer: letsencrypt-prod 10 | nginx.ingress.kubernetes.io/ssl-redirect: "false" 11 | nginx.ingress.kubernetes.io/affinity: cookie 12 | nginx.ingress.kubernetes.io/session-cookie-hash: sha1 13 | nginx.ingress.kubernetes.io/session-cookie-name: REALTIMESERVERID 14 | 15 | spec: 16 | tls: 17 | - hosts: 18 | - smart-accounting.westeurope.cloudapp.azure.com 19 | secretName: tls-secret 20 | rules: 21 | - host: smart-accounting.westeurope.cloudapp.azure.com 22 | http: 23 | paths: 24 | - path: /file-processor-api(/|$)(.*) 25 | pathType: Prefix 26 | backend: 27 | service: 28 | name: file-processor-api-svc 29 | port: 30 | number: 80 31 | - path: /processed-document-api(/|$)(.*) 32 | pathType: Prefix 33 | backend: 34 | service: 35 | name: processed-document-api-svc 36 | port: 37 | number: 80 38 | - path: /notification-api(/|$)(.*) 39 | pathType: Prefix 40 | backend: 41 | service: 42 | name: notification-api-svc 43 | port: 44 | number: 80 -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/kubernetes/document-analyzer-microservice.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: document-analyzer-api-dep 5 | namespace: dev-smart-accounting-services 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: document-analyzer-api 10 | replicas: 3 # tells deployment to run 3 pods matching the template 11 | strategy: 12 | type: RollingUpdate 13 | rollingUpdate: 14 | maxUnavailable: 1 15 | template: # create pods using pod definition in this template 16 | metadata: 17 | labels: 18 | app: document-analyzer-api 19 | aadpodidbinding: "aks-pods-identity" 20 | spec: 21 | containers: 22 | - name: document-analyzer-api-container 23 | image: acrsmartaccounting.azurecr.io/documentanalyzerapi:latest 24 | imagePullPolicy: Always 25 | env: 26 | - name: FormRecognizerConfig__Key 27 | valueFrom: 28 | secretKeyRef: 29 | name: form-recognizer-key 30 | key: form-recognizer-key 31 | - name: ApplicationInsightsConfig__InstrumentationKey 32 | valueFrom: 33 | secretKeyRef: 34 | name: application-insights-key 35 | key: application-insights-key 36 | - name: CosmosDbConfig__ConnectionString 37 | valueFrom: 38 | secretKeyRef: 39 | name: cosmos-db-connection-string 40 | key: cosmos-db-connection-string 41 | - name: SqlDbConfig__ConnectionString 42 | valueFrom: 43 | secretKeyRef: 44 | name: sql-db-connection-string 45 | key: sql-db-connection-string 46 | - name: BlobStorageConfig__ConnectionString 47 | valueFrom: 48 | secretKeyRef: 49 | name: storage-account-connection-string 50 | key: storage-account-connection-string 51 | - name: BlobStorageConfig__Key 52 | valueFrom: 53 | secretKeyRef: 54 | name: storage-account-key 55 | key: storage-account-key 56 | - name: ServiceBusConfig__ListenAndSendConnectionString 57 | valueFrom: 58 | secretKeyRef: 59 | name: service-bus-connection-string 60 | key: service-bus-connection-string 61 | ports: 62 | - containerPort: 80 63 | volumeMounts: 64 | - mountPath: "/mnt/secrets-store" 65 | name: secrets-store-inline 66 | readOnly: true 67 | volumes: 68 | - name: secrets-store-inline 69 | csi: 70 | driver: secrets-store.csi.k8s.io 71 | readOnly: true 72 | volumeAttributes: 73 | secretProviderClass: "kv-smart-accounting" 74 | 75 | --- 76 | 77 | apiVersion: v1 78 | kind: Service 79 | metadata: 80 | name: document-analyzer-api-svc 81 | namespace: dev-smart-accounting-services 82 | spec: 83 | type: ClusterIP 84 | selector: 85 | app: document-analyzer-api 86 | ports: 87 | - name: https 88 | port: 443 89 | targetPort: 80 -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/kubernetes/file-processor-microservice.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: file-processor-api-dep 5 | namespace: dev-smart-accounting-services 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: file-processor-api 10 | replicas: 3 # tells deployment to run 3 pods matching the template 11 | strategy: 12 | type: RollingUpdate 13 | rollingUpdate: 14 | maxUnavailable: 1 15 | template: # create pods using pod definition in this template 16 | metadata: 17 | labels: 18 | app: file-processor-api 19 | aadpodidbinding: "aks-pods-identity" 20 | spec: 21 | containers: 22 | - name: file-processor-api-container 23 | image: acrsmartaccounting.azurecr.io/fileprocessorapi:latest 24 | imagePullPolicy: Always 25 | env: 26 | - name: BlobStorageConfig__ConnectionString 27 | valueFrom: 28 | secretKeyRef: 29 | name: storage-account-connection-string 30 | key: storage-account-connection-string 31 | - name: BlobStorageConfig__Key 32 | valueFrom: 33 | secretKeyRef: 34 | name: storage-account-key 35 | key: storage-account-key 36 | - name: ServiceBusConfig__ListenAndSendConnectionString 37 | valueFrom: 38 | secretKeyRef: 39 | name: service-bus-connection-string 40 | key: service-bus-connection-string 41 | - name: ApplicationInsightsConfig__InstrumentationKey 42 | valueFrom: 43 | secretKeyRef: 44 | name: application-insights-key 45 | key: application-insights-key 46 | - name: CosmosDbConfig__ConnectionString 47 | valueFrom: 48 | secretKeyRef: 49 | name: cosmos-db-connection-string 50 | key: cosmos-db-connection-string 51 | - name: SqlDbConfig__ConnectionString 52 | valueFrom: 53 | secretKeyRef: 54 | name: sql-db-connection-string 55 | key: sql-db-connection-string 56 | ports: 57 | - containerPort: 80 58 | volumeMounts: 59 | - mountPath: "/mnt/secrets-store" 60 | name: secrets-store-inline 61 | readOnly: true 62 | volumes: 63 | - name: secrets-store-inline 64 | csi: 65 | driver: secrets-store.csi.k8s.io 66 | readOnly: true 67 | volumeAttributes: 68 | secretProviderClass: "kv-smart-accounting" 69 | 70 | --- 71 | 72 | apiVersion: v1 73 | kind: Service 74 | metadata: 75 | name: file-processor-api-svc 76 | namespace: dev-smart-accounting-services 77 | spec: 78 | type: ClusterIP 79 | selector: 80 | app: file-processor-api 81 | ports: 82 | - name: https 83 | port: 443 84 | targetPort: 80 -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/kubernetes/notification-microservice.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: notification-api-dep 5 | namespace: dev-smart-accounting-services 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: notification-api 10 | replicas: 3 # tells deployment to run 3 pods matching the template 11 | strategy: 12 | type: RollingUpdate 13 | rollingUpdate: 14 | maxUnavailable: 1 15 | template: # create pods using pod definition in this template 16 | metadata: 17 | labels: 18 | app: notification-api 19 | aadpodidbinding: "aks-pods-identity" 20 | spec: 21 | containers: 22 | - name: notification-api-container 23 | image: acrsmartaccounting.azurecr.io/notificationapi:latest 24 | imagePullPolicy: Always 25 | env: 26 | - name: AzureSignalRConfig__ConnectionString 27 | valueFrom: 28 | secretKeyRef: 29 | name: signalr-connection-string 30 | key: signalr-connection-string 31 | - name: ApplicationInsightsConfig__InstrumentationKey 32 | valueFrom: 33 | secretKeyRef: 34 | name: application-insights-key 35 | key: application-insights-key 36 | - name: SqlDbConfig__ConnectionString 37 | valueFrom: 38 | secretKeyRef: 39 | name: sql-db-connection-string 40 | key: sql-db-connection-string 41 | - name: ServiceBusConfig__ListenAndSendConnectionString 42 | valueFrom: 43 | secretKeyRef: 44 | name: service-bus-connection-string 45 | key: service-bus-connection-string 46 | ports: 47 | - containerPort: 80 48 | volumeMounts: 49 | - mountPath: "/mnt/secrets-store" 50 | name: secrets-store-inline 51 | readOnly: true 52 | volumes: 53 | - name: secrets-store-inline 54 | csi: 55 | driver: secrets-store.csi.k8s.io 56 | readOnly: true 57 | volumeAttributes: 58 | secretProviderClass: "kv-smart-accounting" 59 | 60 | --- 61 | 62 | apiVersion: v1 63 | kind: Service 64 | metadata: 65 | name: notification-api-svc 66 | namespace: dev-smart-accounting-services 67 | spec: 68 | type: ClusterIP 69 | selector: 70 | app: notification-api 71 | ports: 72 | - name: https 73 | port: 443 74 | targetPort: 80 -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/kubernetes/processed-document-microservice.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: processed-document-api-dep 5 | namespace: dev-smart-accounting-services 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: processed-document-api 10 | replicas: 3 # tells deployment to run 3 pods matching the template 11 | strategy: 12 | type: RollingUpdate 13 | rollingUpdate: 14 | maxUnavailable: 1 15 | template: # create pods using pod definition in this template 16 | metadata: 17 | labels: 18 | app: processed-document-api 19 | aadpodidbinding: "aks-pods-identity" 20 | spec: 21 | containers: 22 | - name: processed-document-api-container 23 | image: acrsmartaccounting.azurecr.io/processeddocumentapi:latest 24 | imagePullPolicy: Always 25 | env: 26 | - name: CosmosDbConfig__ConnectionString 27 | valueFrom: 28 | secretKeyRef: 29 | name: cosmos-db-connection-string 30 | key: cosmos-db-connection-string 31 | - name: BlobStorageConfig__ConnectionString 32 | valueFrom: 33 | secretKeyRef: 34 | name: storage-account-connection-string 35 | key: storage-account-connection-string 36 | - name: BlobStorageConfig__Key 37 | valueFrom: 38 | secretKeyRef: 39 | name: storage-account-key 40 | key: storage-account-key 41 | - name: ApplicationInsightsConfig__InstrumentationKey 42 | valueFrom: 43 | secretKeyRef: 44 | name: application-insights-key 45 | key: application-insights-key 46 | ports: 47 | - containerPort: 80 48 | volumeMounts: 49 | - mountPath: "/mnt/secrets-store" 50 | name: secrets-store-inline 51 | readOnly: true 52 | volumes: 53 | - name: secrets-store-inline 54 | csi: 55 | driver: secrets-store.csi.k8s.io 56 | readOnly: true 57 | volumeAttributes: 58 | secretProviderClass: "kv-smart-accounting" 59 | 60 | --- 61 | 62 | apiVersion: v1 63 | kind: Service 64 | metadata: 65 | name: processed-document-api-svc 66 | namespace: dev-smart-accounting-services 67 | spec: 68 | type: ClusterIP 69 | selector: 70 | app: processed-document-api 71 | ports: 72 | - name: https 73 | port: 443 74 | targetPort: 80 -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/BuildingBlocks/SmartAccounting.Common/CommonResponse/OperationError.cs: -------------------------------------------------------------------------------- 1 | namespace SmartAccounting.Common.CommonResponse 2 | { 3 | public record OperationError 4 | { 5 | public string Details { get; } 6 | 7 | public OperationError(string details) => (Details) = (details); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/BuildingBlocks/SmartAccounting.Common/CommonResponse/OperationResponse.cs: -------------------------------------------------------------------------------- 1 | namespace SmartAccounting.Common.CommonResponse 2 | { 3 | public class OperationResponse 4 | { 5 | protected bool _forcedFailedResponse; 6 | public bool CompletedWithSuccess => OperationError == null && !_forcedFailedResponse; 7 | public OperationError OperationError { get; set; } 8 | 9 | public OperationResponse SetAsFailureResponse(OperationError operationError) 10 | { 11 | OperationError = operationError; 12 | _forcedFailedResponse = true; 13 | return this; 14 | } 15 | } 16 | 17 | public class OperationResponse : OperationResponse 18 | { 19 | public OperationResponse() { } 20 | public OperationResponse(T result) 21 | { 22 | Result = result; 23 | } 24 | 25 | public T Result { get; set; } 26 | 27 | public new OperationResponse SetAsFailureResponse(OperationError operationError) 28 | { 29 | base.SetAsFailureResponse(operationError); 30 | return this; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/BuildingBlocks/SmartAccounting.Common/ExceptionMiddleware/ApiExceptionMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Net; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | 8 | namespace SmartAccounting.Common.ExceptionMiddleware 9 | { 10 | public class ApiExceptionMiddleware 11 | { 12 | private readonly RequestDelegate _next; 13 | private readonly ILogger _logger; 14 | 15 | public ApiExceptionMiddleware(RequestDelegate next, 16 | ILogger logger) 17 | { 18 | _next = next; 19 | _logger = logger; 20 | } 21 | 22 | public async Task Invoke(HttpContext context) 23 | { 24 | try 25 | { 26 | await _next(context); 27 | } 28 | catch (Exception ex) 29 | { 30 | await HandleExceptionAsync(context, ex); 31 | } 32 | } 33 | 34 | private Task HandleExceptionAsync(HttpContext context, Exception exception) 35 | { 36 | var error = new ApiRequestProcessingError 37 | { 38 | Id = Guid.NewGuid().ToString(), 39 | Code = (short)HttpStatusCode.InternalServerError, 40 | Title = "Some kind of error occurred in the API.", 41 | Detail = "Please use the id and contact our support team if the problem persists." 42 | }; 43 | 44 | var innerExMessage = exception.GetBaseException().Message; 45 | 46 | _logger.LogError(exception, "API Error: " + "{ErrorMessage} -- {ErrorId}.", innerExMessage, error.Id); 47 | 48 | var result = JsonSerializer.Serialize(error); 49 | context.Response.ContentType = "application/json"; 50 | context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; 51 | return context.Response.WriteAsync(result); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/BuildingBlocks/SmartAccounting.Common/ExceptionMiddleware/ApiRequestProcessingError.cs: -------------------------------------------------------------------------------- 1 | namespace SmartAccounting.Common.ExceptionMiddleware 2 | { 3 | public class ApiRequestProcessingError 4 | { 5 | public string Id { get; set; } 6 | public short Code { get; set; } 7 | public string Title { get; set; } 8 | public string Detail { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/BuildingBlocks/SmartAccounting.Common/SmartAccounting.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/BuildingBlocks/SmartAccounting.EventBus/Configuration/IEventBusConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace SmartAccounting.EventBus.Configuration 2 | { 3 | public interface IEventBusConfiguration 4 | { 5 | string ListenAndSendConnectionString { get; set; } 6 | string TopicName { get; set; } 7 | string Subscription { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/BuildingBlocks/SmartAccounting.EventBus/IntegrationEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace SmartAccounting.EventBus 5 | { 6 | public abstract record IntegrationEvent 7 | { 8 | [JsonPropertyName("id")] 9 | public Guid Id { get; init; } 10 | 11 | [JsonPropertyName("creationDate")] 12 | public DateTime CreationDate { get; init; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/BuildingBlocks/SmartAccounting.EventBus/Interfaces/IEventBus.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace SmartAccounting.EventBus.Interfaces 4 | { 5 | public interface IEventBus 6 | { 7 | Task PublishAsync(IntegrationEvent @event); 8 | 9 | Task SubscribeAsync() 10 | where T : IntegrationEvent 11 | where TH : IIntegrationEventHandler; 12 | 13 | Task UnsubscribeAsync() 14 | where TH : IIntegrationEventHandler 15 | where T : IntegrationEvent; 16 | 17 | Task SetupAsync(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/BuildingBlocks/SmartAccounting.EventBus/Interfaces/IEventBusSubscriptionsManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SmartAccounting.EventBus.Interfaces 5 | { 6 | public interface IEventBusSubscriptionsManager 7 | { 8 | bool IsEmpty { get; } 9 | 10 | event EventHandler OnEventRemoved; 11 | 12 | void AddSubscription() 13 | where T : IntegrationEvent 14 | where TH : IIntegrationEventHandler; 15 | 16 | void RemoveSubscription() 17 | where TH : IIntegrationEventHandler 18 | where T : IntegrationEvent; 19 | 20 | bool HasSubscriptionsForEvent() where T : IntegrationEvent; 21 | 22 | bool HasSubscriptionsForEvent(string eventName); 23 | 24 | Type GetEventTypeByName(string eventName); 25 | 26 | void Clear(); 27 | 28 | IEnumerable GetHandlersForEvent() where T : IntegrationEvent; 29 | 30 | IEnumerable GetHandlersForEvent(string eventName); 31 | 32 | string GetEventKey(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/BuildingBlocks/SmartAccounting.EventBus/Interfaces/IIntegrationEventHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace SmartAccounting.EventBus.Interfaces 4 | { 5 | public interface IIntegrationEventHandler 6 | where TIntegrationEvent : IntegrationEvent 7 | { 8 | Task HandleAsync(TIntegrationEvent @event); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/BuildingBlocks/SmartAccounting.EventBus/SmartAccounting.EventBus.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/BuildingBlocks/SmartAccounting.EventBus/SubscriptionInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SmartAccounting.EventBus 4 | { 5 | public class SubscriptionInfo 6 | { 7 | public Type HandlerType { get; } 8 | 9 | private SubscriptionInfo(Type handlerType) 10 | { 11 | HandlerType = handlerType; 12 | } 13 | 14 | public static SubscriptionInfo Typed(Type handlerType) 15 | { 16 | return new SubscriptionInfo(handlerType); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/BuildingBlocks/SmartAccounting.EventLog/EventLogContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | 4 | namespace SmartAccounting.EventLog 5 | { 6 | public class EventLogContext : DbContext 7 | { 8 | public EventLogContext(DbContextOptions options) : base(options) 9 | { 10 | } 11 | 12 | public DbSet IntegrationEventLogs { get; set; } 13 | 14 | protected override void OnModelCreating(ModelBuilder builder) 15 | { 16 | builder.Entity(ConfigureIntegrationEventLogEntry); 17 | } 18 | 19 | private void ConfigureIntegrationEventLogEntry(EntityTypeBuilder builder) 20 | { 21 | builder.ToTable("IntegrationEventLog"); 22 | 23 | builder.HasKey(e => e.EventId); 24 | 25 | builder.Property(e => e.EventId) 26 | .IsRequired(); 27 | 28 | builder.Property(e => e.Content) 29 | .IsRequired(); 30 | 31 | builder.Property(e => e.CreationTime) 32 | .IsRequired(); 33 | 34 | builder.Property(e => e.State) 35 | .IsRequired(); 36 | 37 | builder.Property(e => e.TimesSent) 38 | .IsRequired(); 39 | 40 | builder.Property(e => e.EventTypeName) 41 | .IsRequired(); 42 | 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/BuildingBlocks/SmartAccounting.EventLog/EventLogService.cs: -------------------------------------------------------------------------------- 1 | using SmartAccounting.EventBus; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Threading.Tasks; 7 | 8 | namespace SmartAccounting.EventLog 9 | { 10 | public interface IEventLogService 11 | { 12 | Task SaveEventAsync(IntegrationEvent @event); 13 | Task MarkEventAsPublishedAsync(Guid eventId); 14 | Task MarkEventAsInProgressAsync(Guid eventId); 15 | Task MarkEventAsFailedAsync(Guid eventId); 16 | } 17 | 18 | public class EventLogService : IEventLogService 19 | { 20 | private readonly EventLogContext _integrationEventLogContext; 21 | private readonly List _eventTypes; 22 | 23 | public EventLogService(EventLogContext integrationEventLogContext) 24 | { 25 | _integrationEventLogContext = integrationEventLogContext; 26 | 27 | _eventTypes = Assembly.Load(Assembly.GetEntryAssembly().FullName) 28 | .GetTypes() 29 | .Where(t => t.Name.EndsWith(nameof(IntegrationEvent))) 30 | .ToList(); 31 | } 32 | 33 | public Task SaveEventAsync(IntegrationEvent @event) 34 | { 35 | var eventLogEntry = new IntegrationEventLogEntry(@event); 36 | _integrationEventLogContext.IntegrationEventLogs.Add(eventLogEntry); 37 | return _integrationEventLogContext.SaveChangesAsync(); 38 | } 39 | 40 | public Task MarkEventAsPublishedAsync(Guid eventId) 41 | { 42 | return UpdateEventStatus(eventId, EventState.Published); 43 | } 44 | 45 | public Task MarkEventAsInProgressAsync(Guid eventId) 46 | { 47 | return UpdateEventStatus(eventId, EventState.InProgress); 48 | } 49 | 50 | public Task MarkEventAsFailedAsync(Guid eventId) 51 | { 52 | return UpdateEventStatus(eventId, EventState.PublishedFailed); 53 | } 54 | 55 | private Task UpdateEventStatus(Guid eventId, EventState status) 56 | { 57 | var eventLogEntry = _integrationEventLogContext.IntegrationEventLogs.Single(ie => ie.EventId == eventId); 58 | eventLogEntry.State = status; 59 | 60 | if (status == EventState.InProgress) 61 | eventLogEntry.TimesSent++; 62 | 63 | _integrationEventLogContext.IntegrationEventLogs.Update(eventLogEntry); 64 | 65 | return _integrationEventLogContext.SaveChangesAsync(); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/BuildingBlocks/SmartAccounting.EventLog/EventState.cs: -------------------------------------------------------------------------------- 1 | namespace SmartAccounting.EventLog 2 | { 3 | public enum EventState 4 | { 5 | NotPublished = 0, 6 | InProgress = 1, 7 | Published = 2, 8 | PublishedFailed = 3 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/BuildingBlocks/SmartAccounting.EventLog/IntegrationEventLogEntry.cs: -------------------------------------------------------------------------------- 1 | using SmartAccounting.EventBus; 2 | using System; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using System.Linq; 5 | using System.Text.Json; 6 | 7 | namespace SmartAccounting.EventLog 8 | { 9 | public class IntegrationEventLogEntry 10 | { 11 | private IntegrationEventLogEntry() { } 12 | public IntegrationEventLogEntry(IntegrationEvent @event) 13 | { 14 | EventId = @event.Id; 15 | CreationTime = @event.CreationDate; 16 | EventTypeName = @event.GetType().FullName; 17 | Content = JsonSerializer.Serialize(@event); 18 | State = EventState.NotPublished; 19 | TimesSent = 0; 20 | } 21 | 22 | public Guid EventId { get; private set; } 23 | public string EventTypeName { get; private set; } 24 | [NotMapped] 25 | public string EventTypeShortName => EventTypeName.Split('.')?.Last(); 26 | [NotMapped] 27 | public IntegrationEvent IntegrationEvent { get; private set; } 28 | public EventState State { get; set; } 29 | public int TimesSent { get; set; } 30 | public DateTime CreationTime { get; private set; } 31 | public string Content { get; private set; } 32 | public string TransactionId { get; private set; } 33 | 34 | public IntegrationEventLogEntry DeserializeJsonContent(Type type) 35 | { 36 | IntegrationEvent = JsonSerializer.Deserialize(Content, type) as IntegrationEvent; 37 | return this; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/BuildingBlocks/SmartAccounting.EventLog/SmartAccounting.EventLog.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/BuildingBlocks/SmartAccounting.Logging/Configuration/ApplicationInsightsServiceConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace SmartAccounting.Logging.Configuration 4 | { 5 | public interface IApplicationInsightsServiceConfiguration 6 | { 7 | string InstrumentationKey { get; set; } 8 | } 9 | 10 | public class ApplicationInsightsServiceConfiguration : IApplicationInsightsServiceConfiguration 11 | { 12 | public string InstrumentationKey { get; set; } 13 | } 14 | 15 | public class ApplicationInsightsServiceConfigurationValidation : IValidateOptions 16 | { 17 | public ValidateOptionsResult Validate(string name, ApplicationInsightsServiceConfiguration options) 18 | { 19 | if (string.IsNullOrEmpty(options.InstrumentationKey)) 20 | { 21 | return ValidateOptionsResult.Fail($"{nameof(options.InstrumentationKey)} configuration parameter for the Azure Application Insights is required"); 22 | } 23 | 24 | return ValidateOptionsResult.Success; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/BuildingBlocks/SmartAccounting.Logging/LoggingServicesInitializer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.ApplicationInsights.Channel; 2 | using Microsoft.ApplicationInsights.DataContracts; 3 | using Microsoft.ApplicationInsights.Extensibility; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.Extensions.Logging.ApplicationInsights; 7 | using SmartAccounting.Logging.Configuration; 8 | using System.Linq; 9 | 10 | namespace SmartAccounting.Logging 11 | { 12 | public static class LoggingServicesInitializer 13 | { 14 | public static IServiceCollection AddLoggingServices(this IServiceCollection services) 15 | { 16 | services.AddLogging(loggingBuilder => 17 | { 18 | var serviceProvider = loggingBuilder.Services.BuildServiceProvider(); 19 | var azureApplicationInsightsConfiguration = serviceProvider.GetRequiredService(); 20 | 21 | string instrumentationKey = azureApplicationInsightsConfiguration.InstrumentationKey; 22 | 23 | loggingBuilder.AddApplicationInsights(instrumentationKey); 24 | loggingBuilder.AddFilter("", LogLevel.Warning); 25 | }); 26 | 27 | return services; 28 | } 29 | } 30 | 31 | 32 | #region AdditionalConfig 33 | 34 | public class CustomTelemetryInitializer : ITelemetryInitializer 35 | { 36 | public void Initialize(ITelemetry telemetry) 37 | { 38 | ISupportProperties propTelematry = (ISupportProperties)telemetry; 39 | 40 | var removeProps = new[] { "RequestId" }; 41 | removeProps = removeProps.Where(prop => propTelematry.Properties.ContainsKey(prop)).ToArray(); 42 | 43 | foreach (var prop in removeProps) 44 | { 45 | propTelematry.Properties.Remove(prop); 46 | } 47 | } 48 | } 49 | 50 | public class SuccessfulDependencyFilter : ITelemetryProcessor 51 | { 52 | private ITelemetryProcessor Next { get; set; } 53 | 54 | public SuccessfulDependencyFilter(ITelemetryProcessor next) 55 | { 56 | Next = next; 57 | } 58 | 59 | public void Process(ITelemetry item) 60 | { 61 | if (!OKtoSend(item)) { return; } 62 | 63 | Next.Process(item); 64 | } 65 | 66 | private bool OKtoSend(ITelemetry item) 67 | { 68 | var dependency = item as DependencyTelemetry; 69 | if (dependency == null) return true; 70 | 71 | return dependency.Success != true; 72 | } 73 | } 74 | #endregion AdditionalConfig 75 | } 76 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/BuildingBlocks/SmartAccounting.Logging/SmartAccounting.Logging.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/Application/IntegrationEvents/DocumentSuccessfullyAnalyzedIntegrationEvent.cs: -------------------------------------------------------------------------------- 1 | using SmartAccounting.EventBus; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace SmartAccounting.DocumentAnalyzer.API.Application.IntegrationEvents 5 | { 6 | internal record DocumentSuccessfullyAnalyzedIntegrationEvent : IntegrationEvent 7 | { 8 | [JsonPropertyName("userId")] 9 | public string UserId { get; init; } 10 | [JsonPropertyName("invoiceId")] 11 | public string InvoiceId { get; init; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/Application/IntegrationEvents/EventHandlers/FileSuccessfullyUploadedEventHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using SmartAccounting.DocumentAnalyzer.API.Infrastructure.Services; 3 | using SmartAccounting.EventBus.Interfaces; 4 | using System; 5 | using System.Threading.Tasks; 6 | 7 | namespace SmartAccounting.DocumentAnalyzer.API.Application.IntegrationEvents.EventHandlers 8 | { 9 | internal class FileSuccessfullyUploadedEventHandler : IIntegrationEventHandler 10 | { 11 | private readonly ILogger _logger; 12 | private readonly IFileContentAnalyzerService _fileContentAnalyzerService; 13 | 14 | public FileSuccessfullyUploadedEventHandler(ILogger logger, 15 | IFileContentAnalyzerService fileContentAnalyzerService) 16 | { 17 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 18 | _fileContentAnalyzerService = fileContentAnalyzerService 19 | ?? throw new ArgumentNullException(nameof(fileContentAnalyzerService)); 20 | } 21 | 22 | public async Task HandleAsync(FileSuccessfullyUploadedIntegrationEvent @event) 23 | { 24 | if (!string.IsNullOrEmpty(@event.FileUrl) 25 | && !string.IsNullOrEmpty(@event.UserId)) 26 | { 27 | await _fileContentAnalyzerService.ScanDocumentAndExtractContentAsync(@event.FileUrl, @event.UserId); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/Application/IntegrationEvents/FileSuccessfullyUploadedIntegrationEvent.cs: -------------------------------------------------------------------------------- 1 | using SmartAccounting.EventBus; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace SmartAccounting.DocumentAnalyzer.API.Application.IntegrationEvents 5 | { 6 | internal record FileSuccessfullyUploadedIntegrationEvent : IntegrationEvent 7 | { 8 | [JsonPropertyName("userId")] 9 | public string UserId { get; init; } 10 | [JsonPropertyName("fileUrl")] 11 | public string FileUrl { get; init; } 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/Application/Model/UserInvoice.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace SmartAccounting.DocumentAnalyzer.API.Application.Model 5 | { 6 | internal class UserInvoice 7 | { 8 | [JsonPropertyName("id")] 9 | public string Id { get; set; } 10 | 11 | [JsonPropertyName("userId")] 12 | public string UserId { get; set; } 13 | 14 | [JsonPropertyName("invoiceDate")] 15 | public DateTime InvoiceDate { get; set; } 16 | 17 | [JsonPropertyName("customerAddress")] 18 | public string CustomerAddress { get; set; } 19 | 20 | [JsonPropertyName("customerAddressRecipient")] 21 | public string CustomerAddressRecipient { get; set; } 22 | 23 | [JsonPropertyName("customerName")] 24 | public string CustomerName { get; set; } 25 | 26 | [JsonPropertyName("dueDate")] 27 | public DateTime DueDate { get; set; } 28 | 29 | [JsonPropertyName("invoiceId")] 30 | public string InvoiceId { get; set; } 31 | 32 | [JsonPropertyName("vendorAddress")] 33 | public string VendorAddress { get; set; } 34 | 35 | [JsonPropertyName("vendorName")] 36 | public string VendorName { get; set; } 37 | 38 | [JsonPropertyName("invoiceTotal")] 39 | public float InvoiceTotal { get; set; } 40 | 41 | [JsonPropertyName("fileUrl")] 42 | public string FileUrl { get; set; } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/Application/Repositories/IUserInvoiceRepository.cs: -------------------------------------------------------------------------------- 1 | using SmartAccounting.DocumentAnalyzer.API.Application.Model; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace SmartAccounting.DocumentAnalyzer.API.Application.Repositories 6 | { 7 | internal interface IUserInvoiceRepository 8 | { 9 | Task AddAsync(UserInvoice userInvoice); 10 | Task DeleteAsync(UserInvoice userInvoice); 11 | Task GetAsync(UserInvoice userInvoice); 12 | Task UpdateAsync(UserInvoice userInvoice); 13 | Task> GetAllAsync(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/Configuration/CosmosDbServiceConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace SmartAccounting.DocumentAnalyzer.API.Configuration 4 | { 5 | internal interface ICosmosDbServiceConfiguration 6 | { 7 | string ConnectionString { get; set; } 8 | string DatabaseName { get; set; } 9 | string InvoiceContainerName { get; set; } 10 | string InvoiceContainerPartitionKeyPath { get; set; } 11 | } 12 | 13 | internal class CosmosDbServiceConfiguration : ICosmosDbServiceConfiguration 14 | { 15 | public string ConnectionString { get; set; } 16 | public string DatabaseName { get; set; } 17 | public string InvoiceContainerName { get; set; } 18 | public string InvoiceContainerPartitionKeyPath { get; set; } 19 | } 20 | 21 | internal class CosmosDbDataServiceConfigurationValidation : IValidateOptions 22 | { 23 | public ValidateOptionsResult Validate(string name, CosmosDbServiceConfiguration options) 24 | { 25 | if (string.IsNullOrEmpty(options.ConnectionString)) 26 | { 27 | return ValidateOptionsResult.Fail($"{nameof(options.ConnectionString)} configuration parameter for the Azure Cosmos DB is required"); 28 | } 29 | 30 | if (string.IsNullOrEmpty(options.InvoiceContainerName)) 31 | { 32 | return ValidateOptionsResult.Fail($"{nameof(options.InvoiceContainerName)} configuration parameter for the Azure Cosmos DB is required"); 33 | } 34 | 35 | if (string.IsNullOrEmpty(options.DatabaseName)) 36 | { 37 | return ValidateOptionsResult.Fail($"{nameof(options.DatabaseName)} configuration parameter for the Azure Cosmos DB is required"); 38 | } 39 | 40 | if (string.IsNullOrEmpty(options.InvoiceContainerPartitionKeyPath)) 41 | { 42 | return ValidateOptionsResult.Fail($"{nameof(options.InvoiceContainerPartitionKeyPath)} configuration parameter for the Azure Cosmos DB is required"); 43 | } 44 | 45 | return ValidateOptionsResult.Success; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/Configuration/EventBusConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using SmartAccounting.EventBus.Configuration; 3 | 4 | namespace SmartAccounting.DocumentAnalyzer.API.Configuration 5 | { 6 | internal class EventBusConfiguration : IEventBusConfiguration 7 | { 8 | public string TopicName { get; set; } 9 | public string Subscription { get; set; } 10 | public string ListenAndSendConnectionString { get; set; } 11 | } 12 | 13 | internal class EventBusConfigurationValidation : IValidateOptions 14 | { 15 | public ValidateOptionsResult Validate(string name, EventBusConfiguration options) 16 | { 17 | if (string.IsNullOrEmpty(options.ListenAndSendConnectionString)) 18 | { 19 | return ValidateOptionsResult.Fail($"{nameof(options.ListenAndSendConnectionString)} configuration parameter for the Azure Service Bus is required"); 20 | } 21 | 22 | if (string.IsNullOrEmpty(options.Subscription)) 23 | { 24 | return ValidateOptionsResult.Fail($"{nameof(options.Subscription)} configuration parameter for the Azure Service Bus is required"); 25 | } 26 | 27 | if (string.IsNullOrEmpty(options.TopicName)) 28 | { 29 | return ValidateOptionsResult.Fail($"{nameof(options.TopicName)} configuration parameter for the Azure Service Bus is required"); 30 | } 31 | 32 | return ValidateOptionsResult.Success; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/Configuration/FormRecognizerServiceConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace SmartAccounting.DocumentAnalyzer.API.Configuration 4 | { 5 | internal interface IFormRecognizerServiceConfiguration 6 | { 7 | string EndpointUrl { get; set; } 8 | string Key { get; set; } 9 | } 10 | 11 | internal class FormRecognizerServiceConfiguration : IFormRecognizerServiceConfiguration 12 | { 13 | public string EndpointUrl { get; set; } 14 | public string Key { get; set; } 15 | } 16 | 17 | internal class FormRecognizerServiceConfigurationValidation : IValidateOptions 18 | { 19 | public ValidateOptionsResult Validate(string name, FormRecognizerServiceConfiguration options) 20 | { 21 | if (string.IsNullOrEmpty(options.EndpointUrl)) 22 | { 23 | return ValidateOptionsResult.Fail($"{nameof(options.EndpointUrl)} configuration parameter for the Form Recognizer is required"); 24 | } 25 | 26 | if (string.IsNullOrEmpty(options.Key)) 27 | { 28 | return ValidateOptionsResult.Fail($"{nameof(options.Key)} configuration parameter for the Form Recognizer is required"); 29 | } 30 | 31 | return ValidateOptionsResult.Success; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/Configuration/SqlDbServiceConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace SmartAccounting.DocumentAnalyzer.API.Configuration 4 | { 5 | internal interface ISqlDbServiceConfiguration 6 | { 7 | string ConnectionString { get; set; } 8 | } 9 | 10 | internal class SqlDbServiceConfiguration : ISqlDbServiceConfiguration 11 | { 12 | public string ConnectionString { get; set; } 13 | } 14 | 15 | internal class SqlDbDataServiceConfigurationValidation : IValidateOptions 16 | { 17 | public ValidateOptionsResult Validate(string name, SqlDbServiceConfiguration options) 18 | { 19 | if (string.IsNullOrEmpty(options.ConnectionString)) 20 | { 21 | return ValidateOptionsResult.Fail($"{nameof(options.ConnectionString)} configuration parameter for the Azure SQL is required"); 22 | } 23 | 24 | return ValidateOptionsResult.Success; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/Configuration/StorageServiceConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace SmartAccounting.DocumentAnalyzer.API.Configuration 4 | { 5 | internal interface IStorageServiceConfiguration 6 | { 7 | string Key { get; set; } 8 | string AccountName { get; set; } 9 | string ConnectionString { get; set; } 10 | } 11 | 12 | internal class StorageServiceConfiguration : IStorageServiceConfiguration 13 | { 14 | public string Key { get; set; } 15 | public string AccountName { get; set; } 16 | public string ContainerName { get; set; } 17 | public string ConnectionString { get; set; } 18 | } 19 | 20 | internal class StorageServiceConfigurationValidation : IValidateOptions 21 | { 22 | public ValidateOptionsResult Validate(string name, StorageServiceConfiguration options) 23 | { 24 | if (string.IsNullOrEmpty(options.Key)) 25 | { 26 | return ValidateOptionsResult.Fail($"{nameof(options.Key)} configuration parameter for the Azure Storage Account is required"); 27 | } 28 | 29 | if (string.IsNullOrEmpty(options.AccountName)) 30 | { 31 | return ValidateOptionsResult.Fail($"{nameof(options.AccountName)} configuration parameter for the Azure Storage Account is required"); 32 | } 33 | 34 | if (string.IsNullOrEmpty(options.ConnectionString)) 35 | { 36 | return ValidateOptionsResult.Fail($"{nameof(options.ConnectionString)} configuration parameter for the Azure Storage Account is required"); 37 | } 38 | 39 | return ValidateOptionsResult.Success; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/Core/DependencyInjection/DataServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Azure.Cosmos; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using SmartAccounting.DocumentAnalyzer.API.Application.Repositories; 4 | using SmartAccounting.DocumentAnalyzer.API.Configuration; 5 | using SmartAccounting.DocumentAnalyzer.API.Infrastructure.Repositories; 6 | 7 | namespace SmartAccounting.DocumentAnalyzer.API.Core.DependencyInjection 8 | { 9 | internal static class DataServiceCollectionExtensions 10 | { 11 | public static IServiceCollection AddDataServices(this IServiceCollection services) 12 | { 13 | 14 | services.AddSingleton(implementationFactory => 15 | { 16 | var cosmoDbConfiguration = implementationFactory.GetRequiredService(); 17 | CosmosClient cosmosClient = new CosmosClient(cosmoDbConfiguration.ConnectionString); 18 | CosmosDatabase database = cosmosClient.CreateDatabaseIfNotExistsAsync(cosmoDbConfiguration.DatabaseName) 19 | .GetAwaiter() 20 | .GetResult(); 21 | 22 | database.CreateContainerIfNotExistsAsync( 23 | cosmoDbConfiguration.InvoiceContainerName, 24 | cosmoDbConfiguration.InvoiceContainerPartitionKeyPath, 25 | 400) 26 | .GetAwaiter() 27 | .GetResult(); 28 | 29 | return cosmosClient; 30 | }); 31 | 32 | services.AddTransient(); 33 | 34 | return services; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/Core/DependencyInjection/FileScanningServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Azure; 2 | using Azure.AI.FormRecognizer; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using SmartAccounting.DocumentAnalyzer.API.Configuration; 5 | using SmartAccounting.DocumentAnalyzer.API.Infrastructure.Services; 6 | using System; 7 | 8 | namespace SmartAccounting.DocumentAnalyzer.API.Core.DependencyInjection 9 | { 10 | internal static class FileScanningServiceCollectionExtensions 11 | { 12 | public static IServiceCollection AddFileContentScanningServices(this IServiceCollection services) 13 | { 14 | services.AddSingleton(implementationFactory => 15 | { 16 | var formRecognizerServiceConfiguration = implementationFactory.GetRequiredService(); 17 | var formRecognizerClient = new FormRecognizerClient(new Uri(formRecognizerServiceConfiguration.EndpointUrl), 18 | new AzureKeyCredential(formRecognizerServiceConfiguration.Key)); 19 | return formRecognizerClient; 20 | }); 21 | 22 | services.AddTransient(); 23 | services.AddTransient(); 24 | 25 | return services; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/Core/DependencyInjection/StorageServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Azure.Storage.Blobs; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using SmartAccounting.DocumentAnalyzer.API.Configuration; 4 | using SmartAccounting.DocumentAnalyzer.API.Infrastructure.Services; 5 | 6 | namespace SmartAccounting.DocumentAnalyzer.API.Core.DependencyInjection 7 | { 8 | internal static class StorageServiceCollectionExtensions 9 | { 10 | public static IServiceCollection AddStorageServices(this IServiceCollection services) 11 | { 12 | var serviceProvider = services.BuildServiceProvider(); 13 | var storageConfiguration = serviceProvider.GetRequiredService(); 14 | 15 | services.AddSingleton(implementationFactory => 16 | { 17 | return new BlobServiceClient(storageConfiguration.ConnectionString); 18 | }); 19 | 20 | services.AddTransient(); 21 | 22 | return services; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | 7 | FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build 8 | WORKDIR /src 9 | COPY ["DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/SmartAccounting.DocumentAnalyzer.API.csproj", "DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/"] 10 | RUN dotnet restore "DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/SmartAccounting.DocumentAnalyzer.API.csproj" 11 | COPY . . 12 | WORKDIR "/src/DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API" 13 | RUN dotnet build "SmartAccounting.DocumentAnalyzer.API.csproj" -c Release -o /app/build 14 | 15 | FROM build AS publish 16 | RUN dotnet publish "SmartAccounting.DocumentAnalyzer.API.csproj" -c Release -o /app/publish 17 | 18 | FROM base AS final 19 | WORKDIR /app 20 | COPY --from=publish /app/publish . 21 | ENTRYPOINT ["dotnet", "SmartAccounting.DocumentAnalyzer.API.dll"] -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/Infrastructure/IntegrationEvents/DocumentAnalyzerEventService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using SmartAccounting.EventBus; 3 | using SmartAccounting.EventBus.Interfaces; 4 | using SmartAccounting.EventLog; 5 | using System; 6 | using System.Threading.Tasks; 7 | 8 | namespace SmartAccounting.DocumentAnalyzer.API.Infrastructure.IntegrationEvents 9 | { 10 | internal interface IDocumentAnalyzerEventService 11 | { 12 | Task PublishEventsThroughEventBusAsync(IntegrationEvent @event); 13 | Task AddAndSaveEventAsync(IntegrationEvent @event); 14 | } 15 | 16 | public class DocumentAnalyzerEventService : IDocumentAnalyzerEventService 17 | { 18 | private readonly ILogger _logger; 19 | private readonly IEventBus _eventBus; 20 | private readonly IEventLogService _eventLogService; 21 | 22 | public DocumentAnalyzerEventService(ILogger logger, 23 | IEventBus eventBus, 24 | IEventLogService eventLogService) 25 | { 26 | _logger = logger; 27 | _eventBus = eventBus; 28 | _eventLogService = eventLogService; 29 | } 30 | 31 | public async Task AddAndSaveEventAsync(IntegrationEvent @event) 32 | { 33 | await _eventLogService.SaveEventAsync(@event); 34 | } 35 | 36 | public async Task PublishEventsThroughEventBusAsync(IntegrationEvent @event) 37 | { 38 | try 39 | { 40 | await _eventLogService.MarkEventAsInProgressAsync(@event.Id); 41 | await _eventBus.PublishAsync(@event); 42 | await _eventLogService.MarkEventAsPublishedAsync(@event.Id); 43 | } 44 | catch (Exception ex) 45 | { 46 | _logger.LogError(ex, "Publishing integration event failed: '{IntegrationEventId}'", @event.Id); 47 | 48 | await _eventLogService.MarkEventAsFailedAsync(@event.Id); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace SmartAccounting.DocumentAnalyzer.API 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => 16 | { 17 | webBuilder.UseStartup(); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/SmartAccounting.DocumentAnalyzer.API.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0 5 | Linux 6 | ..\.. 7 | ..\..\docker-compose.dcproj 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using SmartAccounting.DocumentAnalyzer.API.Core.DependencyInjection; 7 | using SmartAccounting.Logging; 8 | 9 | namespace SmartAccounting.DocumentAnalyzer.API 10 | { 11 | public class Startup 12 | { 13 | public Startup(IConfiguration configuration) 14 | { 15 | Configuration = configuration; 16 | } 17 | 18 | public IConfiguration Configuration { get; } 19 | 20 | // This method gets called by the runtime. Use this method to add services to the container. 21 | public void ConfigureServices(IServiceCollection services) 22 | { 23 | services.AddHealthChecks(); 24 | services.AddAppConfiguration(Configuration); 25 | services.AddLoggingServices(); 26 | services.AddStorageServices(); 27 | services.AddDataServices(); 28 | services.AddFileContentScanningServices(); 29 | services.AddIntegrationServices(); 30 | services.AddControllers(); 31 | } 32 | 33 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 34 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 35 | { 36 | if (env.IsDevelopment()) 37 | { 38 | app.UseDeveloperExceptionPage(); 39 | } 40 | 41 | app.UseRouting(); 42 | 43 | app.UseAuthorization(); 44 | 45 | app.UseEndpoints(endpoints => 46 | { 47 | endpoints.MapHealthChecks("/health"); 48 | }); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | 11 | "ApplicationInsightsConfig": { 12 | "InstrumentationKey": "" 13 | }, 14 | 15 | "CosmosDbConfig": { 16 | "ConnectionString": "", 17 | "DatabaseName": "smart-accounting", 18 | "InvoiceContainerName": "invoice", 19 | "InvoiceContainerPartitionKeyPath": "/userId" 20 | }, 21 | 22 | "SqlDbConfig": { 23 | "ConnectionString": "" 24 | }, 25 | 26 | "BlobStorageConfig": { 27 | "ConnectionString": "", 28 | "AccountName": "stsmartaccounting", 29 | "Key": "" 30 | }, 31 | 32 | "ServiceBusConfig": { 33 | "ListenAndSendConnectionString": "", 34 | "TopicName": "smart-accounting-events", 35 | "Subscription": "document-analyzer" 36 | }, 37 | 38 | "FormRecognizerConfig": { 39 | "EndpointUrl": "https://formrec-smart-accounting.cognitiveservices.azure.com/", 40 | "Key": "" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Application/DTO/FileToProcessDto.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using System.Collections.Generic; 3 | 4 | namespace SmartAccounting.FileProcessor.API.Application.DTO 5 | { 6 | public class FileToProcessDto 7 | { 8 | public IList Files { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Application/ErrorHandling/OperationErrorDictionary.cs: -------------------------------------------------------------------------------- 1 | using SmartAccounting.Common.CommonResponse; 2 | 3 | namespace SmartAccounting.FileProcessor.API.Application.ErrorHandling 4 | { 5 | public static class OperationErrorDictionary 6 | { 7 | public static class FileUpload 8 | { 9 | public static OperationError UploadFailed(string fileName) => 10 | new OperationError($"An error occurred when processing file: {fileName}"); 11 | 12 | public static OperationError NoFileFound() => 13 | new OperationError("No file found to upload"); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Application/IntegrationEvents/FileSuccessfullyUploadedIntegrationEvent.cs: -------------------------------------------------------------------------------- 1 | using SmartAccounting.EventBus; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace SmartAccounting.FileProcessor.API.Application.IntegrationEvents 5 | { 6 | internal record FileSuccessfullyUploadedIntegrationEvent : IntegrationEvent 7 | { 8 | [JsonPropertyName("userId")] 9 | public string UserId { get; init; } 10 | [JsonPropertyName("fileUrl")] 11 | public string FileUrl { get; init; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Application/Model/UserInvoice.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace SmartAccounting.FileProcessor.API.Application.Model 5 | { 6 | public class UserInvoice 7 | { 8 | [JsonPropertyName("id")] 9 | public string Id { get; set; } 10 | public string UserId { get; set; } 11 | public DateTimeOffset CreationDate { get; set; } 12 | public string FileUrl { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/BackgroundServices/Channels/FileProcessingChannel.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | using System.Threading.Channels; 6 | using System.Threading.Tasks; 7 | 8 | namespace SmartAccounting.FileProcessor.API.BackgroundServices.Channels 9 | { 10 | public record FileProcessingChannelElement 11 | { 12 | public string FilePath { get; init; } 13 | public string UserId { get; init; } 14 | } 15 | 16 | public class FileProcessingChannel 17 | { 18 | private const int MaxMessagesInChannel = 100; 19 | 20 | private readonly Channel _channel; 21 | private readonly ILogger _logger; 22 | 23 | public FileProcessingChannel(ILogger logger) 24 | { 25 | var options = new BoundedChannelOptions(MaxMessagesInChannel) 26 | { 27 | SingleWriter = false, 28 | SingleReader = true 29 | }; 30 | 31 | _channel = Channel.CreateBounded(options); 32 | 33 | _logger = logger; 34 | } 35 | 36 | public async Task AddFileAsync(FileProcessingChannelElement fileProcessingChannelElement, CancellationToken ct = default) 37 | { 38 | while (await _channel.Writer.WaitToWriteAsync(ct) && !ct.IsCancellationRequested) 39 | { 40 | if (_channel.Writer.TryWrite(fileProcessingChannelElement)) 41 | { 42 | Log.ChannelMessageWritten(_logger, fileProcessingChannelElement.FilePath); 43 | 44 | return true; 45 | } 46 | } 47 | 48 | return false; 49 | } 50 | 51 | public IAsyncEnumerable ReadAllAsync(CancellationToken ct = default) => 52 | _channel.Reader.ReadAllAsync(ct); 53 | 54 | public bool TryCompleteWriter(Exception ex = null) => _channel.Writer.TryComplete(ex); 55 | 56 | internal static class EventIds 57 | { 58 | public static readonly EventId ChannelMessageWritten = new EventId(100, "ChannelMessageWritten"); 59 | } 60 | 61 | private static class Log 62 | { 63 | private static readonly Action _channelMessageWritten = LoggerMessage.Define( 64 | LogLevel.Debug, 65 | EventIds.ChannelMessageWritten, 66 | "Filename {FileName} was written to the channel."); 67 | 68 | public static void ChannelMessageWritten(ILogger logger, string fileName) 69 | { 70 | _channelMessageWritten(logger, fileName, null); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Configuration/AzureAdB2cServiceConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace SmartAccounting.FileProcessor.API.Configuration 4 | { 5 | internal interface IAzureAdB2cServiceConfiguration 6 | { 7 | string Instance { get; set; } 8 | string ClientId { get; set; } 9 | string Domain { get; set; } 10 | string SignUpSignInPolicyId { get; set; } 11 | } 12 | 13 | internal class AzureAdB2cServiceConfiguration : IAzureAdB2cServiceConfiguration 14 | { 15 | public string Instance { get; set; } 16 | public string ClientId { get; set; } 17 | public string Domain { get; set; } 18 | public string SignUpSignInPolicyId { get; set; } 19 | } 20 | 21 | internal class AzureAdB2cServiceConfigurationValidation : IValidateOptions 22 | { 23 | public ValidateOptionsResult Validate(string name, AzureAdB2cServiceConfiguration options) 24 | { 25 | if (string.IsNullOrEmpty(options.Instance)) 26 | { 27 | return ValidateOptionsResult.Fail($"{nameof(options.Instance)} configuration parameter for the Azure AD B2C is required"); 28 | } 29 | 30 | if (string.IsNullOrEmpty(options.ClientId)) 31 | { 32 | return ValidateOptionsResult.Fail($"{nameof(options.ClientId)} configuration parameter for the Azure AD B2C is required"); 33 | } 34 | 35 | if (string.IsNullOrEmpty(options.Domain)) 36 | { 37 | return ValidateOptionsResult.Fail($"{nameof(options.Domain)} configuration parameter for the Azure AD B2C is required"); 38 | } 39 | 40 | if (string.IsNullOrEmpty(options.SignUpSignInPolicyId)) 41 | { 42 | return ValidateOptionsResult.Fail($"{nameof(options.SignUpSignInPolicyId)} configuration parameter for the Azure AD B2C is required"); 43 | } 44 | 45 | return ValidateOptionsResult.Success; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Configuration/CosmosDbServiceConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace SmartAccounting.FileProcessor.API.Configuration 4 | { 5 | internal interface ICosmosDbServiceConfiguration 6 | { 7 | string ConnectionString { get; set; } 8 | string DatabaseName { get; set; } 9 | string InvoiceContainerName { get; set; } 10 | string InvoiceContainerPartitionKeyPath { get; set; } 11 | } 12 | 13 | internal class CosmosDbServiceConfiguration : ICosmosDbServiceConfiguration 14 | { 15 | public string ConnectionString { get; set; } 16 | public string DatabaseName { get; set; } 17 | public string InvoiceContainerName { get; set; } 18 | public string InvoiceContainerPartitionKeyPath { get; set; } 19 | } 20 | 21 | internal class CosmosDbDataServiceConfigurationValidation : IValidateOptions 22 | { 23 | public ValidateOptionsResult Validate(string name, CosmosDbServiceConfiguration options) 24 | { 25 | if (string.IsNullOrEmpty(options.ConnectionString)) 26 | { 27 | return ValidateOptionsResult.Fail($"{nameof(options.ConnectionString)} configuration parameter for the Azure Cosmos DB is required"); 28 | } 29 | 30 | if (string.IsNullOrEmpty(options.InvoiceContainerName)) 31 | { 32 | return ValidateOptionsResult.Fail($"{nameof(options.InvoiceContainerName)} configuration parameter for the Azure Cosmos DB is required"); 33 | } 34 | 35 | if (string.IsNullOrEmpty(options.DatabaseName)) 36 | { 37 | return ValidateOptionsResult.Fail($"{nameof(options.DatabaseName)} configuration parameter for the Azure Cosmos DB is required"); 38 | } 39 | 40 | if (string.IsNullOrEmpty(options.InvoiceContainerPartitionKeyPath)) 41 | { 42 | return ValidateOptionsResult.Fail($"{nameof(options.InvoiceContainerPartitionKeyPath)} configuration parameter for the Azure Cosmos DB is required"); 43 | } 44 | 45 | return ValidateOptionsResult.Success; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Configuration/EventBusConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using SmartAccounting.EventBus.Configuration; 3 | 4 | namespace SmartAccounting.FileProcessor.API.Configuration 5 | { 6 | internal class EventBusConfiguration : IEventBusConfiguration 7 | { 8 | public string TopicName { get; set; } 9 | public string Subscription { get; set; } 10 | public string ListenAndSendConnectionString { get; set; } 11 | } 12 | 13 | internal class EventBusConfigurationValidation : IValidateOptions 14 | { 15 | public ValidateOptionsResult Validate(string name, EventBusConfiguration options) 16 | { 17 | if (string.IsNullOrEmpty(options.ListenAndSendConnectionString)) 18 | { 19 | return ValidateOptionsResult.Fail($"{nameof(options.ListenAndSendConnectionString)} configuration parameter for the Azure Service Bus is required"); 20 | } 21 | 22 | if (string.IsNullOrEmpty(options.Subscription)) 23 | { 24 | return ValidateOptionsResult.Fail($"{nameof(options.Subscription)} configuration parameter for the Azure Service Bus is required"); 25 | } 26 | 27 | if (string.IsNullOrEmpty(options.TopicName)) 28 | { 29 | return ValidateOptionsResult.Fail($"{nameof(options.TopicName)} configuration parameter for the Azure Service Bus is required"); 30 | } 31 | 32 | return ValidateOptionsResult.Success; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Configuration/SqlDbServiceConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace SmartAccounting.FileProcessor.API.Configuration 4 | { 5 | internal interface ISqlDbServiceConfiguration 6 | { 7 | string ConnectionString { get; set; } 8 | } 9 | 10 | internal class SqlDbServiceConfiguration : ISqlDbServiceConfiguration 11 | { 12 | public string ConnectionString { get; set; } 13 | } 14 | 15 | internal class SqlDbDataServiceConfigurationValidation : IValidateOptions 16 | { 17 | public ValidateOptionsResult Validate(string name, SqlDbServiceConfiguration options) 18 | { 19 | if (string.IsNullOrEmpty(options.ConnectionString)) 20 | { 21 | return ValidateOptionsResult.Fail($"{nameof(options.ConnectionString)} configuration parameter for the Azure SQL is required"); 22 | } 23 | 24 | return ValidateOptionsResult.Success; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Configuration/StorageServiceConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace SmartAccounting.FileProcessor.API.Configuration 4 | { 5 | internal interface IStorageServiceConfiguration 6 | { 7 | string Key { get; set; } 8 | string AccountName { get; set; } 9 | string ConnectionString { get; set; } 10 | } 11 | 12 | internal class StorageServiceConfiguration : IStorageServiceConfiguration 13 | { 14 | public string Key { get; set; } 15 | public string AccountName { get; set; } 16 | public string ConnectionString { get; set; } 17 | } 18 | 19 | internal class StorageServiceConfigurationValidation : IValidateOptions 20 | { 21 | public ValidateOptionsResult Validate(string name, StorageServiceConfiguration options) 22 | { 23 | if (string.IsNullOrEmpty(options.Key)) 24 | { 25 | return ValidateOptionsResult.Fail($"{nameof(options.Key)} configuration parameter for the Azure Storage Account is required"); 26 | } 27 | 28 | if (string.IsNullOrEmpty(options.AccountName)) 29 | { 30 | return ValidateOptionsResult.Fail($"{nameof(options.AccountName)} configuration parameter for the Azure Storage Account is required"); 31 | } 32 | 33 | if (string.IsNullOrEmpty(options.ConnectionString)) 34 | { 35 | return ValidateOptionsResult.Fail($"{nameof(options.ConnectionString)} configuration parameter for the Azure Storage Account is required"); 36 | } 37 | 38 | return ValidateOptionsResult.Success; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Configuration/SwaggerConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace SmartAccounting.FileProcessor.API.Configuration 4 | { 5 | internal interface ISwaggerConfiguration 6 | { 7 | string Root { get; set; } 8 | string ServiceName { get; set; } 9 | string ServiceVersion { get; set; } 10 | } 11 | 12 | internal class SwaggerConfiguration : ISwaggerConfiguration 13 | { 14 | public string Root { get; set; } 15 | public string ServiceName { get; set; } 16 | public string ServiceVersion { get; set; } 17 | } 18 | 19 | internal class SwaggerConfigurationValidation : IValidateOptions 20 | { 21 | public ValidateOptionsResult Validate(string name, SwaggerConfiguration options) 22 | { 23 | if (string.IsNullOrEmpty(options.Root)) 24 | { 25 | return ValidateOptionsResult.Fail($"{nameof(options.Root)} configuration parameter for the Swagger is required"); 26 | } 27 | 28 | if (string.IsNullOrEmpty(options.ServiceName)) 29 | { 30 | return ValidateOptionsResult.Fail($"{nameof(options.ServiceName)} configuration parameter for the Swagger is required"); 31 | } 32 | 33 | if (string.IsNullOrEmpty(options.ServiceVersion)) 34 | { 35 | return ValidateOptionsResult.Fail($"{nameof(options.ServiceVersion)} configuration parameter for the Swagger is required"); 36 | } 37 | 38 | return ValidateOptionsResult.Success; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Controllers/FileController.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | using SmartAccounting.FileProcessor.API.Application.Commands; 5 | using SmartAccounting.FileProcessor.API.Application.DTO; 6 | using System; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace SmartAccounting.FileProcessor.API.Controllers 11 | { 12 | [Authorize(Policy = "AccessAsUser")] 13 | [ApiController] 14 | [Route("[controller]")] 15 | public class FileController : ControllerBase 16 | { 17 | private readonly IMediator _mediator; 18 | 19 | public FileController(IMediator mediator) 20 | { 21 | _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); 22 | } 23 | 24 | [HttpPost] 25 | [Route("upload")] 26 | public async Task Post([FromForm] FileToProcessDto fileToProcessDto, CancellationToken cancellationToken) 27 | { 28 | var uploadFileCommand = new UploadFileCommand 29 | { 30 | Files = fileToProcessDto.Files 31 | }; 32 | 33 | 34 | var response = await _mediator.Send(uploadFileCommand); 35 | 36 | if (response.CompletedWithSuccess) 37 | { 38 | return Ok(); 39 | } 40 | 41 | else 42 | { 43 | return BadRequest(response.OperationError); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Core/DependencyInjection/AuthenticationServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Identity.Web; 5 | using SmartAccounting.FileProcessor.API.Infrastructure.AuthorizationPolicies; 6 | using SmartAccounting.FileProcessor.API.Infrastructure.Identity; 7 | 8 | namespace SmartAccounting.FileProcessor.API.Core.DependencyInjection 9 | { 10 | internal static class AuthenticationServiceCollectionExtensions 11 | { 12 | public static IServiceCollection AddAuthenticationWithAuthorizationSupport(this IServiceCollection services, 13 | IConfiguration config) 14 | { 15 | services.AddMicrosoftIdentityWebApiAuthentication(config, "AzureAdB2CConfig"); 16 | 17 | services.AddAuthorization(options => 18 | { 19 | options.AddPolicy("AccessAsUser", 20 | policy => policy.Requirements.Add(new ScopesRequirement("User.Access"))); 21 | }); 22 | 23 | services.AddSingleton(); 24 | 25 | services.AddHttpContextAccessor(); 26 | services.AddTransient(); 27 | 28 | return services; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Core/DependencyInjection/StorageServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Azure.Storage.Blobs; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using SmartAccounting.FileProcessor.API.Configuration; 4 | using SmartAccounting.FileProcessor.API.Infrastructure.Services; 5 | 6 | namespace SmartAccounting.FileProcessor.API.Core.DependencyInjection 7 | { 8 | internal static class StorageServiceCollectionExtensions 9 | { 10 | public static IServiceCollection AddStorageServices(this IServiceCollection services) 11 | { 12 | var serviceProvider = services.BuildServiceProvider(); 13 | var storageConfiguration = serviceProvider.GetRequiredService(); 14 | 15 | services.AddSingleton(implementationFactory => 16 | { 17 | return new BlobServiceClient(storageConfiguration.ConnectionString); 18 | }); 19 | 20 | services.AddTransient(); 21 | 22 | return services; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | 7 | FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build 8 | WORKDIR /src 9 | COPY ["FileProcessor/SmartAccounting.FileProcessor.API/SmartAccounting.FileProcessor.API.csproj", "FileProcessor/SmartAccounting.FileProcessor.API/"] 10 | RUN dotnet restore "FileProcessor/SmartAccounting.FileProcessor.API/SmartAccounting.FileProcessor.API.csproj" 11 | COPY . . 12 | WORKDIR "/src/FileProcessor/SmartAccounting.FileProcessor.API" 13 | RUN dotnet build "SmartAccounting.FileProcessor.API.csproj" -c Release -o /app/build 14 | 15 | FROM build AS publish 16 | RUN dotnet publish "SmartAccounting.FileProcessor.API.csproj" -c Release -o /app/publish 17 | 18 | FROM base AS final 19 | WORKDIR /app 20 | COPY --from=publish /app/publish . 21 | ENTRYPOINT ["dotnet", "SmartAccounting.FileProcessor.API.dll"] -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Infrastructure/AuthorizationPolicies/ScopesHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.Identity.Web; 3 | using System; 4 | using System.Linq; 5 | using System.Security.Claims; 6 | using System.Threading.Tasks; 7 | 8 | namespace SmartAccounting.FileProcessor.API.Infrastructure.AuthorizationPolicies 9 | { 10 | internal class ScopesHandler : AuthorizationHandler 11 | { 12 | protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, 13 | ScopesRequirement requirement) 14 | { 15 | // If there are no scopes, do not process 16 | if (!context.User.Claims.Any(x => x.Type == ClaimConstants.Scope) 17 | && !context.User.Claims.Any(y => y.Type == ClaimConstants.Scp)) 18 | { 19 | return Task.CompletedTask; 20 | } 21 | 22 | Claim scopeClaim = context?.User?.FindFirst(ClaimConstants.Scp); 23 | 24 | if (scopeClaim == null) 25 | scopeClaim = context?.User?.FindFirst(ClaimConstants.Scope); 26 | 27 | if (scopeClaim != null && scopeClaim.Value.Equals(requirement.ScopeName, StringComparison.InvariantCultureIgnoreCase)) 28 | { 29 | context.Succeed(requirement); 30 | } 31 | 32 | return Task.CompletedTask; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Infrastructure/AuthorizationPolicies/ScopesRequirement.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace SmartAccounting.FileProcessor.API.Infrastructure.AuthorizationPolicies 4 | { 5 | public class ScopesRequirement : IAuthorizationRequirement 6 | { 7 | public readonly string ScopeName; 8 | 9 | public ScopesRequirement(string scopeName) 10 | { 11 | ScopeName = scopeName; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Infrastructure/Identity/IdentityService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using System; 3 | using System.Security.Claims; 4 | 5 | namespace SmartAccounting.FileProcessor.API.Infrastructure.Identity 6 | { 7 | public interface IIdentityService 8 | { 9 | Guid GetUserIdentity(); 10 | string GetUserEmail(); 11 | } 12 | 13 | public class IdentityService : IIdentityService 14 | { 15 | private readonly IHttpContextAccessor _context; 16 | 17 | public IdentityService(IHttpContextAccessor context) 18 | { 19 | _context = context ?? throw new ArgumentNullException(nameof(context)); 20 | } 21 | 22 | public Guid GetUserIdentity() 23 | { 24 | var userId = _context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value; 25 | return new Guid(userId); 26 | } 27 | 28 | public string GetUserEmail() 29 | { 30 | var userEmail = _context.HttpContext.User.FindFirst("emails").Value; 31 | return userEmail; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Infrastructure/IntegrationEvents/FileProcessorIntegrationEventService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using SmartAccounting.EventBus; 3 | using SmartAccounting.EventBus.Interfaces; 4 | using SmartAccounting.EventLog; 5 | using System; 6 | using System.Threading.Tasks; 7 | 8 | namespace SmartAccounting.FileProcessor.API.Infrastructure.IntegrationEvents 9 | { 10 | internal interface IFileProcessorIntegrationEventService 11 | { 12 | Task PublishEventsThroughEventBusAsync(IntegrationEvent @event); 13 | Task AddAndSaveEventAsync(IntegrationEvent @event); 14 | } 15 | 16 | internal class FileProcessorIntegrationEventService : IFileProcessorIntegrationEventService 17 | { 18 | private readonly ILogger _logger; 19 | private readonly IEventBus _eventBus; 20 | private readonly IEventLogService _eventLogService; 21 | 22 | public FileProcessorIntegrationEventService(ILogger logger, 23 | IEventBus eventBus, 24 | IEventLogService eventLogService) 25 | { 26 | _logger = logger; 27 | _eventBus = eventBus; 28 | _eventLogService = eventLogService; 29 | } 30 | 31 | public async Task AddAndSaveEventAsync(IntegrationEvent @event) 32 | { 33 | await _eventLogService.SaveEventAsync(@event); 34 | } 35 | 36 | public async Task PublishEventsThroughEventBusAsync(IntegrationEvent @event) 37 | { 38 | try 39 | { 40 | await _eventLogService.MarkEventAsInProgressAsync(@event.Id); 41 | await _eventBus.PublishAsync(@event); 42 | await _eventLogService.MarkEventAsPublishedAsync(@event.Id); 43 | } 44 | catch (Exception ex) 45 | { 46 | _logger.LogError(ex, "Publishing integration event failed: '{IntegrationEventId}'", @event.Id); 47 | 48 | await _eventLogService.MarkEventAsFailedAsync(@event.Id); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Migrations/20210618095506_initial-migration.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | using SmartAccounting.EventLog; 9 | 10 | namespace SmartAccounting.FileProcessor.API.Migrations 11 | { 12 | [DbContext(typeof(EventLogContext))] 13 | [Migration("20210618095506_initial-migration")] 14 | partial class initialmigration 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 21 | .HasAnnotation("ProductVersion", "5.0.7") 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("SmartAccounting.EventLog.IntegrationEventLogEntry", b => 25 | { 26 | b.Property("EventId") 27 | .ValueGeneratedOnAdd() 28 | .HasColumnType("uniqueidentifier"); 29 | 30 | b.Property("Content") 31 | .IsRequired() 32 | .HasColumnType("nvarchar(max)"); 33 | 34 | b.Property("CreationTime") 35 | .HasColumnType("datetime2"); 36 | 37 | b.Property("EventTypeName") 38 | .IsRequired() 39 | .HasColumnType("nvarchar(max)"); 40 | 41 | b.Property("State") 42 | .HasColumnType("int"); 43 | 44 | b.Property("TimesSent") 45 | .HasColumnType("int"); 46 | 47 | b.Property("TransactionId") 48 | .HasColumnType("nvarchar(max)"); 49 | 50 | b.HasKey("EventId"); 51 | 52 | b.ToTable("IntegrationEventLog"); 53 | }); 54 | #pragma warning restore 612, 618 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Migrations/20210618095506_initial-migration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | using System; 3 | 4 | namespace SmartAccounting.FileProcessor.API.Migrations 5 | { 6 | public partial class initialmigration : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "IntegrationEventLog", 12 | columns: table => new 13 | { 14 | EventId = table.Column(type: "uniqueidentifier", nullable: false), 15 | EventTypeName = table.Column(type: "nvarchar(max)", nullable: false), 16 | State = table.Column(type: "int", nullable: false), 17 | TimesSent = table.Column(type: "int", nullable: false), 18 | CreationTime = table.Column(type: "datetime2", nullable: false), 19 | Content = table.Column(type: "nvarchar(max)", nullable: false), 20 | TransactionId = table.Column(type: "nvarchar(max)", nullable: true) 21 | }, 22 | constraints: table => 23 | { 24 | table.PrimaryKey("PK_IntegrationEventLog", x => x.EventId); 25 | }); 26 | } 27 | 28 | protected override void Down(MigrationBuilder migrationBuilder) 29 | { 30 | migrationBuilder.DropTable( 31 | name: "IntegrationEventLog"); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Migrations/EventLogContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using SmartAccounting.EventLog; 8 | 9 | namespace SmartAccounting.FileProcessor.API.Migrations 10 | { 11 | [DbContext(typeof(EventLogContext))] 12 | partial class EventLogContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 19 | .HasAnnotation("ProductVersion", "5.0.7") 20 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 21 | 22 | modelBuilder.Entity("SmartAccounting.EventLog.IntegrationEventLogEntry", b => 23 | { 24 | b.Property("EventId") 25 | .ValueGeneratedOnAdd() 26 | .HasColumnType("uniqueidentifier"); 27 | 28 | b.Property("Content") 29 | .IsRequired() 30 | .HasColumnType("nvarchar(max)"); 31 | 32 | b.Property("CreationTime") 33 | .HasColumnType("datetime2"); 34 | 35 | b.Property("EventTypeName") 36 | .IsRequired() 37 | .HasColumnType("nvarchar(max)"); 38 | 39 | b.Property("State") 40 | .HasColumnType("int"); 41 | 42 | b.Property("TimesSent") 43 | .HasColumnType("int"); 44 | 45 | b.Property("TransactionId") 46 | .HasColumnType("nvarchar(max)"); 47 | 48 | b.HasKey("EventId"); 49 | 50 | b.ToTable("IntegrationEventLog"); 51 | }); 52 | #pragma warning restore 612, 618 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace SmartAccounting.FileProcessor.API 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => 16 | { 17 | webBuilder.UseStartup(); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/SmartAccounting.FileProcessor.API.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0 5 | Linux 6 | ..\.. 7 | ..\..\docker-compose.dcproj 8 | 8d74c77c-d906-401d-9dc2-bf651c45b2f6 9 | 10 | 11 | 12 | true 13 | $(NoWarn);1591 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | 24 | 25 | 26 | all 27 | runtime; build; native; contentfiles; analyzers; buildtransitive 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/Startup.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using SmartAccounting.Common.ExceptionMiddleware; 8 | using SmartAccounting.FileProcessor.API.BackgroundServices; 9 | using SmartAccounting.FileProcessor.API.BackgroundServices.Channels; 10 | using SmartAccounting.FileProcessor.API.Core.DependencyInjection; 11 | using SmartAccounting.Logging; 12 | 13 | namespace SmartAccounting.FileProcessor.API 14 | { 15 | public class Startup 16 | { 17 | public Startup(IConfiguration configuration) 18 | { 19 | Configuration = configuration; 20 | } 21 | 22 | public IConfiguration Configuration { get; } 23 | 24 | // This method gets called by the runtime. Use this method to add services to the container. 25 | public void ConfigureServices(IServiceCollection services) 26 | { 27 | services.AddAppConfiguration(Configuration); 28 | services.AddLoggingServices(); 29 | services.AddMediatR(typeof(Startup)); 30 | services.AddAuthenticationWithAuthorizationSupport(Configuration); 31 | services.AddIntegrationServices(); 32 | services.AddStorageServices(); 33 | 34 | services.AddSingleton(); 35 | services.AddHostedService(); 36 | 37 | services.AddControllers(); 38 | services.AddSwagger(); 39 | } 40 | 41 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 42 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 43 | { 44 | app.UseMiddleware(); 45 | 46 | if (env.IsDevelopment()) 47 | { 48 | app.UseDeveloperExceptionPage(); 49 | } 50 | 51 | app.UseSwaggerServices(); 52 | 53 | app.UseRouting(); 54 | 55 | app.UseAuthentication(); 56 | app.UseAuthorization(); 57 | 58 | app.UseEndpoints(endpoints => 59 | { 60 | endpoints.MapControllers(); 61 | }); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/FileProcessor/SmartAccounting.FileProcessor.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | 10 | "AllowedHosts": "*", 11 | 12 | "SwaggerConfig": { 13 | "Root": "/file-processor-api", 14 | "ServiceName": "File Processor API", 15 | "ServiceVersion": "v1" 16 | }, 17 | 18 | "AzureAdB2CConfig": { 19 | "Instance": "https://xxx.b2clogin.com", 20 | "ClientId": "", 21 | "Domain": "xxx.onmicrosoft.com", 22 | "SignUpSignInPolicyId": "B2C_1A_SIGNINSIGNUP" 23 | }, 24 | 25 | "ApplicationInsightsConfig": { 26 | "InstrumentationKey": "" 27 | }, 28 | 29 | "CosmosDbConfig": { 30 | "ConnectionString": "", 31 | "DatabaseName": "smart-accounting", 32 | "InvoiceContainerName": "invoice", 33 | "InvoiceContainerPartitionKeyPath": "/userId" 34 | }, 35 | 36 | "SqlDbConfig": { 37 | "ConnectionString": "" 38 | }, 39 | 40 | "BlobStorageConfig": { 41 | "ConnectionString": "", 42 | "AccountName": "stsmartaccounting", 43 | "Key": "" 44 | }, 45 | 46 | "ServiceBusConfig": { 47 | "ListenAndSendConnectionString": "", 48 | "TopicName": "smart-accounting-events", 49 | "Subscription": "file-processor" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/Notification/SmartAccounting.Notification.API/Application/IntegrationEvents/DocumentSuccessfullyAnalyzedIntegrationEvent.cs: -------------------------------------------------------------------------------- 1 | using SmartAccounting.EventBus; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace SmartAccounting.Notification.API.Application.IntegrationEvents 5 | { 6 | internal record DocumentSuccessfullyAnalyzedIntegrationEvent : IntegrationEvent 7 | { 8 | [JsonPropertyName("userId")] 9 | public string UserId { get; init; } 10 | [JsonPropertyName("invoiceId")] 11 | public string InvoiceId { get; init; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/Notification/SmartAccounting.Notification.API/Application/IntegrationEvents/EventHandlers/DocumentSuccessfullyAnalyzedEventHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using SmartAccounting.EventBus.Interfaces; 3 | using SmartAccounting.Notification.API.Application.Model; 4 | using SmartAccounting.Notification.API.Infrastructure.Services; 5 | using System; 6 | using System.Threading.Tasks; 7 | 8 | namespace SmartAccounting.Notification.API.Application.IntegrationEvents.EventHandlers 9 | { 10 | internal class DocumentSuccessfullyAnalyzedEventHandler : IIntegrationEventHandler 11 | { 12 | private readonly ILogger _logger; 13 | private readonly IRealTimeNotificationService _realTimeNotificationService; 14 | 15 | public DocumentSuccessfullyAnalyzedEventHandler(ILogger logger, 16 | IRealTimeNotificationService realTimeNotificationService) 17 | { 18 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 19 | _realTimeNotificationService = realTimeNotificationService 20 | ?? throw new ArgumentNullException(nameof(realTimeNotificationService)); 21 | } 22 | 23 | public async Task HandleAsync(DocumentSuccessfullyAnalyzedIntegrationEvent @event) 24 | { 25 | if (!string.IsNullOrEmpty(@event.UserId) 26 | && !string.IsNullOrEmpty(@event.InvoiceId)) 27 | { 28 | var documentProcessedNotification = new DocumentProcessedNotification 29 | { 30 | InvoiceId = @event.InvoiceId, 31 | UserId = @event.UserId 32 | }; 33 | 34 | await _realTimeNotificationService.SendNotificationAsync(documentProcessedNotification); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/Notification/SmartAccounting.Notification.API/Application/Model/DocumentProcessedNotification.cs: -------------------------------------------------------------------------------- 1 | namespace SmartAccounting.Notification.API.Application.Model 2 | { 3 | internal class DocumentProcessedNotification 4 | { 5 | public string UserId { get; set; } 6 | public string InvoiceId { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/Notification/SmartAccounting.Notification.API/CommunicationHubs/DocumentNotificationHub.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.SignalR; 3 | using SmartAccounting.Notification.API.Application.Model; 4 | using System.Threading.Tasks; 5 | 6 | namespace SmartAccounting.Notification.API.CommunicationHubs 7 | { 8 | [Authorize(Policy = "AccessAsUser")] 9 | internal class DocumentNotificationHub : Hub 10 | { 11 | [HubMethodName(HubMethodName)] 12 | public async Task SendDirectMessageToUserAsync(DocumentProcessedNotification documentProcessedNotification) 13 | { 14 | await Clients.User(documentProcessedNotification.UserId).SendAsync(HubMethodName, documentProcessedNotification); 15 | } 16 | 17 | 18 | public const string HubMethodName = "direct-notification"; 19 | public const string HubName = "DocumentNotificationHub"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/Notification/SmartAccounting.Notification.API/Configuration/AzureAdB2cServiceConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace SmartAccounting.Notification.API.Configuration 4 | { 5 | internal interface IAzureAdB2cServiceConfiguration 6 | { 7 | string Instance { get; set; } 8 | string ClientId { get; set; } 9 | string Domain { get; set; } 10 | string SignUpSignInPolicyId { get; set; } 11 | } 12 | 13 | internal class AzureAdB2cServiceConfiguration : IAzureAdB2cServiceConfiguration 14 | { 15 | public string Instance { get; set; } 16 | public string ClientId { get; set; } 17 | public string Domain { get; set; } 18 | public string SignUpSignInPolicyId { get; set; } 19 | } 20 | 21 | internal class AzureAdB2cServiceConfigurationValidation : IValidateOptions 22 | { 23 | public ValidateOptionsResult Validate(string name, AzureAdB2cServiceConfiguration options) 24 | { 25 | if (string.IsNullOrEmpty(options.Instance)) 26 | { 27 | return ValidateOptionsResult.Fail($"{nameof(options.Instance)} configuration parameter for the Azure AD B2C is required"); 28 | } 29 | 30 | if (string.IsNullOrEmpty(options.ClientId)) 31 | { 32 | return ValidateOptionsResult.Fail($"{nameof(options.ClientId)} configuration parameter for the Azure AD B2C is required"); 33 | } 34 | 35 | if (string.IsNullOrEmpty(options.Domain)) 36 | { 37 | return ValidateOptionsResult.Fail($"{nameof(options.Domain)} configuration parameter for the Azure AD B2C is required"); 38 | } 39 | 40 | if (string.IsNullOrEmpty(options.SignUpSignInPolicyId)) 41 | { 42 | return ValidateOptionsResult.Fail($"{nameof(options.SignUpSignInPolicyId)} configuration parameter for the Azure AD B2C is required"); 43 | } 44 | 45 | return ValidateOptionsResult.Success; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/Notification/SmartAccounting.Notification.API/Configuration/EventBusConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using SmartAccounting.EventBus.Configuration; 3 | 4 | namespace SmartAccounting.Notification.API.Configuration 5 | { 6 | internal class EventBusConfiguration : IEventBusConfiguration 7 | { 8 | public string TopicName { get; set; } 9 | public string Subscription { get; set; } 10 | public string ListenAndSendConnectionString { get; set; } 11 | } 12 | 13 | internal class EventBusConfigurationValidation : IValidateOptions 14 | { 15 | public ValidateOptionsResult Validate(string name, EventBusConfiguration options) 16 | { 17 | if (string.IsNullOrEmpty(options.ListenAndSendConnectionString)) 18 | { 19 | return ValidateOptionsResult.Fail($"{nameof(options.ListenAndSendConnectionString)} configuration parameter for the Azure Service Bus is required"); 20 | } 21 | 22 | if (string.IsNullOrEmpty(options.Subscription)) 23 | { 24 | return ValidateOptionsResult.Fail($"{nameof(options.Subscription)} configuration parameter for the Azure Service Bus is required"); 25 | } 26 | 27 | if (string.IsNullOrEmpty(options.TopicName)) 28 | { 29 | return ValidateOptionsResult.Fail($"{nameof(options.TopicName)} configuration parameter for the Azure Service Bus is required"); 30 | } 31 | 32 | return ValidateOptionsResult.Success; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/Notification/SmartAccounting.Notification.API/Configuration/SignalRServiceConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace SmartAccounting.Notification.API.Configuration 4 | { 5 | internal interface ISignalRServiceConfiguration 6 | { 7 | string ConnectionString { get; set; } 8 | } 9 | 10 | internal class SignalRServiceConfiguration : ISignalRServiceConfiguration 11 | { 12 | public string ConnectionString { get; set; } 13 | } 14 | 15 | internal class SignalRServiceConfigurationValidation : IValidateOptions 16 | { 17 | public ValidateOptionsResult Validate(string name, SignalRServiceConfiguration options) 18 | { 19 | if (string.IsNullOrEmpty(options.ConnectionString)) 20 | { 21 | return ValidateOptionsResult.Fail($"{nameof(options.ConnectionString)} configuration parameter for the Azure SignalR Service is required"); 22 | } 23 | 24 | return ValidateOptionsResult.Success; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/Notification/SmartAccounting.Notification.API/Configuration/SqlDbServiceConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace SmartAccounting.Notification.API.Configuration 4 | { 5 | internal interface ISqlDbServiceConfiguration 6 | { 7 | string ConnectionString { get; set; } 8 | } 9 | 10 | internal class SqlDbServiceConfiguration : ISqlDbServiceConfiguration 11 | { 12 | public string ConnectionString { get; set; } 13 | } 14 | 15 | internal class SqlDbDataServiceConfigurationValidation : IValidateOptions 16 | { 17 | public ValidateOptionsResult Validate(string name, SqlDbServiceConfiguration options) 18 | { 19 | if (string.IsNullOrEmpty(options.ConnectionString)) 20 | { 21 | return ValidateOptionsResult.Fail($"{nameof(options.ConnectionString)} configuration parameter for the Azure SQL is required"); 22 | } 23 | 24 | return ValidateOptionsResult.Success; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/Notification/SmartAccounting.Notification.API/Core/DependencyInjection/AuthenticationServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication.JwtBearer; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Primitives; 6 | using Microsoft.Identity.Web; 7 | using SmartAccounting.Notification.API.Infrastructure.AuthorizationPolicies; 8 | using System.Net.Http.Headers; 9 | using System.Threading.Tasks; 10 | 11 | namespace SmartAccounting.Notification.API.Core.DependencyInjection 12 | { 13 | internal static class AuthenticationServiceCollectionExtensions 14 | { 15 | public static IServiceCollection AddAuthenticationWithAuthorizationSupport(this IServiceCollection services, 16 | IConfiguration config) 17 | { 18 | services.AddMicrosoftIdentityWebApiAuthentication(config, "AzureAdB2CConfig"); 19 | 20 | services.Configure(JwtBearerDefaults.AuthenticationScheme, options => 21 | { 22 | options.Events = new JwtBearerEvents() 23 | { 24 | OnMessageReceived = context => 25 | { 26 | StringValues authorizationHeaderValue; 27 | context.Request.Headers.TryGetValue("Authorization", out authorizationHeaderValue); 28 | AuthenticationHeaderValue.TryParse(authorizationHeaderValue, out var headerValue); 29 | var accessToken = headerValue?.Parameter; 30 | 31 | var path = context.HttpContext.Request.Path; 32 | if (!string.IsNullOrEmpty(accessToken) && 33 | (path.StartsWithSegments("/direct-notification"))) 34 | { 35 | context.Token = accessToken; 36 | } 37 | return Task.CompletedTask; 38 | } 39 | }; 40 | }); 41 | 42 | services.AddAuthorization(options => 43 | { 44 | options.AddPolicy("AccessAsUser", 45 | policy => policy.Requirements.Add(new ScopesRequirement("User.Access"))); 46 | }); 47 | 48 | services.AddSingleton(); 49 | 50 | services.AddHttpContextAccessor(); 51 | 52 | return services; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/Notification/SmartAccounting.Notification.API/Core/DependencyInjection/ConfigurationServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Options; 4 | using SmartAccounting.EventBus.Configuration; 5 | using SmartAccounting.Logging.Configuration; 6 | using SmartAccounting.Notification.API.Configuration; 7 | 8 | namespace SmartAccounting.Notification.API.Core.DependencyInjection 9 | { 10 | internal static class ConfigurationServiceCollectionExtensions 11 | { 12 | public static IServiceCollection AddAppConfiguration(this IServiceCollection services, IConfiguration config) 13 | { 14 | services.Configure(config.GetSection("ApplicationInsightsConfig")); 15 | services.AddSingleton, ApplicationInsightsServiceConfigurationValidation>(); 16 | var applicationInsightsServiceConfiguration = services.BuildServiceProvider().GetRequiredService>().Value; 17 | services.AddSingleton(applicationInsightsServiceConfiguration); 18 | 19 | services.Configure(config.GetSection("ServiceBusConfig")); 20 | services.AddSingleton, EventBusConfigurationValidation>(); 21 | var eventBusConfigurationConfiguration = services.BuildServiceProvider().GetRequiredService>().Value; 22 | services.AddSingleton(eventBusConfigurationConfiguration); 23 | 24 | services.Configure(config.GetSection("SqlDbConfig")); 25 | services.AddSingleton, SqlDbDataServiceConfigurationValidation>(); 26 | var sqlDbServiceConfiguration = services.BuildServiceProvider().GetRequiredService>().Value; 27 | services.AddSingleton(sqlDbServiceConfiguration); 28 | 29 | services.Configure(config.GetSection("AzureSignalRConfig")); 30 | services.AddSingleton, SignalRServiceConfigurationValidation>(); 31 | var signalRServiceConfiguration = services.BuildServiceProvider().GetRequiredService>().Value; 32 | services.AddSingleton(signalRServiceConfiguration); 33 | 34 | services.Configure(config.GetSection("AzureAdB2CConfig")); 35 | services.AddSingleton, AzureAdB2cServiceConfigurationValidation>(); 36 | var azureAdB2cServiceConfiguration = services.BuildServiceProvider().GetRequiredService>().Value; 37 | services.AddSingleton(azureAdB2cServiceConfiguration); 38 | 39 | return services; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/Notification/SmartAccounting.Notification.API/Core/DependencyInjection/RealTimeNotificationServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using SmartAccounting.Notification.API.Configuration; 3 | 4 | namespace SmartAccounting.Notification.API.Core.DependencyInjection 5 | { 6 | internal static class RealTimeNotificationServiceCollectionExtensions 7 | { 8 | public static IServiceCollection AddRealTimeNotificationServices(this IServiceCollection services) 9 | { 10 | var serviceProvider = services.BuildServiceProvider(); 11 | var signalRServiceConfiguration = serviceProvider.GetRequiredService(); 12 | 13 | services.AddSignalR() 14 | .AddAzureSignalR(signalRServiceConfiguration.ConnectionString); 15 | 16 | return services; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/Notification/SmartAccounting.Notification.API/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | 7 | FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build 8 | WORKDIR /src 9 | COPY ["Notification/SmartAccounting.Notification.API/SmartAccounting.Notification.API.csproj", "Notification/SmartAccounting.Notification.API/"] 10 | RUN dotnet restore "Notification/SmartAccounting.Notification.API/SmartAccounting.Notification.API.csproj" 11 | COPY . . 12 | WORKDIR "/src/Notification/SmartAccounting.Notification.API" 13 | RUN dotnet build "SmartAccounting.Notification.API.csproj" -c Release -o /app/build 14 | 15 | FROM build AS publish 16 | RUN dotnet publish "SmartAccounting.Notification.API.csproj" -c Release -o /app/publish 17 | 18 | FROM base AS final 19 | WORKDIR /app 20 | COPY --from=publish /app/publish . 21 | ENTRYPOINT ["dotnet", "SmartAccounting.Notification.API.dll"] -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/Notification/SmartAccounting.Notification.API/Infrastructure/AuthorizationPolicies/ScopesHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.Identity.Web; 3 | using System; 4 | using System.Linq; 5 | using System.Security.Claims; 6 | using System.Threading.Tasks; 7 | 8 | namespace SmartAccounting.Notification.API.Infrastructure.AuthorizationPolicies 9 | { 10 | internal class ScopesHandler : AuthorizationHandler 11 | { 12 | protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, 13 | ScopesRequirement requirement) 14 | { 15 | // If there are no scopes, do not process 16 | if (!context.User.Claims.Any(x => x.Type == ClaimConstants.Scope) 17 | && !context.User.Claims.Any(y => y.Type == ClaimConstants.Scp)) 18 | { 19 | return Task.CompletedTask; 20 | } 21 | 22 | Claim scopeClaim = context?.User?.FindFirst(ClaimConstants.Scp); 23 | 24 | if (scopeClaim == null) 25 | scopeClaim = context?.User?.FindFirst(ClaimConstants.Scope); 26 | 27 | if (scopeClaim != null && scopeClaim.Value.Equals(requirement.ScopeName, StringComparison.InvariantCultureIgnoreCase)) 28 | { 29 | context.Succeed(requirement); 30 | } 31 | 32 | return Task.CompletedTask; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/Notification/SmartAccounting.Notification.API/Infrastructure/AuthorizationPolicies/ScopesRequirement.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace SmartAccounting.Notification.API.Infrastructure.AuthorizationPolicies 4 | { 5 | public class ScopesRequirement : IAuthorizationRequirement 6 | { 7 | public readonly string ScopeName; 8 | 9 | public ScopesRequirement(string scopeName) 10 | { 11 | ScopeName = scopeName; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/Notification/SmartAccounting.Notification.API/Infrastructure/Services/RealTimeNotificationService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.SignalR; 2 | using Microsoft.Azure.SignalR.Management; 3 | using Microsoft.Extensions.Logging; 4 | using SmartAccounting.Notification.API.Application.Model; 5 | using SmartAccounting.Notification.API.CommunicationHubs; 6 | using SmartAccounting.Notification.API.Configuration; 7 | using System; 8 | using System.Threading.Tasks; 9 | 10 | namespace SmartAccounting.Notification.API.Infrastructure.Services 11 | { 12 | internal interface IRealTimeNotificationService 13 | { 14 | Task SendNotificationAsync(DocumentProcessedNotification documentProcessedNotification); 15 | } 16 | 17 | internal class RealTimeNotificationService : IRealTimeNotificationService 18 | { 19 | private readonly ILogger _logger; 20 | private readonly ISignalRServiceConfiguration _signalRServiceConfiguration; 21 | 22 | public RealTimeNotificationService(ILogger logger, 23 | ISignalRServiceConfiguration signalRServiceConfiguration) 24 | { 25 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 26 | _signalRServiceConfiguration = signalRServiceConfiguration 27 | ?? throw new ArgumentNullException(nameof(signalRServiceConfiguration)); 28 | } 29 | 30 | public async Task SendNotificationAsync(DocumentProcessedNotification documentProcessedNotification) 31 | { 32 | using var hubServiceManager = new ServiceManagerBuilder() 33 | .WithOptions(option => 34 | { 35 | option.ConnectionString = _signalRServiceConfiguration.ConnectionString; 36 | option.ServiceTransportType = ServiceTransportType.Persistent; 37 | }).Build(); 38 | 39 | var hubContext = await hubServiceManager.CreateHubContextAsync(DocumentNotificationHub.HubName); 40 | await hubContext 41 | .Clients 42 | .User(documentProcessedNotification.UserId) 43 | .SendAsync(DocumentNotificationHub.HubMethodName, documentProcessedNotification); 44 | 45 | _logger.LogInformation($"Real time notification sent using SignalR Service to the user with ID: {documentProcessedNotification.UserId}"); 46 | _logger.LogInformation($"Real time notification sent using SignalR Service with invoice ID: {documentProcessedNotification.InvoiceId}"); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/Notification/SmartAccounting.Notification.API/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace SmartAccounting.Notification.API 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => 16 | { 17 | webBuilder.UseStartup(); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/Notification/SmartAccounting.Notification.API/SmartAccounting.Notification.API.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0 5 | Linux 6 | ..\.. 7 | ..\..\docker-compose.dcproj 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/Notification/SmartAccounting.Notification.API/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using SmartAccounting.Logging; 7 | using SmartAccounting.Notification.API.CommunicationHubs; 8 | using SmartAccounting.Notification.API.Core.DependencyInjection; 9 | 10 | namespace SmartAccounting.Notification.API 11 | { 12 | public class Startup 13 | { 14 | public Startup(IConfiguration configuration) 15 | { 16 | Configuration = configuration; 17 | } 18 | 19 | public IConfiguration Configuration { get; } 20 | 21 | // This method gets called by the runtime. Use this method to add services to the container. 22 | public void ConfigureServices(IServiceCollection services) 23 | { 24 | services.AddHealthChecks(); 25 | services.AddAppConfiguration(Configuration); 26 | services.AddLoggingServices(); 27 | services.AddAuthenticationWithAuthorizationSupport(Configuration); 28 | services.AddRealTimeNotificationServices(); 29 | services.AddIntegrationServices(); 30 | services.AddControllers(); 31 | } 32 | 33 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 34 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 35 | { 36 | if (env.IsDevelopment()) 37 | { 38 | app.UseDeveloperExceptionPage(); 39 | } 40 | 41 | app.UseRouting(); 42 | 43 | app.UseAuthentication(); 44 | app.UseAuthorization(); 45 | 46 | app.UseEndpoints(endpoints => 47 | { 48 | endpoints.MapHealthChecks("/health"); 49 | endpoints.MapHub("/direct-notification"); 50 | }); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/Notification/SmartAccounting.Notification.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | 11 | "ApplicationInsightsConfig": { 12 | "InstrumentationKey": "" 13 | }, 14 | 15 | "AzureAdB2CConfig": { 16 | "Instance": "https://xxx.b2clogin.com", 17 | "ClientId": "", 18 | "Domain": "xxx.onmicrosoft.com", 19 | "SignUpSignInPolicyId": "B2C_1A_SIGNINSIGNUP" 20 | }, 21 | 22 | "SqlDbConfig": { 23 | "ConnectionString": "" 24 | }, 25 | 26 | "ServiceBusConfig": { 27 | "ListenAndSendConnectionString": "", 28 | "TopicName": "smart-accounting-events", 29 | "Subscription": "notification" 30 | }, 31 | 32 | "AzureSignalRConfig": { 33 | "ConnectionString": "" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/ProcessedDocument/SmartAccounting.ProcessedDocument.API/Application/DTO/UserInvoiceDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SmartAccounting.ProcessedDocument.API.Application.DTO 4 | { 5 | internal class UserInvoiceDto 6 | { 7 | public string Id { get; set; } 8 | 9 | public string UserId { get; set; } 10 | 11 | public DateTime InvoiceDate { get; set; } 12 | 13 | public string CustomerAddress { get; set; } 14 | 15 | public string CustomerAddressRecipient { get; set; } 16 | 17 | public string CustomerName { get; set; } 18 | 19 | public DateTime DueDate { get; set; } 20 | 21 | public string InvoiceId { get; set; } 22 | 23 | public string VendorAddress { get; set; } 24 | 25 | public string VendorName { get; set; } 26 | 27 | public float InvoiceTotal { get; set; } 28 | 29 | public string InvoiceFileUrl { get; set; } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/ProcessedDocument/SmartAccounting.ProcessedDocument.API/Application/Model/UserInvoice.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace SmartAccounting.ProcessedDocument.API.Application.Model 5 | { 6 | internal class UserInvoice 7 | { 8 | [JsonPropertyName("id")] 9 | public string Id { get; set; } 10 | 11 | [JsonPropertyName("userId")] 12 | public string UserId { get; set; } 13 | 14 | [JsonPropertyName("invoiceDate")] 15 | public DateTime InvoiceDate { get; set; } 16 | 17 | [JsonPropertyName("customerAddress")] 18 | public string CustomerAddress { get; set; } 19 | 20 | [JsonPropertyName("customerAddressRecipient")] 21 | public string CustomerAddressRecipient { get; set; } 22 | 23 | [JsonPropertyName("customerName")] 24 | public string CustomerName { get; set; } 25 | 26 | [JsonPropertyName("dueDate")] 27 | public DateTime DueDate { get; set; } 28 | 29 | [JsonPropertyName("invoiceId")] 30 | public string InvoiceId { get; set; } 31 | 32 | [JsonPropertyName("vendorAddress")] 33 | public string VendorAddress { get; set; } 34 | 35 | [JsonPropertyName("vendorName")] 36 | public string VendorName { get; set; } 37 | 38 | [JsonPropertyName("invoiceTotal")] 39 | public float InvoiceTotal { get; set; } 40 | 41 | [JsonPropertyName("fileUrl")] 42 | public string FileUrl { get; set; } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/ProcessedDocument/SmartAccounting.ProcessedDocument.API/Application/Repositories/UserInvoiceRepository.cs: -------------------------------------------------------------------------------- 1 | using SmartAccounting.ProcessedDocument.API.Application.Model; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace SmartAccounting.ProcessedDocument.API.Application.Repositories 6 | { 7 | internal interface IUserInvoiceRepository 8 | { 9 | Task DeleteAsync(UserInvoice userInvoice); 10 | Task GetAsync(UserInvoice userInvoice); 11 | Task> GetAllAsync(string userId); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/ProcessedDocument/SmartAccounting.ProcessedDocument.API/Configuration/CosmosDbServiceConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace SmartAccounting.ProcessedDocument.API.Configuration 4 | { 5 | internal interface ICosmosDbServiceConfiguration 6 | { 7 | string ConnectionString { get; set; } 8 | string DatabaseName { get; set; } 9 | string InvoiceContainerName { get; set; } 10 | string InvoiceContainerPartitionKeyPath { get; set; } 11 | } 12 | 13 | internal class CosmosDbServiceConfiguration : ICosmosDbServiceConfiguration 14 | { 15 | public string ConnectionString { get; set; } 16 | public string DatabaseName { get; set; } 17 | public string InvoiceContainerName { get; set; } 18 | public string InvoiceContainerPartitionKeyPath { get; set; } 19 | } 20 | 21 | internal class CosmosDbDataServiceConfigurationValidation : IValidateOptions 22 | { 23 | public ValidateOptionsResult Validate(string name, CosmosDbServiceConfiguration options) 24 | { 25 | if (string.IsNullOrEmpty(options.ConnectionString)) 26 | { 27 | return ValidateOptionsResult.Fail($"{nameof(options.ConnectionString)} configuration parameter for the Azure Cosmos DB is required"); 28 | } 29 | 30 | if (string.IsNullOrEmpty(options.InvoiceContainerName)) 31 | { 32 | return ValidateOptionsResult.Fail($"{nameof(options.InvoiceContainerName)} configuration parameter for the Azure Cosmos DB is required"); 33 | } 34 | 35 | if (string.IsNullOrEmpty(options.DatabaseName)) 36 | { 37 | return ValidateOptionsResult.Fail($"{nameof(options.DatabaseName)} configuration parameter for the Azure Cosmos DB is required"); 38 | } 39 | 40 | if (string.IsNullOrEmpty(options.InvoiceContainerPartitionKeyPath)) 41 | { 42 | return ValidateOptionsResult.Fail($"{nameof(options.InvoiceContainerPartitionKeyPath)} configuration parameter for the Azure Cosmos DB is required"); 43 | } 44 | 45 | return ValidateOptionsResult.Success; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/ProcessedDocument/SmartAccounting.ProcessedDocument.API/Configuration/StorageServiceConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace SmartAccounting.ProcessedDocument.API.Configuration 4 | { 5 | internal interface IStorageServiceConfiguration 6 | { 7 | string Key { get; set; } 8 | string AccountName { get; set; } 9 | string ConnectionString { get; set; } 10 | } 11 | 12 | internal class StorageServiceConfiguration : IStorageServiceConfiguration 13 | { 14 | public string Key { get; set; } 15 | public string AccountName { get; set; } 16 | public string ContainerName { get; set; } 17 | public string ConnectionString { get; set; } 18 | } 19 | 20 | internal class StorageServiceConfigurationValidation : IValidateOptions 21 | { 22 | public ValidateOptionsResult Validate(string name, StorageServiceConfiguration options) 23 | { 24 | if (string.IsNullOrEmpty(options.Key)) 25 | { 26 | return ValidateOptionsResult.Fail($"{nameof(options.Key)} configuration parameter for the Azure Storage Account is required"); 27 | } 28 | 29 | if (string.IsNullOrEmpty(options.AccountName)) 30 | { 31 | return ValidateOptionsResult.Fail($"{nameof(options.AccountName)} configuration parameter for the Azure Storage Account is required"); 32 | } 33 | 34 | if (string.IsNullOrEmpty(options.ConnectionString)) 35 | { 36 | return ValidateOptionsResult.Fail($"{nameof(options.ConnectionString)} configuration parameter for the Azure Storage Account is required"); 37 | } 38 | 39 | return ValidateOptionsResult.Success; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/ProcessedDocument/SmartAccounting.ProcessedDocument.API/Configuration/SwaggerConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace SmartAccounting.ProcessedDocument.API.Configuration 4 | { 5 | internal interface ISwaggerConfiguration 6 | { 7 | string Root { get; set; } 8 | string ServiceName { get; set; } 9 | string ServiceVersion { get; set; } 10 | } 11 | 12 | internal class SwaggerConfiguration : ISwaggerConfiguration 13 | { 14 | public string Root { get; set; } 15 | public string ServiceName { get; set; } 16 | public string ServiceVersion { get; set; } 17 | } 18 | 19 | internal class SwaggerConfigurationValidation : IValidateOptions 20 | { 21 | public ValidateOptionsResult Validate(string name, SwaggerConfiguration options) 22 | { 23 | if (string.IsNullOrEmpty(options.Root)) 24 | { 25 | return ValidateOptionsResult.Fail($"{nameof(options.Root)} configuration parameter for the Swagger is required"); 26 | } 27 | 28 | if (string.IsNullOrEmpty(options.ServiceName)) 29 | { 30 | return ValidateOptionsResult.Fail($"{nameof(options.ServiceName)} configuration parameter for the Swagger is required"); 31 | } 32 | 33 | if (string.IsNullOrEmpty(options.ServiceVersion)) 34 | { 35 | return ValidateOptionsResult.Fail($"{nameof(options.ServiceVersion)} configuration parameter for the Swagger is required"); 36 | } 37 | 38 | return ValidateOptionsResult.Success; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/ProcessedDocument/SmartAccounting.ProcessedDocument.API/Controllers/DocumentController.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Extensions.Logging; 5 | using SmartAccounting.ProcessedDocument.API.Application.DTO; 6 | using SmartAccounting.ProcessedDocument.API.Application.Queries; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Threading.Tasks; 10 | 11 | namespace SmartAccounting.ProcessedDocument.API.Controllers 12 | { 13 | [Authorize(Policy = "AccessAsUser")] 14 | [ApiController] 15 | [Route("[controller]")] 16 | public class DocumentController : ControllerBase 17 | { 18 | private readonly ILogger _logger; 19 | private readonly IMediator _mediator; 20 | 21 | public DocumentController(ILogger logger, 22 | IMediator mediator) 23 | { 24 | _logger = logger; 25 | _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); 26 | } 27 | 28 | /// 29 | /// Gets all scanned invoices for the user 30 | /// 31 | /// 32 | /// List of all scanned invoices for the user 33 | /// 34 | /// List with scanned invoices 35 | /// Access denied 36 | /// Cars list not found 37 | /// Oops! something went wrong 38 | [ProducesResponseType(typeof(IEnumerable), 200)] 39 | [HttpGet] 40 | public async Task GetAllInvoicesAsync() 41 | { 42 | var response = await _mediator.Send(new GetUserInvoicesQuery()); 43 | 44 | if (response.CompletedWithSuccess) 45 | { 46 | return Ok(response.Result); 47 | } 48 | 49 | else 50 | { 51 | return BadRequest(response.OperationError); 52 | } 53 | } 54 | 55 | /// 56 | /// Gets user invoice by ID 57 | /// 58 | /// 59 | /// Specific user invoice 60 | /// 61 | /// User invoice data 62 | /// Access denied 63 | /// Cars list not found 64 | /// Oops! something went wrong 65 | [ProducesResponseType(typeof(IEnumerable), 200)] 66 | [HttpGet("{invoiceId}")] 67 | public async Task GetInvoiceAsync([FromRoute] string invoiceId) 68 | { 69 | var response = await _mediator.Send(new GetUserInvoiceQuery(invoiceId)); 70 | 71 | if (response.CompletedWithSuccess) 72 | { 73 | return Ok(response.Result); 74 | } 75 | 76 | else 77 | { 78 | return BadRequest(response.OperationError); 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/ProcessedDocument/SmartAccounting.ProcessedDocument.API/Core/DependencyInjection/AuthenticationServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Identity.Web; 5 | using SmartAccounting.ProcessedDocument.API.Infrastructure.AuthorizationPolicies; 6 | using SmartAccounting.ProcessedDocument.API.Infrastructure.Identity; 7 | 8 | namespace SmartAccounting.ProcessedDocument.API.Core.DependencyInjection 9 | { 10 | internal static class AuthenticationServiceCollectionExtensions 11 | { 12 | public static IServiceCollection AddAuthenticationWithAuthorizationSupport(this IServiceCollection services, 13 | IConfiguration config) 14 | { 15 | services.AddMicrosoftIdentityWebApiAuthentication(config, "AzureAdB2CConfig"); 16 | 17 | services.AddAuthorization(options => 18 | { 19 | options.AddPolicy("AccessAsUser", 20 | policy => policy.Requirements.Add(new ScopesRequirement("User.Access"))); 21 | }); 22 | 23 | services.AddSingleton(); 24 | 25 | services.AddHttpContextAccessor(); 26 | services.AddTransient(); 27 | 28 | return services; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/ProcessedDocument/SmartAccounting.ProcessedDocument.API/Core/DependencyInjection/ConfigurationServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Options; 4 | using SmartAccounting.Logging.Configuration; 5 | using SmartAccounting.ProcessedDocument.API.Configuration; 6 | 7 | namespace SmartAccounting.ProcessedDocument.API.Core.DependencyInjection 8 | { 9 | internal static class ConfigurationServiceCollectionExtensions 10 | { 11 | public static IServiceCollection AddAppConfiguration(this IServiceCollection services, IConfiguration config) 12 | { 13 | services.Configure(config.GetSection("SwaggerConfig")); 14 | services.AddSingleton, SwaggerConfigurationValidation>(); 15 | var swaggerConfiguration = services.BuildServiceProvider().GetRequiredService>().Value; 16 | services.AddSingleton(swaggerConfiguration); 17 | 18 | services.Configure(config.GetSection("ApplicationInsightsConfig")); 19 | services.AddSingleton, ApplicationInsightsServiceConfigurationValidation>(); 20 | var applicationInsightsServiceConfiguration = services.BuildServiceProvider().GetRequiredService>().Value; 21 | services.AddSingleton(applicationInsightsServiceConfiguration); 22 | 23 | services.Configure(config.GetSection("BlobStorageConfig")); 24 | services.AddSingleton, StorageServiceConfigurationValidation>(); 25 | var storageServiceConfiguration = services.BuildServiceProvider().GetRequiredService>().Value; 26 | services.AddSingleton(storageServiceConfiguration); 27 | 28 | services.Configure(config.GetSection("CosmosDbConfig")); 29 | services.AddSingleton, CosmosDbDataServiceConfigurationValidation>(); 30 | var cosmosDbDataServiceConfiguration = services.BuildServiceProvider().GetRequiredService>().Value; 31 | services.AddSingleton(cosmosDbDataServiceConfiguration); 32 | 33 | return services; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/ProcessedDocument/SmartAccounting.ProcessedDocument.API/Core/DependencyInjection/DataServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Azure.Cosmos; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using SmartAccounting.ProcessedDocument.API.Application.Repositories; 4 | using SmartAccounting.ProcessedDocument.API.Configuration; 5 | using SmartAccounting.ProcessedDocument.API.Infrastructure.Repositories; 6 | 7 | namespace SmartAccounting.ProcessedDocument.API.Core.DependencyInjection 8 | { 9 | internal static class DataServiceCollectionExtensions 10 | { 11 | public static IServiceCollection AddDataServices(this IServiceCollection services) 12 | { 13 | 14 | services.AddSingleton(implementationFactory => 15 | { 16 | var cosmoDbConfiguration = implementationFactory.GetRequiredService(); 17 | CosmosClient cosmosClient = new CosmosClient(cosmoDbConfiguration.ConnectionString); 18 | CosmosDatabase database = cosmosClient.CreateDatabaseIfNotExistsAsync(cosmoDbConfiguration.DatabaseName) 19 | .GetAwaiter() 20 | .GetResult(); 21 | 22 | database.CreateContainerIfNotExistsAsync( 23 | cosmoDbConfiguration.InvoiceContainerName, 24 | cosmoDbConfiguration.InvoiceContainerPartitionKeyPath, 25 | 400) 26 | .GetAwaiter() 27 | .GetResult(); 28 | 29 | return cosmosClient; 30 | }); 31 | 32 | services.AddTransient(); 33 | 34 | return services; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/ProcessedDocument/SmartAccounting.ProcessedDocument.API/Core/DependencyInjection/StorageServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Azure.Storage.Blobs; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using SmartAccounting.ProcessedDocument.API.Configuration; 4 | using SmartAccounting.ProcessedDocument.API.Infrastructure.Services; 5 | 6 | namespace SmartAccounting.ProcessedDocument.API.Core.DependencyInjection 7 | { 8 | internal static class StorageServiceCollectionExtensions 9 | { 10 | public static IServiceCollection AddStorageServices(this IServiceCollection services) 11 | { 12 | var serviceProvider = services.BuildServiceProvider(); 13 | var storageConfiguration = serviceProvider.GetRequiredService(); 14 | 15 | services.AddSingleton(implementationFactory => 16 | { 17 | return new BlobServiceClient(storageConfiguration.ConnectionString); 18 | }); 19 | 20 | services.AddTransient(); 21 | 22 | return services; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/ProcessedDocument/SmartAccounting.ProcessedDocument.API/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | 7 | FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build 8 | WORKDIR /src 9 | COPY ["ProcessedDocument/SmartAccounting.ProcessedDocument.API/SmartAccounting.ProcessedDocument.API.csproj", "ProcessedDocument/SmartAccounting.ProcessedDocument.API/"] 10 | RUN dotnet restore "ProcessedDocument/SmartAccounting.ProcessedDocument.API/SmartAccounting.ProcessedDocument.API.csproj" 11 | COPY . . 12 | WORKDIR "/src/ProcessedDocument/SmartAccounting.ProcessedDocument.API" 13 | RUN dotnet build "SmartAccounting.ProcessedDocument.API.csproj" -c Release -o /app/build 14 | 15 | FROM build AS publish 16 | RUN dotnet publish "SmartAccounting.ProcessedDocument.API.csproj" -c Release -o /app/publish 17 | 18 | FROM base AS final 19 | WORKDIR /app 20 | COPY --from=publish /app/publish . 21 | ENTRYPOINT ["dotnet", "SmartAccounting.ProcessedDocument.API.dll"] -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/ProcessedDocument/SmartAccounting.ProcessedDocument.API/Infrastructure/AuthorizationPolicies/ScopesHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.Identity.Web; 3 | using System; 4 | using System.Linq; 5 | using System.Security.Claims; 6 | using System.Threading.Tasks; 7 | 8 | namespace SmartAccounting.ProcessedDocument.API.Infrastructure.AuthorizationPolicies 9 | { 10 | internal class ScopesHandler : AuthorizationHandler 11 | { 12 | protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, 13 | ScopesRequirement requirement) 14 | { 15 | // If there are no scopes, do not process 16 | if (!context.User.Claims.Any(x => x.Type == ClaimConstants.Scope) 17 | && !context.User.Claims.Any(y => y.Type == ClaimConstants.Scp)) 18 | { 19 | return Task.CompletedTask; 20 | } 21 | 22 | Claim scopeClaim = context?.User?.FindFirst(ClaimConstants.Scp); 23 | 24 | if (scopeClaim == null) 25 | scopeClaim = context?.User?.FindFirst(ClaimConstants.Scope); 26 | 27 | if (scopeClaim != null && scopeClaim.Value.Equals(requirement.ScopeName, StringComparison.InvariantCultureIgnoreCase)) 28 | { 29 | context.Succeed(requirement); 30 | } 31 | 32 | return Task.CompletedTask; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/ProcessedDocument/SmartAccounting.ProcessedDocument.API/Infrastructure/AuthorizationPolicies/ScopesRequirement.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace SmartAccounting.ProcessedDocument.API.Infrastructure.AuthorizationPolicies 4 | { 5 | public class ScopesRequirement : IAuthorizationRequirement 6 | { 7 | public readonly string ScopeName; 8 | 9 | public ScopesRequirement(string scopeName) 10 | { 11 | ScopeName = scopeName; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/ProcessedDocument/SmartAccounting.ProcessedDocument.API/Infrastructure/Identity/IdentityService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using System; 3 | using System.Security.Claims; 4 | 5 | namespace SmartAccounting.ProcessedDocument.API.Infrastructure.Identity 6 | { 7 | public interface IIdentityService 8 | { 9 | Guid GetUserIdentity(); 10 | string GetUserEmail(); 11 | } 12 | 13 | public class IdentityService : IIdentityService 14 | { 15 | private readonly IHttpContextAccessor _context; 16 | 17 | public IdentityService(IHttpContextAccessor context) 18 | { 19 | _context = context ?? throw new ArgumentNullException(nameof(context)); 20 | } 21 | 22 | public Guid GetUserIdentity() 23 | { 24 | var userId = _context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value; 25 | return new Guid(userId); 26 | } 27 | 28 | public string GetUserEmail() 29 | { 30 | var userEmail = _context.HttpContext.User.FindFirst("emails").Value; 31 | return userEmail; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/ProcessedDocument/SmartAccounting.ProcessedDocument.API/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace SmartAccounting.ProcessedDocument.API 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => 16 | { 17 | webBuilder.UseStartup(); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/ProcessedDocument/SmartAccounting.ProcessedDocument.API/SmartAccounting.ProcessedDocument.API.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0 5 | Linux 6 | ..\.. 7 | ..\..\docker-compose.dcproj 8 | 9 | 10 | 11 | true 12 | $(NoWarn);1591 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/ProcessedDocument/SmartAccounting.ProcessedDocument.API/Startup.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using SmartAccounting.Common.ExceptionMiddleware; 8 | using SmartAccounting.Logging; 9 | using SmartAccounting.ProcessedDocument.API.Core.DependencyInjection; 10 | 11 | namespace SmartAccounting.ProcessedDocument.API 12 | { 13 | public class Startup 14 | { 15 | public Startup(IConfiguration configuration) 16 | { 17 | Configuration = configuration; 18 | } 19 | 20 | public IConfiguration Configuration { get; } 21 | 22 | // This method gets called by the runtime. Use this method to add services to the container. 23 | public void ConfigureServices(IServiceCollection services) 24 | { 25 | services.AddAppConfiguration(Configuration); 26 | services.AddLoggingServices(); 27 | services.AddAuthenticationWithAuthorizationSupport(Configuration); 28 | services.AddMediatR(typeof(Startup)); 29 | services.AddStorageServices(); 30 | services.AddDataServices(); 31 | services.AddControllers(); 32 | services.AddSwagger(); 33 | } 34 | 35 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 36 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 37 | { 38 | app.UseMiddleware(); 39 | 40 | if (env.IsDevelopment()) 41 | { 42 | app.UseDeveloperExceptionPage(); 43 | } 44 | 45 | app.UseSwaggerServices(); 46 | app.UseRouting(); 47 | 48 | app.UseAuthentication(); 49 | app.UseAuthorization(); 50 | 51 | app.UseEndpoints(endpoints => 52 | { 53 | endpoints.MapControllers(); 54 | }); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/ProcessedDocument/SmartAccounting.ProcessedDocument.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | 11 | "SwaggerConfig": { 12 | "Root": "/processed-document-api", 13 | "ServiceName": "Processed Document API", 14 | "ServiceVersion": "v1" 15 | }, 16 | "AzureAdB2CConfig": { 17 | "Instance": "https://xxx.b2clogin.com", 18 | "ClientId": "", 19 | "Domain": "xxx.onmicrosoft.com", 20 | "SignUpSignInPolicyId": "B2C_1A_SIGNINSIGNUP" 21 | }, 22 | 23 | "ApplicationInsightsConfig": { 24 | "InstrumentationKey": "" 25 | }, 26 | 27 | "CosmosDbConfig": { 28 | "ConnectionString": "", 29 | "DatabaseName": "smart-accounting", 30 | "InvoiceContainerName": "invoice", 31 | "InvoiceContainerPartitionKeyPath": "/userId" 32 | }, 33 | 34 | "BlobStorageConfig": { 35 | "ConnectionString": "", 36 | "AccountName": "stsmartaccounting", 37 | "Key": "" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/docker-compose.dcproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2.1 5 | Linux 6 | 645a6ca1-9ee5-4d8b-90da-b57144057b31 7 | LaunchBrowser 8 | {Scheme}://localhost:{ServicePort}/swagger 9 | smartaccounting.documentanalyzer.api 10 | 11 | 12 | 13 | docker-compose.yml 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | documentanalyzer.api: 5 | environment: 6 | - ASPNETCORE_ENVIRONMENT=Development 7 | ports: 8 | - "80" 9 | 10 | fileprocessor.api: 11 | environment: 12 | - ASPNETCORE_ENVIRONMENT=Development 13 | ports: 14 | - "80" 15 | 16 | 17 | notification.api: 18 | environment: 19 | - ASPNETCORE_ENVIRONMENT=Development 20 | ports: 21 | - "80" 22 | 23 | 24 | processeddocument.api: 25 | environment: 26 | - ASPNETCORE_ENVIRONMENT=Development 27 | ports: 28 | - "80" 29 | 30 | -------------------------------------------------------------------------------- /src/smart-accounting-backend-services/src/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | documentanalyzer.api: 5 | image: ${DOCKER_REGISTRY-}documentanalyzerapi 6 | build: 7 | context: . 8 | dockerfile: DocumentAnalyzer/SmartAccounting.DocumentAnalyzer.API/Dockerfile 9 | 10 | 11 | fileprocessor.api: 12 | image: ${DOCKER_REGISTRY-}fileprocessorapi 13 | build: 14 | context: . 15 | dockerfile: FileProcessor/SmartAccounting.FileProcessor.API/Dockerfile 16 | 17 | 18 | notification.api: 19 | image: ${DOCKER_REGISTRY-}notificationapi 20 | build: 21 | context: . 22 | dockerfile: Notification/SmartAccounting.Notification.API/Dockerfile 23 | 24 | 25 | processeddocument.api: 26 | image: ${DOCKER_REGISTRY-}processeddocumentapi 27 | build: 28 | context: . 29 | dockerfile: ProcessedDocument/SmartAccounting.ProcessedDocument.API/Dockerfile 30 | -------------------------------------------------------------------------------- /src/smart-accounting-infrastructure/aks-config/aks-namespace.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: dev-smart-accounting-services -------------------------------------------------------------------------------- /src/smart-accounting-infrastructure/aks-config/certificates.yml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1alpha2 2 | kind: Certificate 3 | metadata: 4 | name: tls-secret 5 | namespace: ingress-basic 6 | spec: 7 | secretName: tls-secret 8 | dnsNames: 9 | - smart-accounting.westeurope.cloudapp.azure.com 10 | acme: 11 | config: 12 | - http01: 13 | ingressClass: nginx 14 | domains: 15 | - smart-accounting.westeurope.cloudapp.azure.com 16 | issuerRef: 17 | name: letsencrypt-prod 18 | kind: ClusterIssuer -------------------------------------------------------------------------------- /src/smart-accounting-infrastructure/aks-config/cluster-issuer.yml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1alpha2 2 | kind: ClusterIssuer 3 | metadata: 4 | name: letsencrypt-prod 5 | spec: 6 | acme: 7 | server: https://acme-v02.api.letsencrypt.org/directory 8 | email: TBA@xxx.com 9 | privateKeySecretRef: 10 | name: letsencrypt-prod 11 | solvers: 12 | - http01: 13 | ingress: 14 | class: nginx 15 | podTemplate: 16 | spec: 17 | nodeSelector: 18 | "kubernetes.io/os": linux -------------------------------------------------------------------------------- /src/smart-accounting-web-app/azure-pipelines/azure-pipelines-build-template.yml: -------------------------------------------------------------------------------- 1 | 2 | jobs: 3 | - job: 'Build' 4 | displayName: "Build Smart Accounting Web App" 5 | pool: 6 | vmImage: 'VS2017-Win2016' 7 | steps: 8 | 9 | - task: DotNetCoreCLI@2 10 | displayName: Restore NuGet packages 11 | inputs: 12 | command: 'restore' 13 | projects: '**/*.csproj' 14 | 15 | - task: DotNetCoreCLI@2 16 | displayName: Build project 17 | inputs: 18 | command: 'build' 19 | projects: '**/*.csproj' 20 | 21 | - task: DotNetCoreCLI@2 22 | displayName: Publish project 23 | inputs: 24 | command: publish 25 | arguments: '--configuration Release --output $(Build.ArtifactStagingDirectory)' 26 | projects: '**/*.csproj' 27 | zipAfterPublish: true 28 | 29 | - task: PublishBuildArtifacts@1 30 | displayName: Publish package ready for deployment 31 | inputs: 32 | PathtoPublish: '$(Build.ArtifactStagingDirectory)' 33 | artifactName: 'drop' -------------------------------------------------------------------------------- /src/smart-accounting-web-app/azure-pipelines/azure-pipelines-deployment-template.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | - deployment: Deploy 3 | displayName: "Deploy Smart Accounting Web App" 4 | environment: ${{parameters.environment}} 5 | pool: 6 | vmImage: 'VS2017-Win2016' 7 | strategy: 8 | runOnce: 9 | deploy: 10 | steps: 11 | - task: DownloadBuildArtifacts@0 12 | displayName: 'Download the build artifacts' 13 | inputs: 14 | buildType: 'current' 15 | downloadType: 'single' 16 | artifactName: 'drop' 17 | downloadPath: '$(System.DefaultWorkingDirectory)' 18 | 19 | - task: AzureWebApp@1 20 | displayName: Release package to Azure Web App 21 | inputs: 22 | azureSubscription: ${{parameters.azureConnectionName}} 23 | appName: ${{parameters.webAppName}} 24 | resourceGroupName: ${{parameters.resourceGroupName}} 25 | package: $(System.DefaultWorkingDirectory)/**/*.zip 26 | deploymentMethod: zipDeploy -------------------------------------------------------------------------------- /src/smart-accounting-web-app/azure-pipelines/azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | 4 | stages: 5 | - stage: Build 6 | displayName: 'Build Smart Accounting Web App' 7 | jobs: 8 | - template: azure-pipelines-build-template.yml 9 | 10 | - stage: DeployDEV 11 | displayName: 'Deploy Smart Accounting Web App to DEV environment' 12 | condition: succeeded() 13 | dependsOn: Build 14 | variables: 15 | - group: 'smart-accounting-web-app-dev-env-vg' 16 | jobs: 17 | - template: azure-pipelines-deployment-template.yml 18 | parameters: 19 | azureConnectionName: '$(azureResourceManagerConnectionName)' 20 | webAppName: '$(webAppName)' 21 | resourceGroupName: '$(resourceGroupName)' 22 | environment: 'DEV' -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Sorry, there's nothing at this address.

9 |
10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/Application/Infrastructure/ApiService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Identity.Web; 3 | using System; 4 | using System.Net.Http; 5 | using System.Net.Http.Headers; 6 | using System.Threading.Tasks; 7 | 8 | namespace SmartAccounting.WebPortal.Application.Infrastructure 9 | { 10 | internal abstract class ApiService 11 | { 12 | protected readonly HttpClient _httpClient; 13 | protected readonly ITokenAcquisition _tokenAcquisition; 14 | protected readonly IConfiguration _configuration; 15 | 16 | protected abstract string ApiScope { get; } 17 | 18 | public ApiService(HttpClient httpClient, 19 | ITokenAcquisition tokenAcquisition, 20 | IConfiguration configuration) 21 | { 22 | _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); 23 | _tokenAcquisition = tokenAcquisition ?? throw new ArgumentNullException(nameof(tokenAcquisition)); 24 | _configuration = configuration ?? throw new ArgumentNullException(nameof(tokenAcquisition)); 25 | } 26 | 27 | protected async Task GetAndAddApiAccessTokenToAuthorizationHeaderAsync() 28 | { 29 | string[] scopes = new[] { _configuration[ApiScope] }; 30 | string accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes); 31 | _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); 32 | } 33 | 34 | protected void GetAndAddApiSubscriptionKeyHeaderAsync() 35 | { 36 | var subscriptionKey = _configuration["SmartAccountingApis:SubscriptionKey"]; 37 | _httpClient.DefaultRequestHeaders.Remove("Ocp-Apim-Subscription-Key"); 38 | _httpClient.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", subscriptionKey); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/Application/Infrastructure/FileProcessorApiService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Identity.Web; 3 | using SmartAccounting.WebPortal.Application.Model; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | 7 | namespace SmartAccounting.WebPortal.Application.Infrastructure 8 | { 9 | internal interface IFileProcessorApiService 10 | { 11 | Task UploadInvoiceAsync(string attachmentFileName, InvoiceUpload invoiceUpload); 12 | } 13 | 14 | internal class FileProcessorApiService : ApiService, IFileProcessorApiService 15 | { 16 | public FileProcessorApiService(HttpClient httpClient, 17 | ITokenAcquisition tokenAcquisition, 18 | IConfiguration configuration) : base(httpClient, tokenAcquisition, configuration) { } 19 | 20 | protected override string ApiScope => "SmartAccountingApis:FileProcessorApiScope"; 21 | 22 | public async Task UploadInvoiceAsync(string attachmentFileName, InvoiceUpload invoiceUpload) 23 | { 24 | await GetAndAddApiAccessTokenToAuthorizationHeaderAsync(); 25 | GetAndAddApiSubscriptionKeyHeaderAsync(); 26 | 27 | var multipartContent = new MultipartFormDataContent(); 28 | if (invoiceUpload.Attachment != null) 29 | { 30 | multipartContent.Add(new StreamContent(invoiceUpload.Attachment), "Files", attachmentFileName); 31 | } 32 | var requestURI = _configuration["SmartAccountingApis:FileProcessorApiUrl"]; 33 | var response = await _httpClient.PostAsync($"{requestURI}/file/upload", multipartContent); 34 | return response; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/Application/Infrastructure/ProcessedDocumentApiService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Identity.Web; 3 | using SmartAccounting.WebPortal.Application.Model; 4 | using System.Collections.Generic; 5 | using System.Net.Http; 6 | using System.Text.Json; 7 | using System.Threading.Tasks; 8 | 9 | namespace SmartAccounting.WebPortal.Application.Infrastructure 10 | { 11 | internal interface IProcessedDocumentApiService 12 | { 13 | Task> GetAllInvoicesAsync(); 14 | } 15 | 16 | internal class ProcessedDocumentApiService : ApiService, IProcessedDocumentApiService 17 | { 18 | public ProcessedDocumentApiService(HttpClient httpClient, 19 | ITokenAcquisition tokenAcquisition, 20 | IConfiguration configuration) : base(httpClient, tokenAcquisition, configuration) { } 21 | 22 | protected override string ApiScope => "SmartAccountingApis:ProcessedDocumentApiScope"; 23 | 24 | public async Task> GetAllInvoicesAsync() 25 | { 26 | await GetAndAddApiAccessTokenToAuthorizationHeaderAsync(); 27 | GetAndAddApiSubscriptionKeyHeaderAsync(); 28 | var allInvoices = new List(); 29 | 30 | var requestURI = _configuration["SmartAccountingApis:ProcessedDocumentApiUrl"]; 31 | var response = await _httpClient.GetAsync($"{requestURI}/document"); 32 | if (response.IsSuccessStatusCode) 33 | { 34 | var dataAsString = await response.Content.ReadAsStringAsync(); 35 | allInvoices = JsonSerializer.Deserialize>(dataAsString); 36 | } 37 | 38 | return allInvoices.AsReadOnly(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/Application/Infrastructure/SignalRCommunicationService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.SignalR.Client; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Identity.Web; 4 | using SmartAccounting.WebPortal.Application.Model; 5 | using System; 6 | using System.Threading.Tasks; 7 | 8 | namespace SmartAccounting.WebPortal.Application.Infrastructure 9 | { 10 | internal class SignalRCommunicationService 11 | { 12 | private readonly ITokenAcquisition _tokenAcquisition; 13 | private readonly IConfiguration _config; 14 | private HubConnection _hubConnection; 15 | 16 | public event Action OnMessageReceived; 17 | 18 | public SignalRCommunicationService(ITokenAcquisition tokenAcquisition, 19 | IConfiguration config) 20 | { 21 | _tokenAcquisition = tokenAcquisition ?? throw new ArgumentNullException(nameof(tokenAcquisition)); 22 | _config = config ?? throw new ArgumentNullException(nameof(config)); 23 | } 24 | 25 | public async Task InitializeAsync() 26 | { 27 | try 28 | { 29 | var hubUrl = _config["SmartAccountingApis:NotificationHubUrl"]; 30 | 31 | _hubConnection = new HubConnectionBuilder() 32 | .WithUrl(hubUrl, options => 33 | { 34 | options.AccessTokenProvider = async () => 35 | { 36 | string[] scopes = new[] { _config["SmartAccountingApis:NotificationApiScope"] }; 37 | string accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes); 38 | return accessToken; 39 | }; 40 | }) 41 | .Build(); 42 | 43 | await _hubConnection.StartAsync(); 44 | } 45 | 46 | catch (Exception ex) 47 | { 48 | if (ex.Message.Contains("IDW10502", 49 | StringComparison.InvariantCultureIgnoreCase)) 50 | { 51 | System.Diagnostics.Debug.WriteLine("SignalR connection cannot be estabilished before user is authenticated."); 52 | } 53 | 54 | else 55 | { 56 | System.Diagnostics.Debug.WriteLine("SignalR connection cannot be estabilished - unauthorized access."); 57 | } 58 | } 59 | } 60 | 61 | public void SubscribeHubMethod() 62 | { 63 | _hubConnection.On("direct-notification", (documentProcessedNotification) => 64 | { 65 | OnMessageReceived?.Invoke(documentProcessedNotification); 66 | }); 67 | } 68 | 69 | public async Task CloseConnectionAsync() 70 | { 71 | await _hubConnection.DisposeAsync(); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/Application/Model/DocumentProcessedNotification.cs: -------------------------------------------------------------------------------- 1 | namespace SmartAccounting.WebPortal.Application.Model 2 | { 3 | internal class DocumentProcessedNotification 4 | { 5 | public string UserId { get; set; } 6 | public string InvoiceId { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/Application/Model/Invoice.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace SmartAccounting.WebPortal.Application.Model 5 | { 6 | internal class Invoice 7 | { 8 | [JsonPropertyName("id")] 9 | public string Id { get; set; } 10 | 11 | [JsonPropertyName("userId")] 12 | public string UserId { get; set; } 13 | 14 | [JsonPropertyName("invoiceDate")] 15 | public DateTime InvoiceDate { get; set; } 16 | 17 | [JsonPropertyName("customerAddress")] 18 | public string CustomerAddress { get; set; } 19 | 20 | [JsonPropertyName("customerAddressRecipient")] 21 | public string CustomerAddressRecipient { get; set; } 22 | 23 | [JsonPropertyName("customerName")] 24 | public string CustomerName { get; set; } 25 | 26 | [JsonPropertyName("dueDate")] 27 | public DateTime DueDate { get; set; } 28 | 29 | [JsonPropertyName("invoiceId")] 30 | public string InvoiceId { get; set; } 31 | 32 | [JsonPropertyName("vendorAddress")] 33 | public string VendorAddress { get; set; } 34 | 35 | [JsonPropertyName("vendorName")] 36 | public string VendorName { get; set; } 37 | 38 | [JsonPropertyName("invoiceTotal")] 39 | public float InvoiceTotal { get; set; } 40 | 41 | [JsonPropertyName("invoiceFileUrl")] 42 | public string InvoiceFileUrl { get; set; } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/Application/Model/InvoiceUpload.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace SmartAccounting.WebPortal.Application.Model 4 | { 5 | public class InvoiceUpload 6 | { 7 | public Stream Attachment { get; set; } 8 | public string AttachmentFileName { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/AuthorizationPolicies/ClaimAuthorizationHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using System; 3 | using System.Linq; 4 | using System.Security.Claims; 5 | using System.Threading.Tasks; 6 | 7 | namespace SmartAccounting.WebPortal.AuthorizationPolicies 8 | { 9 | public class ClaimAuthorizationHandler : AuthorizationHandler 10 | { 11 | protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ClaimRequirement requirement) 12 | { 13 | var roleClaims = context 14 | .User 15 | .Claims 16 | .Where(c => c.Type == ClaimTypes.Role); 17 | 18 | if (roleClaims != null) 19 | { 20 | foreach (var roleClaim in roleClaims) 21 | { 22 | if (roleClaim != null) 23 | { 24 | if (roleClaim.Value.Equals(requirement.ClaimName, 25 | StringComparison.InvariantCultureIgnoreCase)) 26 | { 27 | context.Succeed(requirement); 28 | } 29 | } 30 | } 31 | } 32 | 33 | return Task.CompletedTask; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/AuthorizationPolicies/ClaimRequirement.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace SmartAccounting.WebPortal.AuthorizationPolicies 4 | { 5 | public class ClaimRequirement : IAuthorizationRequirement 6 | { 7 | public string ClaimName { get; set; } 8 | 9 | public ClaimRequirement(string claimName) 10 | { 11 | ClaimName = claimName; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model SmartAccounting.WebPortal.Pages.ErrorModel 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Error 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

Error.

19 |

An error occurred while processing your request.

20 | 21 | @if (Model.ShowRequestId) 22 | { 23 |

24 | Request ID: @Model.RequestId 25 |

26 | } 27 | 28 |

Development Mode

29 |

30 | Swapping to the Development environment displays detailed information about the error that occurred. 31 |

32 |

33 | The Development environment shouldn't be enabled for deployed applications. 34 | It can result in displaying sensitive information from exceptions to end users. 35 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 36 | and restarting the app. 37 |

38 |
39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using Microsoft.Extensions.Logging; 4 | using System.Diagnostics; 5 | 6 | namespace SmartAccounting.WebPortal.Pages 7 | { 8 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 9 | [IgnoreAntiforgeryToken] 10 | public class ErrorModel : PageModel 11 | { 12 | public string RequestId { get; set; } 13 | 14 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 15 | 16 | private readonly ILogger _logger; 17 | 18 | public ErrorModel(ILogger logger) 19 | { 20 | _logger = logger; 21 | } 22 | 23 | public void OnGet() 24 | { 25 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/Pages/MyInvoices.razor: -------------------------------------------------------------------------------- 1 | @page "/my-invoices" 2 | @using SmartAccounting.WebPortal.Application.Model; 3 | @inject SmartAccounting.WebPortal.Application.Infrastructure.IProcessedDocumentApiService ProcessedDocumentApiService; 4 | @inject AuthenticationStateProvider AuthenticationStateProvider 5 | @inject IJSRuntime JSRuntime; 6 | 7 |

My invoices

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | @foreach (var invoice in _invoices) 23 | { 24 | 25 | 26 | 27 | 28 | 29 | 30 | 36 | 37 | } 38 | 39 |
Invoice IDInvoice dateCustomer nameVendor nameInvoice total
@invoice.InvoiceId@invoice.InvoiceDate@invoice.CustomerName@invoice.VendorName@invoice.InvoiceTotal 31 | 35 |
40 |
41 | 42 |

43 | Please authenticate first. 44 |

45 |
46 |
47 | 48 | 49 | @code { 50 | private IReadOnlyCollection _invoices = new List(); 51 | 52 | private async Task LoadInvoicesAsync() 53 | { 54 | var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); 55 | var user = authState.User; 56 | if (user.Identity.IsAuthenticated) 57 | { 58 | _invoices = await ProcessedDocumentApiService.GetAllInvoicesAsync(); 59 | } 60 | } 61 | 62 | private async Task ShowInvoiceFileAsync(Invoice invoice) 63 | { 64 | await JSRuntime.InvokeAsync("open", invoice.InvoiceFileUrl, "_blank"); 65 | } 66 | 67 | protected override async Task OnInitializedAsync() 68 | { 69 | await LoadInvoicesAsync(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/Pages/Upload.razor: -------------------------------------------------------------------------------- 1 | @page "/upload" 2 | 3 | @using SmartAccounting.WebPortal.Application.Model; 4 | @inject SmartAccounting.WebPortal.Application.Infrastructure.IFileProcessorApiService FileProcessorApiService; 5 | @inject AuthenticationStateProvider AuthenticationStateProvider; 6 | 7 |

Upload new invoice file

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

File name: @invoiceUpload.AttachmentFileName

18 |
19 |
20 | 21 |
22 | @if (showUploadConfirmation) 23 | { 24 |

Invoice file successfully uploaded!

25 | } 26 | 27 | @if (showFailure) 28 | { 29 |

There was an error during file upload. Please try again.

30 | } 31 | 32 | @if (showProcessingConfirmation) 33 | { 34 |

File was successfully processed. Open "my invoices" tab to see invoice details.

35 | } 36 | 37 |
38 |
39 | 40 |

41 | Please authenticate first. 42 |

43 |
44 |
45 | 46 | @code { 47 | private bool showUploadConfirmation = false; 48 | private bool showFailure = false; 49 | private bool showProcessingConfirmation = false; 50 | private InvoiceUpload invoiceUpload = new InvoiceUpload(); 51 | 52 | private async Task HandleUploadAsync() 53 | { 54 | var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); 55 | var user = authState.User; 56 | if (user.Identity.IsAuthenticated) 57 | { 58 | var response = await FileProcessorApiService.UploadInvoiceAsync(invoiceUpload.AttachmentFileName, invoiceUpload); 59 | if (response.IsSuccessStatusCode) 60 | { 61 | showUploadConfirmation = true; 62 | } 63 | 64 | else 65 | { 66 | showFailure = true; 67 | } 68 | } 69 | } 70 | 71 | private void HandleFileSelected(InputFileChangeEventArgs files) 72 | { 73 | invoiceUpload.Attachment = files.GetMultipleFiles().First().OpenReadStream(); 74 | invoiceUpload.AttachmentFileName = files.GetMultipleFiles().First().Name; 75 | } 76 | } -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/Pages/_Host.cshtml: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @namespace SmartAccounting.WebPortal.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | @{ 5 | Layout = null; 6 | } 7 | 8 | 9 | 10 | 11 | 12 | 13 | SmartAccounting.WebPortal 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | An error has occurred. This application may no longer respond until reloaded. 25 | 26 | 27 | An unhandled exception has occurred. See browser dev tools for details. 28 | 29 | Reload 30 | 🗙 31 |
32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace SmartAccounting.WebPortal 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => 16 | { 17 | webBuilder.UseStartup(); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/Shared/LoginDisplay.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.Identity.Web 2 | @using Microsoft.Extensions.Options 3 | @inject IOptionsMonitor microsoftIdentityOptions 4 | 5 | 6 | 7 | @if (canEditProfile) 8 | { 9 | Hello, @context.User.Identity.Name 10 | } 11 | else 12 | { 13 | Hello, @context.User.Identity.Name 14 | } 15 | Log out 16 | 17 | 18 | Log in 19 | 20 | 21 | 22 | @code { 23 | private bool canEditProfile; 24 | 25 | protected override void OnInitialized() 26 | { 27 | var options = microsoftIdentityOptions.CurrentValue; 28 | canEditProfile = !string.IsNullOrEmpty(options.EditProfilePolicyId); 29 | } 30 | } -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 | 7 | 8 |
9 |
10 | 11 |
12 | 13 |
14 | @Body 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/Shared/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(75, 105, 130) 0%, #70C6C7 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #70C6C7; 17 | border-bottom: 1px solid #70C6C7; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | } 28 | 29 | .top-row a:first-child { 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | } 33 | 34 | @media (max-width: 640.98px) { 35 | .top-row:not(.auth) { 36 | display: none; 37 | } 38 | 39 | .top-row.auth { 40 | justify-content: space-between; 41 | } 42 | 43 | .top-row a, .top-row .btn-link { 44 | margin-left: 0; 45 | } 46 | } 47 | 48 | @media (min-width: 641px) { 49 | .page { 50 | flex-direction: row; 51 | } 52 | 53 | .sidebar { 54 | width: 250px; 55 | height: 100vh; 56 | position: sticky; 57 | top: 0; 58 | } 59 | 60 | .top-row { 61 | position: sticky; 62 | top: 0; 63 | z-index: 1; 64 | } 65 | 66 | .main > div { 67 | padding-left: 2rem !important; 68 | padding-right: 1.5rem !important; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 |  7 | 8 |
9 | 26 |
27 | 28 | @code { 29 | private bool collapseNavMenu = true; 30 | 31 | private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; 32 | 33 | private void ToggleNavMenu() 34 | { 35 | collapseNavMenu = !collapseNavMenu; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/Shared/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | background-color: rgba(255, 255, 255, 0.1); 3 | } 4 | 5 | .top-row { 6 | height: 3.5rem; 7 | background-color: rgba(0,0,0,0.4); 8 | } 9 | 10 | .navbar-brand { 11 | font-size: 1.1rem; 12 | } 13 | 14 | .oi { 15 | width: 2rem; 16 | font-size: 1.1rem; 17 | vertical-align: text-top; 18 | top: -2px; 19 | } 20 | 21 | .nav-item { 22 | font-size: 0.9rem; 23 | padding-bottom: 0.5rem; 24 | } 25 | 26 | .nav-item:first-of-type { 27 | padding-top: 1rem; 28 | } 29 | 30 | .nav-item:last-of-type { 31 | padding-bottom: 1rem; 32 | } 33 | 34 | .nav-item ::deep a { 35 | color: #d7d7d7; 36 | border-radius: 4px; 37 | height: 3rem; 38 | display: flex; 39 | align-items: center; 40 | line-height: 3rem; 41 | } 42 | 43 | .nav-item ::deep a.active { 44 | background-color: rgba(255,255,255,0.25); 45 | color: white; 46 | } 47 | 48 | .nav-item ::deep a:hover { 49 | background-color: rgba(255,255,255,0.1); 50 | color: white; 51 | } 52 | 53 | @media (min-width: 641px) { 54 | .navbar-toggler { 55 | display: none; 56 | } 57 | 58 | .collapse { 59 | /* Never collapse the sidebar for wide screens */ 60 | display: block; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/Shared/SurveyPrompt.razor: -------------------------------------------------------------------------------- 1 |  11 | 12 | @code { 13 | // Demonstrates how a parent component can supply parameters 14 | [Parameter] 15 | public string Title { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/SmartAccounting.WebPortal.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | aspnet-SmartAccounting.WebPortal-14F71D0F-31E8-4C14-84B4-8E6AA4F65477 6 | 0 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Authorization 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using SmartAccounting.WebPortal 10 | @using SmartAccounting.WebPortal.Shared 11 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | 11 | "AzureAdB2C": { 12 | "Instance": "https://xxx.b2clogin.com/", 13 | "ClientId": "", 14 | "ClientSecret": "", 15 | "CallbackPath": "/signin-oidc", 16 | "Domain": "xxx.onmicrosoft.com", 17 | "SignUpSignInPolicyId": "B2C_1A_SIGNINSIGNUP", 18 | "ResetPasswordPolicyId": "B2C_1A_PASSWORDRESET", 19 | "EditProfilePolicyId": "B2C_1A_PROFILEEDIT" 20 | }, 21 | 22 | "SmartAccountingApis": { 23 | "FileProcessorApiUrl": "https://apim-smart-accounting.azure-api.net/file-processor-api", 24 | "FileProcessorApiScope": "https://xxx.onmicrosoft.com/75545ea9-37b7-4ed6-a193-73dd9801482e/User.Access", 25 | "ProcessedDocumentApiUrl": "https://apim-smart-accounting.azure-api.net/processed-document-api", 26 | "ProcessedDocumentApiScope": "https://xxx.onmicrosoft.com/df7b7854-d322-487a-a78a-b0fde3fb2726/User.Access", 27 | "NotificationHubUrl": "https://smart-accounting.westeurope.cloudapp.azure.com/notification-api/direct-notification", 28 | "NotificationApiScope": "https://xxx.onmicrosoft.com/fc1ad7a6-a68b-4f59-b407-5c9b2e5da5dc/User.Access", 29 | "SubscriptionKey": "" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/src/smart-accounting-web-app/src/SmartAccounting.WebPortal/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/src/smart-accounting-web-app/src/SmartAccounting.WebPortal/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/src/smart-accounting-web-app/src/SmartAccounting.WebPortal/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/src/smart-accounting-web-app/src/SmartAccounting.WebPortal/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | a, .btn-link { 8 | color: #4B6982; 9 | } 10 | 11 | .btn-primary { 12 | color: #fff; 13 | background-color: #1b6ec2; 14 | border-color: #1861ac; 15 | } 16 | 17 | .content { 18 | padding-top: 1.1rem; 19 | } 20 | 21 | .valid.modified:not([type=checkbox]) { 22 | outline: 1px solid #26b050; 23 | } 24 | 25 | .invalid { 26 | outline: 1px solid red; 27 | } 28 | 29 | .validation-message { 30 | color: red; 31 | } 32 | 33 | #blazor-error-ui { 34 | background: lightyellow; 35 | bottom: 0; 36 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 37 | display: none; 38 | left: 0; 39 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 40 | position: fixed; 41 | width: 100%; 42 | z-index: 1000; 43 | } 44 | 45 | #blazor-error-ui .dismiss { 46 | cursor: pointer; 47 | position: absolute; 48 | right: 0.75rem; 49 | top: 0.5rem; 50 | } 51 | -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/src/smart-accounting-web-app/src/SmartAccounting.WebPortal/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.WebPortal/wwwroot/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Daniel-Krzyczkowski/Smart-Accounting/1eea32f2c488458c2ab5aed4058ec8e5b7e2c645/src/smart-accounting-web-app/src/SmartAccounting.WebPortal/wwwroot/images/logo.png -------------------------------------------------------------------------------- /src/smart-accounting-web-app/src/SmartAccounting.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31025.194 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmartAccounting.WebPortal", "SmartAccounting.WebPortal\SmartAccounting.WebPortal.csproj", "{AFF655EF-BD59-4FF3-ADAC-6944F5FC7107}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {AFF655EF-BD59-4FF3-ADAC-6944F5FC7107}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {AFF655EF-BD59-4FF3-ADAC-6944F5FC7107}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {AFF655EF-BD59-4FF3-ADAC-6944F5FC7107}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {AFF655EF-BD59-4FF3-ADAC-6944F5FC7107}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {56A95663-7789-4361-8CD4-01D069BF4584} 24 | EndGlobalSection 25 | EndGlobal 26 | --------------------------------------------------------------------------------