[
  {
    "path": ".aspire/settings.json",
    "content": "{\n  \"appHostPath\": \"../src/eShop.AppHost/eShop.AppHost.csproj\"\n}"
  },
  {
    "path": ".config/CredScanSuppressions.json",
    "content": "{\n  \"tool\": \"Credential Scanner\",\n  \"suppressions\": [\n    {\n      \"placeholder\": \"Pass123$\",\n      \"_justification\": \"Dummy.\"\n    },\n    {\n      \"placeholder\": \"yourWeak(!)Password\",\n      \"_justification\": \"Dummy.\"\n    }\n  ]\n}\n"
  },
  {
    "path": ".config/configuration.vs.winget",
    "content": "# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/0.2\nproperties:\n  configurationVersion: 0.2.0\n  ########################################\n  ### RESOURCES: System Configuration\n  ########################################\n  resources:\n    ########################################\n    ### OS Configurations\n    ########################################\n    ### Enable: Developer Mode\n    ### -------------------------------------\n    - resource: Microsoft.Windows.Developer/DeveloperMode\n      directives:\n        description: Enable Developer Mode\n        # Requires elevation only for the set operation\n        securityContext: elevated\n        allowPrerelease: true\n      settings:\n        Ensure: Present\n    ### Install Windows VirtualMachinePlatform\n    ### -------------------------------------\n    - resource: PSDscResources/WindowsOptionalFeature\n      directives:\n        description: Install VirtualMachinePlatform\n        securityContext: elevated\n      settings:\n        name: VirtualMachinePlatform\n        ensure: Present\n    ### Install WSL\n    ### -------------------------------------\n    - resource: PSDscResources/WindowsOptionalFeature\n      directives:\n        description: Install WSL\n        securityContext: elevated\n      settings:\n        name: Microsoft-Windows-Subsystem-Linux\n        ensure: Present\n    ### Configure Install Ubuntu\n    ### -------------------------------------\n    - resource: PSDscResources/Script\n      id: ubuntuwsl\n      directives:\n        description: Install Ubuntu for WSL\n      settings:\n        SetScript: |\n          $env:Path = [System.Environment]::GetEnvironmentVariable(\"Path\",\"Machine\") + \";\" + [System.Environment]::GetEnvironmentVariable(\"Path\",\"User\")\n          wsl --install -d Ubuntu\n        GetScript: return $false\n        TestScript: return $false\n    ########################################\n    ### Install CLIs, SDKs & Tools\n    ########################################\n    ### Install DotNET SDK Preview\n    ### -------------------------------------\n    - resource: Microsoft.WinGet.DSC/WinGetPackage\n      id: dotnetsdk\n      directives:\n        description: Install DotNET SDK Preview\n        # Requires elevation only for the set operation (i.e., installing the package)\n        securityContext: elevated\n      settings:\n        id: Microsoft.DotNet.SDK.Preview\n    ### Install Azure CLI\n    ### -------------------------------------\n    - resource: Microsoft.WinGet.DSC/WinGetPackage\n      id: azurecli\n      directives:\n        description: Install Azure CLI\n        # Requires elevation only for the set operation (i.e., installing the package)\n        securityContext: elevated\n      settings:\n        id:  Microsoft.AzureCLI\n    ### Install Azd\n    ### -------------------------------------\n    - resource: Microsoft.WinGet.DSC/WinGetPackage\n      id: Azd\n      directives:\n        description: Install Azd\n      settings:\n        id: Microsoft.Azd\n    ### Install Docker Desktop\n    ### -------------------------------------\n    - resource: Microsoft.WinGet.DSC/WinGetPackage\n      id: docker\n      directives:\n        description: Install Docker Desktop\n        # Requires elevation only for the set operation (i.e., installing the package)\n        securityContext: elevated\n      settings:\n        id: Docker.DockerDesktop\n    ### Install Visual Sudio\n    ### -------------------------------------\n    - resource: Microsoft.WinGet.DSC/WinGetPackage\n      id: vscommunity\n      directives:\n        description: Install Visual Studio 2022 Community\n        # Requires elevation only for the set operation (i.e., installing the package)\n        # Only requires elevation when the VS Installer isn't installed yet\n        securityContext: elevated\n      settings:\n        id: Microsoft.VisualStudio.2022.Community.Preview\n    ### Install VS Workloads\n    ### -------------------------------------\n    - resource: Microsoft.VisualStudio.DSC/VSComponents\n      directives:\n        description: Install required VS workloads from vsconfig file\n        securityContext: elevated\n        allowPrerelease: true\n      dependsOn:\n        - vscommunity\n      settings:\n        productId: Microsoft.VisualStudio.Product.Community\n        channelId: VisualStudio.17.Preview\n        components: [Microsoft.VisualStudio.Workload.NetWeb, Microsoft.VisualStudio.Workload.NetCrossPlat, aspire]\n        includeRecommended: true\n"
  },
  {
    "path": ".config/configuration.vsCode.winget",
    "content": "# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/0.2\nproperties:\n  configurationVersion: 0.2.0\n  ########################################\n  ### RESOURCES: System Configuration\n  ########################################\n  resources:\n    ########################################\n    ### OS Configurations\n    ########################################\n    ### Enable: Developer Mode\n    ### -------------------------------------\n    - resource: Microsoft.Windows.Developer/DeveloperMode\n      directives:\n        description: Enable Developer Mode\n        # Requires elevation only for the set operation\n        securityContext: elevated\n        allowPrerelease: true\n      settings:\n        Ensure: Present\n    ### Install Windows VirtualMachinePlatform\n    ### -------------------------------------\n    - resource: PSDscResources/WindowsOptionalFeature\n      directives:\n        description: Install VirtualMachinePlatform\n        securityContext: elevated\n      settings:\n        name: VirtualMachinePlatform\n        ensure: Present\n    ### Install WSL\n    ### -------------------------------------\n    - resource: PSDscResources/WindowsOptionalFeature\n      directives:\n        description: Install WSL\n        securityContext: elevated\n      settings:\n        name: Microsoft-Windows-Subsystem-Linux\n        ensure: Present\n    ### Configure Install Ubuntu\n    ### -------------------------------------\n    - resource: PSDscResources/Script\n      id: ubuntuwsl\n      directives:\n        description: Install Ubuntu for WSL\n      settings:\n        SetScript: |\n          $env:Path = [System.Environment]::GetEnvironmentVariable(\"Path\",\"Machine\") + \";\" + [System.Environment]::GetEnvironmentVariable(\"Path\",\"User\")\n          wsl --install -d Ubuntu\n        GetScript: return $false\n        TestScript: return $false\n    ########################################\n    ### Install CLIs, SDKs & Tools\n    ########################################\n    ### Install DotNET SDK Preview\n    ### -------------------------------------\n    - resource: Microsoft.WinGet.DSC/WinGetPackage\n      id: dotnetsdk\n      directives:\n        description: Install DotNET SDK Preview\n        # Requires elevation only for the set operation (i.e., installing the package)\n        securityContext: elevated\n      settings:\n        id: Microsoft.DotNet.SDK.Preview\n    ### Install Azure CLI\n    ### -------------------------------------\n    - resource: Microsoft.WinGet.DSC/WinGetPackage\n      id: azurecli\n      directives:\n        description: Install Azure CLI\n        # Requires elevation only for the set operation (i.e., installing the package)\n        securityContext: elevated\n      settings:\n        id:  Microsoft.AzureCLI\n    ### Install Azd\n    ### -------------------------------------\n    - resource: Microsoft.WinGet.DSC/WinGetPackage\n      id: Azd\n      directives:\n        description: Install Azd\n      settings:\n        id: Microsoft.Azd\n    ### Install Docker Desktop\n    ### -------------------------------------\n    - resource: Microsoft.WinGet.DSC/WinGetPackage\n      id: docker\n      directives:\n        description: Install Docker Desktop\n        # Requires elevation only for the set operation (i.e., installing the package)\n        securityContext: elevated\n      settings:\n        id: Docker.DockerDesktop\n    ### Install Microsoft Visual Studio Code\n    ### -------------------------------------\n    - resource: Microsoft.WinGet.DSC/WinGetPackage\n      id: vscode\n      directives:\n        description: Install Microsoft Visual Studio Code\n      settings:\n        id: Microsoft.VisualStudioCode\n        ensure: Present\n    ########################################\n    ### Install VSCode Extensions\n    ########################################\n    ### Install VSCode Azure Developer CLI Extension\n    ### -------------------------------------\n    - resource: Microsoft.VSCode.Dsc/VSCodeExtension\n      id: azure-dev-cli-extension\n      dependsOn:\n        - vscode\n        - docker\n      directives:\n        description: Install Azure Developer CLI Extension\n        allowPrerelease: true\n      settings:\n        name: ms-azuretools.azure-dev\n        exist: true\n    ### Install VSCode WSL Extension\n    ### -------------------------------------\n    - resource: Microsoft.VSCode.Dsc/VSCodeExtension\n      id: wsl-extension\n      dependsOn:\n        - vscode\n        - docker\n      directives:\n        description: Install WSL extension\n        allowPrerelease: true\n      settings:\n        name: ms-vscode-remote.remote-wsl\n        exist: true\n    ### Install VSCode Dev Containers Extension\n    ### -------------------------------------\n    - resource: Microsoft.VSCode.Dsc/VSCodeExtension\n      id: devcontainers-extension\n      dependsOn:\n        - vscode\n        - docker\n      directives:\n        description: Install Dev Containers extension\n        allowPrerelease: true\n      settings:\n        name: ms-vscode-remote.remote-containers\n        exist: true\n    ### Install VSCode Docker Extension\n    ### -------------------------------------\n    - resource: Microsoft.VSCode.Dsc/VSCodeExtension\n      id: docker-extension\n      dependsOn:\n        - vscode\n        - docker\n      directives:\n        description: Install Docker extension\n        allowPrerelease: true\n      settings:\n        name: ms-azuretools.vscode-docker\n        exist: true\n    ### Install VSCode C# DevKit Extension\n    ### -------------------------------------\n    - resource: Microsoft.VSCode.Dsc/VSCodeExtension\n      id: c#-devkit-extension\n      dependsOn:\n        - vscode\n        - docker\n      directives:\n        description: Install C# DevKit extension\n        allowPrerelease: true\n      settings:\n        name: ms-dotnettools.csdevkit\n        exist: true\n    ### Install .NET MAUI Extension\n    ### -------------------------------------\n    - resource: Microsoft.VSCode.Dsc/VSCodeExtension\n      id: dotnet-maui-extension\n      dependsOn:\n        - vscode\n        - docker\n      directives:\n        description: Install .NET MAUI extension\n        allowPrerelease: true\n      settings:\n        name: ms-dotnettools.dotnet-maui\n        exist: true\n"
  },
  {
    "path": ".config/tsaoptions.json",
    "content": "{\n    \"areaPath\": \"DevDiv\\\\ASP.NET Core\\\\Policy Violations\",\n    \"codebaseName\": \"eShop\",\n    \"instanceUrl\": \"https://devdiv.visualstudio.com/\",\n    \"iterationPath\": \"DevDiv\",\n    \"notificationAliases\": [\n      \"aspnetcore-build@microsoft.com\"\n    ],\n    \"projectName\": \"DEVDIV\",\n    \"repositoryName\": \"eShop\",\n    \"template\": \"TFSDEVDIV\"\n  }"
  },
  {
    "path": ".devcenter/catalog/definitions/imagedefinition.yaml",
    "content": "Initial Commit\n"
  },
  {
    "path": ".editorconfig",
    "content": "###############################\n# Core EditorConfig Options   #\n###############################\nroot = true\n# All files\n[*]\nindent_style = space\n\n# XML project files\n[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]\nindent_size = 2\n\n# XML config files\n[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]\nindent_size = 2\n\n# Code files\n[*.{cs,csx,vb,vbx}]\nindent_size = 4\ninsert_final_newline = true\ncharset = utf-8-bom\n###############################\n# .NET Coding Conventions     #\n###############################\n[*.{cs,vb}]\n# Organize usings\ndotnet_sort_system_directives_first = true\n# this. preferences\ndotnet_style_qualification_for_field = false:silent\ndotnet_style_qualification_for_property = false:silent\ndotnet_style_qualification_for_method = false:silent\ndotnet_style_qualification_for_event = false:silent\n# Language keywords vs BCL types preferences\ndotnet_style_predefined_type_for_locals_parameters_members = true:silent\ndotnet_style_predefined_type_for_member_access = true:silent\n# Parentheses preferences\ndotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent\ndotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent\ndotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent\ndotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent\n# Modifier preferences\ndotnet_style_require_accessibility_modifiers = for_non_interface_members:silent\ndotnet_style_readonly_field = true:suggestion\n# Expression-level preferences\ndotnet_style_object_initializer = true:suggestion\ndotnet_style_collection_initializer = true:suggestion\ndotnet_style_explicit_tuple_names = true:suggestion\ndotnet_style_null_propagation = true:suggestion\ndotnet_style_coalesce_expression = true:suggestion\ndotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent\ndotnet_style_prefer_inferred_tuple_names = true:suggestion\ndotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion\ndotnet_style_prefer_auto_properties = true:silent\ndotnet_style_prefer_conditional_expression_over_assignment = true:silent\ndotnet_style_prefer_conditional_expression_over_return = true:silent\n###############################\n# Naming Conventions          #\n###############################\n# Style Definitions\ndotnet_naming_style.pascal_case_style.capitalization             = pascal_case\n# Use PascalCase for constant fields\ndotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion\ndotnet_naming_rule.constant_fields_should_be_pascal_case.symbols  = constant_fields\ndotnet_naming_rule.constant_fields_should_be_pascal_case.style    = pascal_case_style\ndotnet_naming_symbols.constant_fields.applicable_kinds            = field\ndotnet_naming_symbols.constant_fields.applicable_accessibilities  = *\ndotnet_naming_symbols.constant_fields.required_modifiers          = const\n###############################\n# C# Coding Conventions       #\n###############################\n[*.cs]\n# var preferences\ncsharp_style_var_for_built_in_types = true:silent\ncsharp_style_var_when_type_is_apparent = true:silent\ncsharp_style_var_elsewhere = true:silent\n# Expression-bodied members\ncsharp_style_expression_bodied_methods = false:silent\ncsharp_style_expression_bodied_constructors = false:silent\ncsharp_style_expression_bodied_operators = false:silent\ncsharp_style_expression_bodied_properties = true:silent\ncsharp_style_expression_bodied_indexers = true:silent\ncsharp_style_expression_bodied_accessors = true:silent\n# Pattern matching preferences\ncsharp_style_pattern_matching_over_is_with_cast_check = true:suggestion\ncsharp_style_pattern_matching_over_as_with_null_check = true:suggestion\n# Null-checking preferences\ncsharp_style_throw_expression = true:suggestion\ncsharp_style_conditional_delegate_call = true:suggestion\n# Modifier preferences\ncsharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion\n# Expression-level preferences\ncsharp_prefer_braces = true:silent\ncsharp_style_deconstructed_variable_declaration = true:suggestion\ncsharp_prefer_simple_default_expression = true:suggestion\ncsharp_style_prefer_local_over_anonymous_function = true:suggestion\ncsharp_style_inlined_variable_declaration = true:suggestion\n###############################\n# C# Formatting Rules         #\n###############################\n# New line preferences\ncsharp_new_line_before_open_brace = all\ncsharp_new_line_before_else = true\ncsharp_new_line_before_catch = true\ncsharp_new_line_before_finally = true\ncsharp_new_line_before_members_in_object_initializers = true\ncsharp_new_line_before_members_in_anonymous_types = true\ncsharp_new_line_between_query_expression_clauses = true\n# Indentation preferences\ncsharp_indent_case_contents = true\ncsharp_indent_switch_labels = true\ncsharp_indent_labels = flush_left\n# Space preferences\ncsharp_space_after_cast = false\ncsharp_space_after_keywords_in_control_flow_statements = true\ncsharp_space_between_method_call_parameter_list_parentheses = false\ncsharp_space_between_method_declaration_parameter_list_parentheses = false\ncsharp_space_between_parentheses = false\ncsharp_space_before_colon_in_inheritance_clause = true\ncsharp_space_after_colon_in_inheritance_clause = true\ncsharp_space_around_binary_operators = before_and_after\ncsharp_space_between_method_declaration_empty_parameter_list_parentheses = false\ncsharp_space_between_method_call_name_and_opening_parenthesis = false\ncsharp_space_between_method_call_empty_parameter_list_parentheses = false\n# Wrapping preferences\ncsharp_preserve_single_line_statements = true\ncsharp_preserve_single_line_blocks = true\n###############################\n# VB Coding Conventions       #\n###############################\n[*.vb]\n# Modifier preferences\nvisual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion"
  },
  {
    "path": ".gitattributes",
    "content": "###############################################################################\n# Set default behavior to automatically normalize line endings.\n###############################################################################\n* text=auto\n*.sh text eol=lf\n\n###############################################################################\n# Set default behavior for command prompt diff.\n#\n# This is need for earlier builds of msysgit that does not have it on by\n# default for csharp files.\n# Note: This is only used by command line\n###############################################################################\n#*.cs     diff=csharp\n\n###############################################################################\n# Set the merge driver for project and solution files\n#\n# Merging from the command prompt will add diff markers to the files if there\n# are conflicts (Merging from VS is not affected by the settings below, in VS\n# the diff markers are never inserted). Diff markers may cause the following \n# file extensions to fail to load in VS. An alternative would be to treat\n# these files as binary and thus will always conflict and require user\n# intervention with every merge. To do so, just uncomment the entries below\n###############################################################################\n#*.sln       merge=binary\n#*.csproj    merge=binary\n#*.vbproj    merge=binary\n#*.vcxproj   merge=binary\n#*.vcproj    merge=binary\n#*.dbproj    merge=binary\n#*.fsproj    merge=binary\n#*.lsproj    merge=binary\n#*.wixproj   merge=binary\n#*.modelproj merge=binary\n#*.sqlproj   merge=binary\n#*.wwaproj   merge=binary\n\n###############################################################################\n# behavior for image files\n#\n# image files are treated as binary by default.\n###############################################################################\n#*.jpg   binary\n#*.png   binary\n#*.gif   binary\n\n###############################################################################\n# diff behavior for common document formats\n# \n# Convert binary document formats to text before diffing them. This feature\n# is only available from the command line. Turn it on by uncommenting the \n# entries below.\n###############################################################################\n#*.doc   diff=astextplain\n#*.DOC   diff=astextplain\n#*.docx  diff=astextplain\n#*.DOCX  diff=astextplain\n#*.dot   diff=astextplain\n#*.DOT   diff=astextplain\n#*.pdf   diff=astextplain\n#*.PDF   diff=astextplain\n#*.rtf   diff=astextplain\n#*.RTF   diff=astextplain\n\n\n###############################################################################\n# Certificates are binary\n###############################################################################\n*.pfx binary\n\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nregistries:\n  public-nuget:\n    type: nuget-feed\n    url: https://api.nuget.org/v3/index.json\nupdates:\n  - package-ecosystem: nuget\n    directory: \"/\"\n    registries:\n      - public-nuget\n    schedule:\n      interval: weekly\n    open-pull-requests-limit: 15\n    groups:\n      Aspire:\n        patterns:\n          - \"Aspire.*\"\n          - \"Microsoft.Extensions.ServiceDiscovery.*\"\n      Azure:\n        patterns:\n          - \"Azure.*\"\n          - \"Microsoft.Azure.*\"\n          - \"Microsoft.Extensions.Azure\"\n      AspNetCoreHealthChecks:\n        patterns:\n          - \"AspNetCore.HealthChecks.*\"\n      AspNetCore:\n        patterns:\n          - \"Microsoft.AspNetCore.*\"\n          - \"Microsoft.Extensions.Features\"\n      MicrosoftExtensions:\n        patterns:\n          - \"Microsoft.Extensions.*\"\n          - \"Microsoft.Bcl.*\"\n      EntityFrameworkCore:\n        patterns:\n          - \"Microsoft.EntityFrameworkCore.*\"\n      FluentUi:\n        patterns:\n          - \"Microsoft.FluentUI.*\"\n      OpenTelemetry:\n        patterns:\n          - \"OpenTelemetry.*\"\n      Npgsql:\n        patterns:\n          - \"Npgsql.*\"\n      MicrosoftDotNet:\n        patterns:\n          - \"Microsoft.DotNet.*\"\n      Grpc:\n        patterns:\n          - \"Grpc.*\"\n      Pgvector:\n        patterns:\n          - \"Pgvector.*\"\n      Duende:\n        patterns:\n          - \"Duende.*\"\n      MSTest:\n        patterns:\n          - \"MSTest.*\"\n\n"
  },
  {
    "path": ".github/workflows/markdownlint-problem-matcher.json",
    "content": "{\n    \"problemMatcher\": [\n        {\n            \"owner\": \"markdownlint\",\n            \"pattern\": [\n                {\n                    \"regexp\": \"^([^:]*):(\\\\d+):?(\\\\d+)?\\\\s([\\\\w-\\\\/]*)\\\\s(.*)$\",\n                    \"file\": 1,\n                    \"line\": 2,\n                    \"column\": 3,\n                    \"code\": 4,\n                    \"message\": 5\n                }\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": ".github/workflows/markdownlint.yml",
    "content": "name: Markdownlint\n\npermissions:\n  contents: read\n\n# run even on changes without markdown changes, so that we can\n# make it in GitHub a required check for PR's\non:\n  pull_request:\n\njobs:\n  lint:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n    - name: Use Node.js\n      uses: actions/setup-node@v4\n      with:\n        node-version: 16.x\n    - name: Run Markdownlint\n      run: |\n        echo \"::add-matcher::.github/workflows/markdownlint-problem-matcher.json\"\n        npm i -g markdownlint-cli\n        markdownlint --ignore '.dotnet/' '**/*.md'\n"
  },
  {
    "path": ".github/workflows/playwright.yml",
    "content": "name: Playwright Tests for eShop\non:\n  push:\n    branches: [ main ]\n    paths-ignore:\n      - '**.md'\n      - 'src/ClientApp/**'\n      - 'tests/ClientApp.UnitTests/**'\n      - '.github/workflows/pr-validation-maui.yml'\n  pull_request:\n    branches: [ main ]\n    paths-ignore:\n      - '**.md'\n      - 'src/ClientApp/**'\n      - 'test/ClientApp.UnitTests/**'\n      - '.github/workflows/pr-validation-maui.yml'\njobs:\n  test:\n    timeout-minutes: 60\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v4\n    - uses: actions/setup-dotnet@v4\n      with:\n        dotnet-version: '10.0.x'\n        dotnet-quality: 'preview'\n    - uses: actions/setup-node@v4\n      with:\n        node-version: lts/*\n    - name: Install dependencies\n      run: npm ci\n    - name: Install .NET HTTPS Development Certificate\n      # if: matrix.os == 'ubuntu-latest'\n      run: |\n        dotnet dev-certs https --clean\n        dotnet dev-certs https --trust\n    - name: Install Playwright Browsers\n      run: npx playwright install chromium\n    - name: Run Playwright tests\n      run: npx playwright test\n      env:\n        ESHOP_USE_HTTP_ENDPOINTS: 1\n        USERNAME1: bob\n        PASSWORD: Pass123$\n    - uses: actions/upload-artifact@v4\n      if: always()\n      with:\n        name: playwright-report\n        path: playwright-report/\n        retention-days: 30\n"
  },
  {
    "path": ".github/workflows/pr-validation-maui.yml",
    "content": "name: eShop Pull Request Validation - .NET MAUI\r\n\r\non:\r\n  pull_request:\r\n    branches:\r\n      - '**'\r\n    paths:\r\n      - 'src/ClientApp/**'\r\n      - 'tests/ClientApp.UnitTests/**'\r\n      - '.github/workflows/pr-validation-maui.yml'\r\n  push:\r\n    branches:\r\n      - main\r\n    paths:\r\n      - 'src/ClientApp/**'\r\n      - 'tests/ClientApp.UnitTests/**'\r\n      - '.github/workflows/pr-validation-maui.yml'\r\n\r\njobs:  \r\n  test:\r\n    runs-on: windows-latest    \r\n    steps:\r\n      - uses: actions/checkout@v4\r\n      - name: Setup .NET (global.json)\r\n        uses: actions/setup-dotnet@v3\r\n\r\n      - name: Update Workloads\r\n        run: dotnet workload update\r\n\r\n      - name: Install Workloads\r\n        shell: pwsh\r\n        run: |\r\n          dotnet workload install android\r\n          dotnet workload install ios\r\n          dotnet workload install maccatalyst\r\n          dotnet workload install maui\r\n\r\n      - name: Build \r\n        run: dotnet build src/ClientApp/ClientApp.csproj\r\n        \r\n      - name: Test\r\n        run: dotnet test --project tests/ClientApp.UnitTests/ClientApp.UnitTests.csproj --no-progress --output detailed"
  },
  {
    "path": ".github/workflows/pr-validation.yml",
    "content": "name: eShop Pull Request Validation\n\non:\n  pull_request:\n    paths-ignore:\n      - '**.md'\n      - 'src/ClientApp/**'\n      - 'tests/ClientApp.UnitTests/**'\n      - '.github/workflows/pr-validation-maui.yml'\n  push:\n    branches:\n      - main\n    paths-ignore:\n      - '**.md'\n      - 'src/ClientApp/**'\n      - 'tests/ClientApp.UnitTests/**'\n      - '.github/workflows/pr-validation-maui.yml'\n\njobs:  \n  test:\n    runs-on: ubuntu-latest    \n    steps:\n      - uses: actions/checkout@v4\n      - name: Setup .NET (global.json)\n        uses: actions/setup-dotnet@v3\n      - name: Build \n        run: dotnet build eShop.Web.slnf\n      - name: Test\n        run: dotnet test --solution eShop.Web.slnf --no-build --no-progress --output detailed"
  },
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## Get latest from `dotnet new gitignore`\n\n# dotenv files\n.env\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Mono auto generated files\nmono_crash.*\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Ww][Ii][Nn]32/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# Visual Studio 2017 auto generated files\nGenerated\\ Files/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUnit\n*.VisualState.xml\nTestResult.xml\nnunit-*.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# Benchmark Results\nBenchmarkDotNet.Artifacts/\n\n# .NET\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n# Tye\n.tye/\n\n# ASP.NET Scaffolding\nScaffoldingReadMe.txt\n\n# StyleCop\nStyleCopReport.xml\n\n# Files built by Visual Studio\n*_i.c\n*_p.c\n*_h.h\n*.ilk\n*.meta\n*.obj\n*.iobj\n*.pch\n*.pdb\n*.ipdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*_wpftmp.csproj\n*.log\n*.tlog\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# Visual Studio Trace Files\n*.e2e\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# AxoCover is a Code Coverage Tool\n.axoCover/*\n!.axoCover/settings.json\n\n# Coverlet is a free, cross platform Code Coverage Tool\ncoverage*.json\ncoverage*.xml\ncoverage*.info\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# Note: Comment the next line if you want to checkin your web deploy settings,\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# NuGet Symbol Packages\n*.snupkg\n# The packages folder can be ignored because of Package Restore\n**/[Pp]ackages/*\n# except build/, which is used as an MSBuild target.\n!**/[Pp]ackages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/[Pp]ackages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n*.appx\n*.appxbundle\n*.appxupload\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!?*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\norleans.codegen.cs\n\n# Including strong name files can present a security risk\n# (https://github.com/github/gitignore/pull/2483#issue-259490424)\n#*.snk\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\nServiceFabricBackup/\n*.rptproj.bak\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n*.rptproj.rsuser\n*- [Bb]ackup.rdl\n*- [Bb]ackup ([0-9]).rdl\n*- [Bb]ackup ([0-9][0-9]).rdl\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio 6 auto-generated project file (contains which files were open etc.)\n*.vbp\n\n# Visual Studio 6 workspace and project file (working project files containing files to include in project)\n*.dsw\n*.dsp\n\n# Visual Studio 6 technical files\n*.ncb\n*.aps\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# CodeRush personal settings\n.cr/personal\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/**\n# !tools/packages.config\n\n# Tabs Studio\n*.tss\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n\n# OpenCover UI analysis results\nOpenCover/\n\n# Azure Stream Analytics local run output\nASALocalRun/\n\n# MSBuild Binary and Structured Log\n*.binlog\n\n# NVidia Nsight GPU debugger configuration file\n*.nvuser\n\n# MFractors (Xamarin productivity tool) working folder\n.mfractor/\n\n# Local History for Visual Studio\n.localhistory/\n\n# Visual Studio History (VSHistory) files\n.vshistory/\n\n# BeatPulse healthcheck temp database\nhealthchecksdb\n\n# Backup folder for Package Reference Convert tool in Visual Studio 2017\nMigrationBackup/\n\n# Ionide (cross platform F# VS Code tools) working folder\n.ionide/\n\n# Fody - auto-generated XML schema\nFodyWeavers.xsd\n\n# VS Code files for those working on multiple tools\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n*.code-workspace\n\n# Local History for Visual Studio Code\n.history/\n\n# Windows Installer files from build outputs\n*.cab\n*.msi\n*.msix\n*.msm\n*.msp\n\n# JetBrains Rider\n*.sln.iml\n.idea\n\n##\n## Visual studio for Mac\n##\n\n\n# globs\nMakefile.in\n*.userprefs\n*.usertasks\nconfig.make\nconfig.status\naclocal.m4\ninstall-sh\nautom4te.cache/\n*.tar.gz\ntarballs/\ntest-results/\n\n# Mac bundle stuff\n*.dmg\n*.app\n\n# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore\n# General\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\n\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore\n# Windows thumbnail cache files\nThumbs.db\nehthumbs.db\nehthumbs_vista.db\n\n# Dump file\n*.stackdump\n\n# Folder config file\n[Dd]esktop.ini\n\n# Recycle Bin used on file shares\n$RECYCLE.BIN/\n\n# Windows Installer files\n*.cab\n*.msi\n*.msix\n*.msm\n*.msp\n\n# Windows shortcuts\n*.lnk\n\n# Vim temporary swap files\n*.swp\n\n# Playwright\n/test-results/\n/playwright-report/\n/blob-report/\n/playwright/.cache/\n/playwright/.auth/\n/user.json\n.azure\n"
  },
  {
    "path": ".markdownlint.json",
    "content": "{\n    \"default\": true,\n    \"ul-indent\":                       false,\n    \"ul-style\":                        false,\n    \"no-trailing-spaces\":              false,\n    \"line-length\":                     false,\n    \"blanks-around-headings\":          false,\n    \"no-duplicate-heading\":            { \"siblings_only\": true },\n    \"no-trailing-punctuation\":         false,\n    \"blanks-around-fences\":            false,\n    \"blanks-around-lists\":             false,\n    \"no-inline-html\":                  { \"allowed_elements\": [ \"summary\", \"details\", \"kbd\", \"br\" ]},\n    \"no-bare-urls\":                    false,\n    \"single-trailing-newline\":         false,\n    \"emphasis-style\":                  false,\n\n    // rule settings and options are documented in https://github.com/DavidAnson/markdownlint\n    // feel free to disable more low value rules in here; get rule name from the error message.\n    // the purpose of the linter is to catch significant issues like broken links.\n}\n"
  },
  {
    "path": ".markdownlintignore",
    "content": "# This is for editors; keep .github/workflows/markdownlint.yml in sync\n.dotnet/\n"
  },
  {
    "path": ".spectral.yml",
    "content": "extends: spectral:oas\nrules:\n  info-contact: off\n\n  success-response:\n    description: All operations should have a success response.\n    message: Operation is missing a success response.\n    severity: warn\n    given: $.paths.*.*.responses\n    then:\n      function: schema\n      functionOptions:\n        schema:\n          anyOf:\n            - required: [\"200\"]\n            - required: [\"201\"]\n            - required: [\"204\"]\n\n  error-response:\n    description: All operations should have a error response.\n    message: Operation is missing a error response.\n    severity: warn\n    given: $.paths.*.*.responses\n    then:\n      function: schema\n      functionOptions:\n        schema:\n          anyOf:\n            - required: [\"400\"]\n            - required: [\"404\"]\n\n  parameter-description:\n    description: All parameters should have a description\n    message: Parameter is missing a description.\n    severity: warn\n    given: $.paths.*.*.parameters[*]\n    then:\n      field: description\n      function: truthy\n"
  },
  {
    "path": "CODE-OF-CONDUCT.md",
    "content": "# Code of Conduct\n\nThis project has adopted the code of conduct defined by the Contributor Covenant\nto clarify expected behavior in our community.\n\nFor more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct).\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to eShop\n\nThank you for your interest in contributing to eShop! We're excited to collaborate with you and see how together we can improve and evolve this sample application.\n\n## Getting Started\n\nIf this is your first visit, a great way to begin is by tackling issues tagged as `\"help wanted\"` or `\"good first issue\"`. These are specially curated to help you get acquainted with the project and make a meaningful impact early on.\n\n## Spot a Typo?\n\nTypos and other small fixes are important to us -— no contribution is too small! If you spot something small, go ahead and open a Pull Request. For these types of contributions, there's no need to create a separate issue.\n\n## Have a Suggestion?\n\nIf you have a suggestion on how to enhance eShop, please open an issue with the following details:\n- A clear title and description of the suggestion\n- Any relevant examples or mockups\n- Indicate whether you're interested in implementing the feature yourself\n\nWe'll review your suggestion and have a discussion about its potential inclusion in the project.\n\n## Contribution Principles\n\nWhen considering contributions, we're guided by several principles that align with the project's vision:\n\n- **Best Practices**: We want this sample to be canonical and reflect the best practices in the industry and in .NET.\n\n- **Selectivity in Tools and Libraries**: There is a rich ecosystem of tools, projects, and libraries out there, and we cannot use all of them in this sample. We would like this repo to reflect the use of a realistic set of technologies, not to be a showcase or example of every possible thing it could use.\n\n- **Architectural Integrity**: We welcome refactoring and architectural improvements, provided they're justified. Large-scale changes should come with a clear rationale, such as significant enhancements to the application's design or performance.\n\n- **Enhancing Reliability and Scalability**: We welcome contributions that improve the application's reliability and scalability. These could include updates to error handling, redundancy mechanisms, data access, and any other changes that help eShop operate more robustly under load. We'd love to see relevant test scenarios or metrics for these contributions.\n\n- **Performance Enhancements**: If you're looking to speed up eShop, we're all for it! Please include benchmark comparisons to demonstrate the improvements. Performance improvements that make the code less readable or canonical may have more scrutiny applied.\n\n## Code of Conduct\n\nTo ensure a welcoming and positive environment for everyone, please adhere to our Code of Conduct. Respectful collaboration is key to a successful project.\n"
  },
  {
    "path": "Directory.Build.props",
    "content": "<Project>\n\n  <PropertyGroup>\n    <WarnOnPackingNonPackableProject>false</WarnOnPackingNonPackableProject>\n    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n    <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>\n    <DebugType>embedded</DebugType>\n    <DebugSymbols>true</DebugSymbols>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <!-- Temporarily disable security warnings for transitive packages. -->\n    <NoWarn>NU1901;NU1902;NU1903;NU1904</NoWarn>\n    <UseArtifactsOutput>true</UseArtifactsOutput>\n  </PropertyGroup>\n\n  <!-- Enable OpenAI OpenTelemetry so OpenAI calls participate in tracing and metrics. -->\n  <ItemGroup>\n    <RuntimeHostConfigurationOption Include=\"OpenAI.Experimental.EnableOpenTelemetry\"\n                                    Value=\"true\" />\n  </ItemGroup>\n  \n</Project>\n"
  },
  {
    "path": "Directory.Build.targets",
    "content": "<Project>\n\n  <PropertyGroup Condition=\"'$(PublishAot)' == 'true'\">\n    <!-- EventSource is disabled by default for non-ASP.NET AOT'd apps, re-enable it -->\n    <EventSourceSupport>true</EventSourceSupport>\n\n    <!-- Ensure individual warnings are shown when publishing -->\n    <TrimmerSingleWarn>false</TrimmerSingleWarn>\n  </PropertyGroup>\n\n</Project>"
  },
  {
    "path": "Directory.Packages.props",
    "content": "<Project>\n  <PropertyGroup>\n    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>\n    <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>\n    <AspnetVersion>10.0.0</AspnetVersion>\n    <MicrosoftExtensionsVersion>10.1.0</MicrosoftExtensionsVersion>\n    <AspireVersion>13.1.0</AspireVersion>\n    <AspireUnstablePackagesVersion>13.1.0-preview.1.25616.3</AspireUnstablePackagesVersion>\n    <GrpcVersion>2.71.0</GrpcVersion>\n    <DuendeVersion>7.3.1</DuendeVersion>\n    <ApiVersioningVersion>8.1.0</ApiVersioningVersion>\n  </PropertyGroup>\n  <ItemGroup>\n    <!-- Version together with Aspire -->\n    <PackageVersion Include=\"Aspire.Hosting.Azure.CognitiveServices\" Version=\"$(AspireVersion)\" />\n    <PackageVersion Include=\"Aspire.Hosting.PostgreSQL\" Version=\"$(AspireVersion)\" />\n    <PackageVersion Include=\"Aspire.Hosting.RabbitMQ\" Version=\"$(AspireVersion)\" />\n    <PackageVersion Include=\"Aspire.Hosting.Redis\" Version=\"$(AspireVersion)\" />\n    <PackageVersion Include=\"Aspire.Hosting.Yarp\" Version=\"$(AspireVersion)\" />\n    <PackageVersion Include=\"Aspire.Npgsql\" Version=\"$(AspireVersion)\" />\n    <PackageVersion Include=\"Aspire.Npgsql.EntityFrameworkCore.PostgreSQL\" Version=\"$(AspireVersion)\" />\n    <PackageVersion Include=\"Aspire.RabbitMQ.Client\" Version=\"$(AspireVersion)\" />\n    <PackageVersion Include=\"Aspire.StackExchange.Redis\" Version=\"$(AspireVersion)\" />\n    <PackageVersion Include=\"Aspire.Azure.AI.OpenAI\" Version=\"$(AspireUnstablePackagesVersion)\" />\n    <PackageVersion Include=\"Microsoft.Extensions.ApiDescription.Server\" Version=\"10.0.0\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageVersion>\n    <PackageVersion Include=\"Microsoft.Extensions.ServiceDiscovery\" Version=\"$(MicrosoftExtensionsVersion)\" />\n    <PackageVersion Include=\"Microsoft.Extensions.ServiceDiscovery.Yarp\" Version=\"$(MicrosoftExtensionsVersion)\" />\n    <!-- Version together with Asp.Versioning -->\n    <PackageVersion Include=\"Asp.Versioning.Http\" Version=\"$(ApiVersioningVersion)\" />\n    <PackageVersion Include=\"Asp.Versioning.Http.Client\" Version=\"$(ApiVersioningVersion)\" />\n    <PackageVersion Include=\"Asp.Versioning.Mvc.ApiExplorer\" Version=\"$(ApiVersioningVersion)\" />\n    <!-- Version together with ASP.NET -->\n    <PackageVersion Include=\"Microsoft.AspNetCore.Authentication.JwtBearer\" Version=\"10.0.1\" />\n    <PackageVersion Include=\"Microsoft.AspNetCore.Authentication.OpenIdConnect\" Version=\"10.0.1\" />\n    <PackageVersion Include=\"Microsoft.AspNetCore.Components.QuickGrid\" Version=\"10.0.1\" />\n    <PackageVersion Include=\"Microsoft.AspNetCore.Components.Web\" Version=\"10.0.1\" />\n    <PackageVersion Include=\"Microsoft.AspNetCore.Identity.EntityFrameworkCore\" Version=\"10.0.1\" />\n    <PackageVersion Include=\"Microsoft.AspNetCore.Identity.UI\" Version=\"10.0.1\" />\n    <PackageVersion Include=\"Microsoft.AspNetCore.Mvc.Testing\" Version=\"10.0.1\" />\n    <PackageVersion Include=\"Microsoft.AspNetCore.OpenApi\" Version=\"10.0.1\" />\n    <PackageVersion Include=\"Microsoft.AspNetCore.TestHost\" Version=\"10.0.1\" />\n    <PackageVersion Include=\"Microsoft.Extensions.Identity.Stores\" Version=\"10.0.1\" />\n    <PackageVersion Include=\"Microsoft.Extensions.Http.Resilience\" Version=\"10.1.0\" />\n    <PackageVersion Include=\"Microsoft.OpenApi\" Version=\"2.3.2\" />\n    <PackageVersion Include=\"MSTest\" Version=\"4.0.2\" />\n    <!-- Version together with EF -->\n    <PackageVersion Include=\"Npgsql.EntityFrameworkCore.PostgreSQL\" Version=\"10.0.0\" />\n    <PackageVersion Include=\"Microsoft.EntityFrameworkCore.Tools\" Version=\"10.0.1\" />\n    <PackageVersion Include=\"NSubstitute\" Version=\"5.3.0\" />\n    <PackageVersion Include=\"NSubstitute.Analyzers.CSharp\" Version=\"1.0.17\" />\n    <PackageVersion Include=\"Pgvector\" Version=\"0.3.2\" />\n    <PackageVersion Include=\"Pgvector.EntityFrameworkCore\" Version=\"0.2.2\" />\n    <!-- Version together with runtime -->\n    <PackageVersion Include=\"Microsoft.Extensions.Options\" Version=\"10.0.1\" />\n    <PackageVersion Include=\"Microsoft.Extensions.Configuration.Abstractions\" Version=\"10.0.1\" />\n    <PackageVersion Include=\"Microsoft.Extensions.Logging.Abstractions\" Version=\"10.0.1\" />\n    <!-- Xabaril packages -->\n    <PackageVersion Include=\"AspNetCore.HealthChecks.Uris\" Version=\"9.0.0\" />\n    <!-- AI -->\n    <PackageVersion Include=\"CommunityToolkit.Aspire.Hosting.Ollama\" Version=\"9.8.0-beta.395\" />\n    <PackageVersion Include=\"CommunityToolkit.Aspire.OllamaSharp\" Version=\"9.8.0-beta.395\" />\n    <!-- Open Telemetry -->\n    <PackageVersion Include=\"OpenTelemetry.Exporter.OpenTelemetryProtocol\" Version=\"1.14.0\" />\n    <PackageVersion Include=\"OpenTelemetry.Extensions.Hosting\" Version=\"1.14.0\" />\n    <PackageVersion Include=\"OpenTelemetry.Instrumentation.AspNetCore\" Version=\"1.14.0\" />\n    <PackageVersion Include=\"OpenTelemetry.Instrumentation.GrpcNetClient\" Version=\"1.14.0-beta.1\" />\n    <PackageVersion Include=\"OpenTelemetry.Instrumentation.Http\" Version=\"1.14.0\" />\n    <PackageVersion Include=\"OpenTelemetry.Instrumentation.Runtime\" Version=\"1.14.0\" />\n    <!-- IdentityServer -->\n    <PackageVersion Include=\"Duende.IdentityServer\" Version=\"7.3.2\" />\n    <PackageVersion Include=\"Duende.IdentityServer.AspNetIdentity\" Version=\"7.3.2\" />\n    <PackageVersion Include=\"Duende.IdentityServer.EntityFramework\" Version=\"7.3.2\" />\n    <PackageVersion Include=\"Duende.IdentityServer.EntityFramework.Storage\" Version=\"7.3.2\" />\n    <PackageVersion Include=\"Duende.IdentityServer.Storage\" Version=\"7.3.2\" />\n    <!-- Scaffolding -->\n    <PackageVersion Include=\"Microsoft.VisualStudio.Web.CodeGeneration.Design\" Version=\"8.0.0-rc.1.23461.3\" />\n    <!-- Grpc -->\n    <PackageVersion Include=\"Grpc.AspNetCore\" Version=\"$(GrpcVersion)\" />\n    <PackageVersion Include=\"Grpc.Net.ClientFactory\" Version=\"$(GrpcVersion)\" />\n    <PackageVersion Include=\"Grpc.Tools\" Version=\"2.72.0\" PrivateAssets=\"All\" />\n    <!-- Miscellaneous -->\n    <PackageVersion Include=\"Dapper\" Version=\"2.1.35\" />\n    <PackageVersion Include=\"FluentValidation\" Version=\"12.0.0\" />\n    <PackageVersion Include=\"FluentValidation.DependencyInjectionExtensions\" Version=\"12.0.0\" />\n    <PackageVersion Include=\"Google.Protobuf\" Version=\"3.33.0\" />\n    <PackageVersion Include=\"Microsoft.Web.LibraryManager.Build\" Version=\"3.0.71\" />\n    <PackageVersion Include=\"System.Reflection.TypeExtensions\" Version=\"4.7.0\" />\n    <PackageVersion Include=\"xunit.v3.mtp-v2\" Version=\"3.2.1\" />\n    <PackageVersion Include=\"IdentityModel\" Version=\"7.0.0\" />\n    <PackageVersion Include=\"Scalar.AspNetCore\" Version=\"2.8.6\" />\n    <!-- Before license change -->\n    <PackageVersion Include=\"MediatR\" Version=\"13.0.0\" />\n  </ItemGroup>\n</Project>"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) .NET Foundation and Contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# eShop Reference Application - \"AdventureWorks\"\n\nA reference .NET application implementing an e-commerce website using a services-based architecture using [.NET Aspire](https://learn.microsoft.com/dotnet/aspire/).\n\n![eShop Reference Application architecture diagram](img/eshop_architecture.png)\n\n![eShop homepage screenshot](img/eshop_homepage.png)\n\n## Getting Started\n\nThis version of eShop is based on .NET 9. \n\nPrevious eShop versions:\n* [.NET 8](https://github.com/dotnet/eShop/tree/release/8.0)\n\n### Prerequisites\n\n- Clone the eShop repository: https://github.com/dotnet/eshop\n- [Install & start Docker Desktop](https://docs.docker.com/engine/install/)\n\n#### Windows with Visual Studio\n- Install [Visual Studio 2022 version 17.10 or newer](https://visualstudio.microsoft.com/vs/).\n  - Select the following workloads:\n    - `ASP.NET and web development` workload.\n    - `.NET Aspire SDK` component in `Individual components`.\n    - Optional: `.NET Multi-platform App UI development` to run client apps\n\nOr\n\n- Run the following commands in a Powershell & Terminal running as `Administrator` to automatically configure your environment with the required tools to build and run this application. (Note: A restart is required and included in the script below.)\n\n```powershell\ninstall-Module -Name Microsoft.WinGet.Configuration -AllowPrerelease -AcceptLicense -Force\n$env:Path = [System.Environment]::GetEnvironmentVariable(\"Path\",\"Machine\") + \";\" + [System.Environment]::GetEnvironmentVariable(\"Path\",\"User\")\nget-WinGetConfiguration -file .\\.configurations\\vside.dsc.yaml | Invoke-WinGetConfiguration -AcceptConfigurationAgreements\n```\n\nOr\n\n- From Dev Home go to `Machine Configuration -> Clone repositories`. Enter the URL for this repository. In the confirmation screen look for the section `Configuration File Detected` and click `Run File`.\n\n#### Mac, Linux, & Windows without Visual Studio\n- Install the latest [.NET 9 SDK](https://dot.net/download?cid=eshop)\n\nOr\n\n- Run the following commands in a Powershell & Terminal running as `Administrator` to automatically configuration your environment with the required tools to build and run this application. (Note: A restart is required after running the script below.)\n\n##### Install Visual Studio Code and related extensions\n```powershell\ninstall-Module -Name Microsoft.WinGet.Configuration -AllowPrerelease -AcceptLicense  -Force\n$env:Path = [System.Environment]::GetEnvironmentVariable(\"Path\",\"Machine\") + \";\" + [System.Environment]::GetEnvironmentVariable(\"Path\",\"User\")\nget-WinGetConfiguration -file .\\.configurations\\vscode.dsc.yaml | Invoke-WinGetConfiguration -AcceptConfigurationAgreements\n```\n\n> Note: These commands may require `sudo`\n\n- Optional: Install [Visual Studio Code with C# Dev Kit](https://code.visualstudio.com/docs/csharp/get-started)\n- Optional: Install [.NET MAUI Workload](https://learn.microsoft.com/dotnet/maui/get-started/installation?tabs=visual-studio-code)\n\n> Note: When running on Mac with Apple Silicon (M series processor), Rosetta 2 for grpc-tools. \n\n### Running the solution\n\n> [!WARNING]\n> Remember to ensure that Docker is started\n\n* (Windows only) Run the application from Visual Studio:\n - Open the `eShop.Web.slnf` file in Visual Studio\n - Ensure that `eShop.AppHost.csproj` is your startup project\n - Hit Ctrl-F5 to launch Aspire\n\n* Or run the application from your terminal:\n```powershell\ndotnet run --project src/eShop.AppHost/eShop.AppHost.csproj\n```\nthen look for lines like this in the console output in order to find the URL to open the Aspire dashboard:\n```sh\nLogin to the dashboard at: http://localhost:19888/login?t=uniquelogincodeforyou\n```\n\n> You may need to install ASP.NET Core HTTPS development certificates first, and then close all browser tabs. Learn more at https://aka.ms/aspnet/https-trust-dev-cert\n\n### Azure Open AI\n\nWhen using Azure OpenAI, inside *eShop.AppHost/appsettings.json*, add the following section:\n\n```json\n  \"ConnectionStrings\": {\n    \"OpenAi\": \"Endpoint=xxx;Key=xxx;\"\n  }\n```\n\nReplace the values with your own. Then, in the eShop.AppHost *Program.cs*, set this value to **true**\n\n```csharp\nbool useOpenAI = false;\n```\n\nHere's additional guidance on the [.NET Aspire OpenAI component](https://learn.microsoft.com/dotnet/aspire/azureai/azureai-openai-component?tabs=dotnet-cli). \n\n### Use Azure Developer CLI\n\nYou can use the [Azure Developer CLI](https://aka.ms/azd) to run this project on Azure with only a few commands. Follow the next instructions:\n\n- Install the latest or update to the latest [Azure Developer CLI (azd)](https://aka.ms/azure-dev/install).\n- Log in `azd` (if you haven't done it before) to your Azure account:\n```sh\nazd auth login\n```\n- Initialize `azd` from the root of the repo.\n```sh\nazd init\n```\n- During init:\n  - Select `Use code in the current directory`. Azd will automatically detect the .NET Aspire project.\n  - Confirm `.NET (Aspire)` and continue.\n  - Select which services to expose to the Internet (exposing `webapp` is enough to test the sample).\n  - Finalize the initialization by giving a name to your environment.\n\n- Create Azure resources and deploy the sample by running:\n```sh\nazd up\n```\nNotes:\n  - The operation takes a few minutes the first time it is ever run for an environment.\n  - At the end of the process, `azd` will display the `url` for the webapp. Follow that link to test the sample.\n  - You can run `azd up` after saving changes to the sample to re-deploy and update the sample.\n  - Report any issues to [azure-dev](https://github.com/Azure/azure-dev/issues) repo.\n  - [FAQ and troubleshoot](https://learn.microsoft.com/azure/developer/azure-developer-cli/troubleshoot?tabs=Browser) for azd.\n\n## Contributing\n\nFor more information on contributing to this repo, read [the contribution documentation](./CONTRIBUTING.md) and [the Code of Conduct](CODE-OF-CONDUCT.md).\n\n### Sample data\n\nThe sample catalog data is defined in [catalog.json](https://github.com/dotnet/eShop/blob/main/src/Catalog.API/Setup/catalog.json). Those product names, descriptions, and brand names are fictional and were generated using [GPT-35-Turbo](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/chatgpt), and the corresponding [product images](https://github.com/dotnet/eShop/tree/main/src/Catalog.API/Pics) were generated using [DALL·E 3](https://openai.com/dall-e-3).\n\n## eShop on Azure\n\nFor a version of this app configured for deployment on Azure, please view [the eShop on Azure](https://github.com/Azure-Samples/eShopOnAzure) repo.\n"
  },
  {
    "path": "build/acr-build/queue-all.ps1",
    "content": "Param(\n    [parameter(Mandatory=$false)][string]$acrName,\n    [parameter(Mandatory=$false)][string]$gitUser,\n    [parameter(Mandatory=$false)][string]$repoName=\"eShopOnContainers\",\n    [parameter(Mandatory=$false)][string]$gitBranch=\"dev\",\n    [parameter(Mandatory=$true)][string]$patToken\n)\n\n$gitContext = \"https://github.com/$gitUser/$repoName\"\n\n$services = @( \n    @{ Name=\"eshopbasket\"; Image=\"eshop/basket.api\"; File=\"src/Services/Basket/Basket.API/Dockerfile\" },\n    @{ Name=\"eshopcatalog\"; Image=\"eshop/catalog.api\"; File=\"src/Services/Catalog/Catalog.API/Dockerfile\" },\n    @{ Name=\"eshopidentity\"; Image=\"eshop/identity.api\"; File=\"src/Services/Identity/Identity.API/Dockerfile\" },\n    @{ Name=\"eshopordering\"; Image=\"eshop/ordering.api\"; File=\"src/Services/Ordering/Ordering.API/Dockerfile\" },\n\t@{ Name=\"eshoporderingbg\"; Image=\"eshop/orderprocessor\"; File=\"src/Services/Ordering/OrderProcessor/Dockerfile\" },\n    @{ Name=\"eshopwebspa\"; Image=\"eshop/webspa\"; File=\"src/Web/WebSPA/Dockerfile\" },\n    @{ Name=\"eshopwebmvc\"; Image=\"eshop/webmvc\"; File=\"src/Web/WebMVC/Dockerfile\" },\n    @{ Name=\"eshopwebstatus\"; Image=\"eshop/webstatus\"; File=\"src/Web/WebStatus/Dockerfile\" },\n    @{ Name=\"eshoppayment\"; Image=\"eshop/paymentprocessor\"; File=\"src/Services/Payment/PaymentProcessor/Dockerfile\" },\n    @{ Name=\"eshopocelotapigw\"; Image=\"eshop/ocelotapigw\"; File=\"src/ApiGateways/ApiGw-Base/Dockerfile\" },\n    @{ Name=\"eshopmobileshoppingagg\"; Image=\"eshop/mobileshoppingagg\"; File=\"src/ApiGateways/Mobile.Bff.Shopping/aggregator/Dockerfile\" },\n    @{ Name=\"eshopwebshoppingagg\"; Image=\"eshop/webshoppingagg\"; File=\"src/ApiGateways/Web.Bff.Shopping/aggregator/Dockerfile\" },\n    @{ Name=\"eshoporderingsignalrhub\"; Image=\"eshop/ordering.signalrhub\"; File=\"src/Services/Ordering/Ordering.SignalrHub/Dockerfile\" }\n)\n\n$services |% {\n    $bname = $_.Name\n    $bimg = $_.Image\n    $bfile = $_.File\n    Write-Host \"Setting ACR build $bname ($bimg)\"    \n    az acr build-task create --registry $acrName --name $bname --image ${bimg}:$gitBranch --context $gitContext --branch $gitBranch --git-access-token $patToken --file $bfile\n}\n"
  },
  {
    "path": "build/multiarch-manifests/create-manifests.ps1",
    "content": "Param(\n    [parameter(Mandatory=$true)][string]$registry\n)\n\nif ([String]::IsNullOrEmpty($registry)) {\n    Write-Host \"Registry must be set to docker registry to use\" -ForegroundColor Red\n    exit 1 \n}\n\nWrite-Host \"This script creates the local manifests, for pushing the multi-arch manifests\" -ForegroundColor Yellow\nWrite-Host \"Tags used are linux-master, win-master, linux-dev, win-dev, linux-latest, win-latest\" -ForegroundColor Yellow\nWrite-Host \"Multiarch images tags will be master, dev, latest\" -ForegroundColor Yellow\n\n\n$services = \"identity.api\", \"basket.api\", \"catalog.api\", \"ordering.api\", \"orderprocessor\", \"paymentprocessor\", \"webhooks.api\", \"ocelotapigw\", \"mobileshoppingagg\", \"webshoppingagg\", \"ordering.signalrhub\", \"webstatus\", \"webspa\", \"webmvc\", \"webhooks.client\"\n\nforeach ($svc in $services) {\n    Write-Host \"Creating manifest for $svc and tags :latest, :master, and :dev\"\n    docker manifest create $registry/${svc}:master $registry/${svc}:linux-master $registry/${svc}:win-master\n    docker manifest create $registry/${svc}:dev $registry/${svc}:linux-dev $registry/${svc}:win-dev\n    docker manifest create $registry/${svc}:latest $registry/${svc}:linux-latest $registry/${svc}:win-latest\n    Write-Host \"Pushing manifest for $svc and tags :latest, :master, and :dev\"\n    docker manifest push $registry/${svc}:latest\n    docker manifest push $registry/${svc}:dev\n    docker manifest push $registry/${svc}:master\n}"
  },
  {
    "path": "ci.yml",
    "content": "# Configure which branches trigger builds\ntrigger:\n  batch: true\n  branches:\n    include:\n      - main\n\nvariables:\n  - name: TeamName\n    value: dotnet-aspire\n\nresources:\n  repositories:\n    # Repo: 1ESPipelineTemplates/1ESPipelineTemplates\n    - repository: 1esPipelines\n      type: git\n      name: 1ESPipelineTemplates/1ESPipelineTemplates\n      ref: refs/tags/release\nextends:\n  template: v1/1ES.Unofficial.PipelineTemplate.yml@1esPipelines\n  parameters:\n    sdl:\n      policheck:\n        enabled: true\n      tsa:\n        enabled: true\n    pool:\n      name: NetCore1ESPool-Svc-Internal\n      image: windows.vs2019.amd64\n      os: windows\n    stages:\n      - stage: buildStage\n        displayName: Build Stage\n        jobs:\n          - job: Build\n            displayName: Windows Build\n            timeoutInMinutes: 90\n            workspace:\n              clean: all\n            steps:\n              - task: UseDotNet@2\n                inputs:\n                  useGlobalJson: true\n              - script: dotnet build eShop.Web.slnf\n                displayName: Build Step\n"
  },
  {
    "path": "e2e/AddItemTest.spec.ts",
    "content": "import { test, expect } from '@playwright/test';\n\ntest('Add item to the cart', async ({ page }) => {\n  await page.goto('/');\n\n  await expect(page.getByRole('heading', { name: 'Ready for a new adventure?' })).toBeVisible();\n  await page.getByRole('link', { name: 'Adventurer GPS Watch' }).click();\n  await page.getByRole('button', { name: 'Add to shopping bag' }).click();\n  await page.getByRole('link', { name: 'shopping bag' }).click();\n  await page.getByRole('heading', { name: 'Shopping bag' }).click();\n\n  await page.getByText('Total').nth(1).click();\n  await page.getByLabel('product quantity').getByText('1');\n\n  await expect.poll(() => page.getByLabel('product quantity').count()).toBeGreaterThan(0);\n});"
  },
  {
    "path": "e2e/BrowseItemTest.spec.ts",
    "content": "import { test, expect } from '@playwright/test';\n\ntest('Browse Items', async ({ page }) => {\n  await page.goto('/');\n\n  await expect(page.getByRole('heading', { name: 'Ready for a new adventure?' })).toBeVisible();\n\n  await page.getByRole('link', { name: 'Adventurer GPS Watch' }).click(); \n  await page.getByRole('heading', { name: 'Adventurer GPS Watch' }).click();\n  \n  //Expect\n  await expect(page.getByRole('heading', { name: 'Adventurer GPS Watch' })).toBeVisible();\n});"
  },
  {
    "path": "e2e/RemoveItemTest.spec.ts",
    "content": "import { test, expect } from '@playwright/test';\n\ntest('Remove item from cart', async ({ page }) => {\n  await page.goto('/');\n  await expect(page.getByRole('heading', { name: 'Ready for a new adventure?' })).toBeVisible();\n  \n  await page.getByRole('link', { name: 'Adventurer GPS Watch' }).click();\n  await expect(page.getByRole('heading', { name: 'Adventurer GPS Watch' })).toBeVisible();\n  \n  await page.getByRole('button', { name: 'Add to shopping bag' }).click();\n  await page.getByRole('link', { name: 'shopping bag' }).click();\n  await expect(page.getByRole('heading', { name: 'Shopping bag' })).toBeVisible();\n\n  await expect.poll(() => page.getByLabel('product quantity').count()).toBeGreaterThan(0);\n  \n  await page.getByLabel('product quantity').fill('0');\n\n  await page.getByRole('button', { name: 'Update' }).click();\n\n  await expect(page.getByText('Your shopping bag is empty')).toBeVisible();\n});"
  },
  {
    "path": "e2e/login.setup.ts",
    "content": "import { test as setup, expect } from '@playwright/test';\nimport { STORAGE_STATE } from '../playwright.config';\nimport { assert } from 'console';\n\nassert(process.env.USERNAME1, 'USERNAME1 is not set');\nassert(process.env.PASSWORD, 'PASSWORD is not set');\n\nsetup('Login', async ({ page }) => {\n  await page.goto('/');\n  await expect(page.getByRole('heading', { name: 'Ready for a new adventure?' })).toBeVisible();\n\n  await page.getByLabel('Sign in').click();\n  await expect(page.getByRole('heading', { name: 'Login' })).toBeVisible();\n\n  await page.getByPlaceholder('Username').fill(process.env.USERNAME1!);\n  await page.getByPlaceholder('Password').fill(process.env.PASSWORD!);\n  await page.getByRole('button', { name: 'Login' }).click();\n  await expect(page.getByRole('heading', { name: 'Ready for a new adventure?' })).toBeVisible();\n  await page.context().storageState({ path: STORAGE_STATE });\n})\n"
  },
  {
    "path": "eShop.Web.slnf",
    "content": "{\n  \"solution\": {\n    \"path\": \"eShop.slnx\",\n    \"projects\": [\n      \"src\\\\Basket.API\\\\Basket.API.csproj\",\n      \"src\\\\Catalog.API\\\\Catalog.API.csproj\",\n      \"src\\\\EventBusRabbitMQ\\\\EventBusRabbitMQ.csproj\",\n      \"src\\\\EventBus\\\\EventBus.csproj\",\n      \"src\\\\Identity.API\\\\Identity.API.csproj\",\n      \"src\\\\IntegrationEventLogEF\\\\IntegrationEventLogEF.csproj\",\n      \"src\\\\Ordering.API\\\\Ordering.API.csproj\",\n      \"src\\\\OrderProcessor\\\\OrderProcessor.csproj\",\n      \"src\\\\Ordering.Domain\\\\Ordering.Domain.csproj\",\n      \"src\\\\Ordering.Infrastructure\\\\Ordering.Infrastructure.csproj\",\n      \"src\\\\PaymentProcessor\\\\PaymentProcessor.csproj\",\n      \"src\\\\WebAppComponents\\\\WebAppComponents.csproj\",\n      \"src\\\\WebApp\\\\WebApp.csproj\",\n      \"src\\\\WebhookClient\\\\WebhookClient.csproj\",\n      \"src\\\\Webhooks.API\\\\Webhooks.API.csproj\",\n      \"src\\\\eShop.AppHost\\\\eShop.AppHost.csproj\",\n      \"src\\\\eShop.ServiceDefaults\\\\eShop.ServiceDefaults.csproj\",\n      \"tests\\\\Basket.UnitTests\\\\Basket.UnitTests.csproj\",\n      \"tests\\\\Catalog.FunctionalTests\\\\Catalog.FunctionalTests.csproj\",\n      \"tests\\\\Ordering.FunctionalTests\\\\Ordering.FunctionalTests.csproj\",\n      \"tests\\\\Ordering.UnitTests\\\\Ordering.UnitTests.csproj\"\n    ]\n  }\n}"
  },
  {
    "path": "eShop.slnx",
    "content": "﻿<Solution>\n  <Folder Name=\"/src/\">\n    <Project Path=\"src/eShop.AppHost/eShop.AppHost.csproj\" />\n    <Project Path=\"src/Basket.API/Basket.API.csproj\" />\n    <Project Path=\"src/Catalog.API/Catalog.API.csproj\" />\n    <Project Path=\"src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj\" />\n    <Project Path=\"src/EventBus/EventBus.csproj\" />\n    <Project Path=\"src/EventBusRabbitMQ/EventBusRabbitMQ.csproj\" />\n    <Project Path=\"src/Identity.API/Identity.API.csproj\" />\n    <Project Path=\"src/IntegrationEventLogEF/IntegrationEventLogEF.csproj\" />\n    <Project Path=\"src/Ordering.API/Ordering.API.csproj\" />\n    <Project Path=\"src/OrderProcessor/OrderProcessor.csproj\" />\n    <Project Path=\"src/Ordering.Domain/Ordering.Domain.csproj\" />\n    <Project Path=\"src/Ordering.Infrastructure/Ordering.Infrastructure.csproj\" />\n    <Project Path=\"src/PaymentProcessor/PaymentProcessor.csproj\" />\n    <Project Path=\"src/WebApp/WebApp.csproj\" />\n    <Project Path=\"src/WebhookClient/WebhookClient.csproj\" />\n    <Project Path=\"src/Webhooks.API/Webhooks.API.csproj\" />\n    <Project Path=\"src/WebAppComponents/WebAppComponents.csproj\" />\n    <Project Path=\"src/HybridApp/HybridApp.csproj\" />\n    <Project Path=\"src/ClientApp/ClientApp.csproj\" />\n  </Folder>\n  <Folder Name=\"/tests/\">\n    <Project Path=\"tests/Basket.UnitTests/Basket.UnitTests.csproj\" />\n    <Project Path=\"tests/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj\" />\n    <Project Path=\"tests/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj\" />\n    <Project Path=\"tests/Ordering.UnitTests/Ordering.UnitTests.csproj\" />\n    <Project Path=\"tests/ClientApp.UnitTests/ClientApp.UnitTests.csproj\" />\n  </Folder>\n  <Folder Name=\"/Solution Items/\">\n    <File Path=\".editorconfig\" />\n    <File Path=\"Directory.Packages.props\" />\n    <File Path=\"NuGet.config\" />\n    <File Path=\"README.md\" />\n  </Folder>\n</Solution>"
  },
  {
    "path": "es-metadata.yml",
    "content": "schemaVersion: 0.0.1\nisProduction: true\naccountableOwners:\n  service: 4db45fa9-fb0f-43ce-b523-ad1da773dfbc\nrouting:\n  defaultAreaPath:\n    org: devdiv\n    path: DevDiv\\ASP.NET Core\n"
  },
  {
    "path": "global.json",
    "content": "{\n  \"sdk\": {\n    \"version\": \"10.0.100\",\n    \"rollForward\": \"latestFeature\",\n    \"allowPrerelease\": true\n  },\n  \"test\": {\n    \"runner\": \"Microsoft.Testing.Platform\"\n  },\n  \"msbuild-sdks\": {\n    \"MSTest.Sdk\": \"4.0.2\"\n  }\n}\n"
  },
  {
    "path": "nuget.config",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<configuration>\n  <packageSources>\n    <!--To inherit the global NuGet package sources remove the <clear/> line below -->\n    <clear />\n    <add key=\"nuget\" value=\"https://api.nuget.org/v3/index.json\" />\n  </packageSources>\n\n  <packageSourceMapping>\n    <!-- key value for <packageSource> should match key values from <packageSources> element -->\n    <clear />\n    <packageSource key=\"nuget\">\n      <package pattern=\"*\" />\n    </packageSource>\n  </packageSourceMapping>\n</configuration>\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"eshop\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A reference .NET application implementing an eCommerce web site using a services-based architecture.\",\n  \"directories\": {\n    \"test\": \"tests\"\n  },\n  \"scripts\": {},\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@playwright/test\": \"^1.42.1\",\n    \"@types/node\": \"^20.11.25\",\n    \"dotenv\": \"^16.4.5\"\n  }\n}\n"
  },
  {
    "path": "playwright.config.ts",
    "content": "import { defineConfig, devices } from '@playwright/test';\nrequire(\"dotenv\").config({ path: \"./.env\" });\nimport path from 'path';\n\nexport const STORAGE_STATE = path.join(__dirname, 'playwright/.auth/user.json');\n\n/**\n * See https://playwright.dev/docs/test-configuration.\n */\nexport default defineConfig({\n  testDir: './e2e',\n  /* Run tests in files in parallel */\n  fullyParallel: true,\n  /* Fail the build on CI if you accidentally left test.only in the source code. */\n  forbidOnly: !!process.env.CI,\n  /* Retry on CI only */\n  retries: process.env.CI ? 2 : 0,\n  /* Opt out of parallel tests on CI. */\n  workers: process.env.CI ? 1 : undefined,\n  /* Reporter to use. See https://playwright.dev/docs/test-reporters */\n  reporter: 'html',\n  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */\n  use: {\n    /* Base URL to use in actions like `await page.goto('/')`. */\n    baseURL: 'http://localhost:5045',\n\n    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */\n    trace: 'on-first-retry',\n    ...devices['Desktop Chrome'],\n  },\n\n  /* Configure projects for major browsers */\n  projects: [\n    {\n      name: 'setup',\n      testMatch: '**/*.setup.ts',\n    },\n    {\n      name: 'e2e tests logged in',\n      testMatch: ['**/AddItemTest.spec.ts', '**/RemoveItemTest.spec.ts'],\n      dependencies: ['setup'],\n      use: {\n        storageState: STORAGE_STATE,\n      },\n    },\n    {\n      name: 'e2e tests without logged in',\n      testMatch: ['**/BrowseItemTest.spec.ts'],\n    }\n    // {\n    //   name: 'chromium',\n    //   use: { ...devices['Desktop Chrome'] },\n    // },\n\n    // {\n    //   name: 'firefox',\n    //   use: { ...devices['Desktop Firefox'] },\n    // },\n\n    // {\n    //   name: 'webkit',\n    //   use: { ...devices['Desktop Safari'] },\n    // },\n\n    /* Test against mobile viewports. */\n    // {\n    //   name: 'Mobile Chrome',\n    //   use: { ...devices['Pixel 5'] },\n    // },\n    // {\n    //   name: 'Mobile Safari',\n    //   use: { ...devices['iPhone 12'] },\n    // },\n\n    /* Test against branded browsers. */\n    // {\n    //   name: 'Microsoft Edge',\n    //   use: { ...devices['Desktop Edge'], channel: 'msedge' },\n    // },\n    // {\n    //   name: 'Google Chrome',\n    //   use: { ...devices['Desktop Chrome'], channel: 'chrome' },\n    // },\n  ],\n\n  /* Run your local dev server before starting the tests */\n  webServer: {\n    command: 'dotnet run --project src/eShop.AppHost/eShop.AppHost.csproj',\n    url: 'http://localhost:5045',\n    reuseExistingServer: !process.env.CI,\n    stderr: 'pipe',\n    stdout: 'pipe',\n    timeout: process.env.CI ? (5 * 60_000) : 60_000,\n  },\n});\n"
  },
  {
    "path": "src/Basket.API/Basket.API.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <UserSecretsId>2964ec8e-0d48-4541-b305-94cab537f867</UserSecretsId>\n    <PublishAot Condition=\"'$(EnableAotPublishing)' == 'true'\">true</PublishAot>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Aspire.StackExchange.Redis\" />\n    <PackageReference Include=\"Grpc.AspNetCore\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Protobuf Include=\"Proto\\basket.proto\" GrpcServices=\"Server\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\eShop.ServiceDefaults\\eShop.ServiceDefaults.csproj\" />\n    <ProjectReference Include=\"..\\EventBusRabbitMQ\\EventBusRabbitMQ.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <InternalsVisibleTo Include=\"Basket.FunctionalTests\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Basket.API/Extensions/Extensions.cs",
    "content": "﻿using System.Text.Json.Serialization;\nusing eShop.Basket.API.Repositories;\nusing eShop.Basket.API.IntegrationEvents.EventHandling;\nusing eShop.Basket.API.IntegrationEvents.EventHandling.Events;\n\nnamespace eShop.Basket.API.Extensions;\n\npublic static class Extensions\n{\n    public static void AddApplicationServices(this IHostApplicationBuilder builder)\n    {\n        builder.AddDefaultAuthentication();\n\n        builder.AddRedisClient(\"redis\");\n\n        builder.Services.AddSingleton<IBasketRepository, RedisBasketRepository>();\n\n        builder.AddRabbitMqEventBus(\"eventbus\")\n               .AddSubscription<OrderStartedIntegrationEvent, OrderStartedIntegrationEventHandler>()\n               .ConfigureJsonOptions(options => options.TypeInfoResolverChain.Add(IntegrationEventContext.Default));\n    }\n}\n\n[JsonSerializable(typeof(OrderStartedIntegrationEvent))]\npartial class IntegrationEventContext : JsonSerializerContext\n{\n\n}\n"
  },
  {
    "path": "src/Basket.API/Extensions/ServerCallContextIdentityExtensions.cs",
    "content": "﻿#nullable enable\n\nnamespace eShop.Basket.API.Extensions;\n\ninternal static class ServerCallContextIdentityExtensions\n{\n    public static string? GetUserIdentity(this ServerCallContext context) => context.GetHttpContext().User.FindFirst(\"sub\")?.Value;\n    public static string? GetUserName(this ServerCallContext context) => context.GetHttpContext().User.FindFirst(x => x.Type == ClaimTypes.Name)?.Value;\n}\n"
  },
  {
    "path": "src/Basket.API/GlobalUsings.cs",
    "content": "﻿global using System.ComponentModel.DataAnnotations;\nglobal using System.Security.Claims;\nglobal using System.Text.Json;\nglobal using Grpc.Core;\nglobal using Microsoft.AspNetCore.Authorization;\nglobal using eShop.Basket.API.Extensions;\nglobal using eShop.Basket.API.Grpc;\nglobal using eShop.EventBus.Abstractions;\nglobal using eShop.EventBus.Events;\nglobal using eShop.ServiceDefaults;\nglobal using StackExchange.Redis;\n"
  },
  {
    "path": "src/Basket.API/Grpc/BasketService.cs",
    "content": "﻿using System.Diagnostics.CodeAnalysis;\nusing eShop.Basket.API.Repositories;\nusing eShop.Basket.API.Extensions;\nusing eShop.Basket.API.Model;\n\nnamespace eShop.Basket.API.Grpc;\n\npublic class BasketService(\n    IBasketRepository repository,\n    ILogger<BasketService> logger) : Basket.BasketBase\n{\n    [AllowAnonymous]\n    public override async Task<CustomerBasketResponse> GetBasket(GetBasketRequest request, ServerCallContext context)\n    {\n        var userId = context.GetUserIdentity();\n        if (string.IsNullOrEmpty(userId))\n        {\n            return new();\n        }\n\n        if (logger.IsEnabled(LogLevel.Debug))\n        {\n            logger.LogDebug(\"Begin GetBasketById call from method {Method} for basket id {Id}\", context.Method, userId);\n        }\n\n        var data = await repository.GetBasketAsync(userId);\n\n        if (data is not null)\n        {\n            return MapToCustomerBasketResponse(data);\n        }\n\n        return new();\n    }\n\n    public override async Task<CustomerBasketResponse> UpdateBasket(UpdateBasketRequest request, ServerCallContext context)\n    {\n        var userId = context.GetUserIdentity();\n        if (string.IsNullOrEmpty(userId))\n        {\n            ThrowNotAuthenticated();\n        }\n\n        if (logger.IsEnabled(LogLevel.Debug))\n        {\n            logger.LogDebug(\"Begin UpdateBasket call from method {Method} for basket id {Id}\", context.Method, userId);\n        }\n\n        var customerBasket = MapToCustomerBasket(userId, request);\n        var response = await repository.UpdateBasketAsync(customerBasket);\n        if (response is null)\n        {\n            ThrowBasketDoesNotExist(userId);\n        }\n\n        return MapToCustomerBasketResponse(response);\n    }\n\n    public override async Task<DeleteBasketResponse> DeleteBasket(DeleteBasketRequest request, ServerCallContext context)\n    {\n        var userId = context.GetUserIdentity();\n        if (string.IsNullOrEmpty(userId))\n        {\n            ThrowNotAuthenticated();\n        }\n\n        await repository.DeleteBasketAsync(userId);\n        return new();\n    }\n\n    [DoesNotReturn]\n    private static void ThrowNotAuthenticated() => throw new RpcException(new Status(StatusCode.Unauthenticated, \"The caller is not authenticated.\"));\n\n    [DoesNotReturn]\n    private static void ThrowBasketDoesNotExist(string userId) => throw new RpcException(new Status(StatusCode.NotFound, $\"Basket with buyer id {userId} does not exist\"));\n\n    private static CustomerBasketResponse MapToCustomerBasketResponse(CustomerBasket customerBasket)\n    {\n        var response = new CustomerBasketResponse();\n\n        foreach (var item in customerBasket.Items)\n        {\n            response.Items.Add(new BasketItem()\n            {\n                ProductId = item.ProductId,\n                Quantity = item.Quantity,\n            });\n        }\n\n        return response;\n    }\n\n    private static CustomerBasket MapToCustomerBasket(string userId, UpdateBasketRequest customerBasketRequest)\n    {\n        var response = new CustomerBasket\n        {\n            BuyerId = userId\n        };\n\n        foreach (var item in customerBasketRequest.Items)\n        {\n            response.Items.Add(new()\n            {\n                ProductId = item.ProductId,\n                Quantity = item.Quantity,\n            });\n        }\n\n        return response;\n    }\n}\n"
  },
  {
    "path": "src/Basket.API/IntegrationEvents/EventHandling/OrderStartedIntegrationEventHandler.cs",
    "content": "﻿using eShop.Basket.API.Repositories;\nusing eShop.Basket.API.IntegrationEvents.EventHandling.Events;\n\nnamespace eShop.Basket.API.IntegrationEvents.EventHandling;\n\npublic class OrderStartedIntegrationEventHandler(\n    IBasketRepository repository,\n    ILogger<OrderStartedIntegrationEventHandler> logger) : IIntegrationEventHandler<OrderStartedIntegrationEvent>\n{\n    public async Task Handle(OrderStartedIntegrationEvent @event)\n    {\n        logger.LogInformation(\"Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})\", @event.Id, @event);\n\n        await repository.DeleteBasketAsync(@event.UserId);\n    }\n}\n"
  },
  {
    "path": "src/Basket.API/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs",
    "content": "﻿namespace eShop.Basket.API.IntegrationEvents.EventHandling.Events;\n\n// Integration Events notes:\n// An Event is \"something that has happened in the past\", therefore its name has to be\n// An Integration Event is an event that can cause side effects to other microservices, Bounded-Contexts or external systems.\npublic record OrderStartedIntegrationEvent(string UserId) : IntegrationEvent;\n"
  },
  {
    "path": "src/Basket.API/Model/BasketItem.cs",
    "content": "﻿namespace eShop.Basket.API.Model;\n\npublic class BasketItem : IValidatableObject\n{\n    public string Id { get; set; }\n    public int ProductId { get; set; }\n    public string ProductName { get; set; }\n    public decimal UnitPrice { get; set; }\n    public decimal OldUnitPrice { get; set; }\n    public int Quantity { get; set; }\n    public string PictureUrl { get; set; }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        var results = new List<ValidationResult>();\n\n        if (Quantity < 1)\n        {\n            results.Add(new ValidationResult(\"Invalid number of units\", new[] { \"Quantity\" }));\n        }\n\n        return results;\n    }\n}\n"
  },
  {
    "path": "src/Basket.API/Model/CustomerBasket.cs",
    "content": "﻿namespace eShop.Basket.API.Model;\n\npublic class CustomerBasket\n{\n    public string BuyerId { get; set; }\n\n    public List<BasketItem> Items { get; set; } = [];\n\n    public CustomerBasket() { }\n\n    public CustomerBasket(string customerId)\n    {\n        BuyerId = customerId;\n    }\n}\n"
  },
  {
    "path": "src/Basket.API/Program.cs",
    "content": "﻿var builder = WebApplication.CreateBuilder(args);\n\nbuilder.AddBasicServiceDefaults();\nbuilder.AddApplicationServices();\n\nbuilder.Services.AddGrpc();\n\nvar app = builder.Build();\n\napp.MapDefaultEndpoints();\n\napp.MapGrpcService<BasketService>();\n\napp.Run();\n"
  },
  {
    "path": "src/Basket.API/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"http\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"applicationUrl\": \"http://localhost:5221\",\n      \"environmentVariables\": {\n        \"Identity__Url\": \"http://localhost:5223\",\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "src/Basket.API/Proto/basket.proto",
    "content": "﻿syntax = \"proto3\";\n\noption csharp_namespace = \"eShop.Basket.API.Grpc\";\n\npackage BasketApi;\n\nservice Basket {\n    rpc GetBasket(GetBasketRequest) returns (CustomerBasketResponse) {}\n    rpc UpdateBasket(UpdateBasketRequest) returns (CustomerBasketResponse) {}\n    rpc DeleteBasket(DeleteBasketRequest) returns (DeleteBasketResponse) {}\n}\n\nmessage GetBasketRequest {\n}\n\nmessage CustomerBasketResponse {\n    repeated BasketItem items = 1;\n}\n\nmessage BasketItem {\n    int32 product_id = 2;\n    int32 quantity = 6;\n}\n\nmessage UpdateBasketRequest {\n    repeated BasketItem items = 2;\n}\n\nmessage DeleteBasketRequest {\n}\n\nmessage DeleteBasketResponse {\n}"
  },
  {
    "path": "src/Basket.API/Repositories/IBasketRepository.cs",
    "content": "﻿using eShop.Basket.API.Model;\n\nnamespace eShop.Basket.API.Repositories;\n\npublic interface IBasketRepository\n{\n    Task<CustomerBasket> GetBasketAsync(string customerId);\n    Task<CustomerBasket> UpdateBasketAsync(CustomerBasket basket);\n    Task<bool> DeleteBasketAsync(string id);\n}\n"
  },
  {
    "path": "src/Basket.API/Repositories/RedisBasketRepository.cs",
    "content": "﻿using System.Text.Json.Serialization;\nusing eShop.Basket.API.Model;\n\nnamespace eShop.Basket.API.Repositories;\n\npublic class RedisBasketRepository(ILogger<RedisBasketRepository> logger, IConnectionMultiplexer redis) : IBasketRepository\n{\n    private readonly IDatabase _database = redis.GetDatabase();\n\n    // implementation:\n\n    // - /basket/{id} \"string\" per unique basket\n    private static RedisKey BasketKeyPrefix = \"/basket/\"u8.ToArray();\n    // note on UTF8 here: library limitation (to be fixed) - prefixes are more efficient as blobs\n\n    private static RedisKey GetBasketKey(string userId) => BasketKeyPrefix.Append(userId);\n\n    public async Task<bool> DeleteBasketAsync(string id)\n    {\n        return await _database.KeyDeleteAsync(GetBasketKey(id));\n    }\n\n    public async Task<CustomerBasket> GetBasketAsync(string customerId)\n    {\n        using var data = await _database.StringGetLeaseAsync(GetBasketKey(customerId));\n\n        if (data is null || data.Length == 0)\n        {\n            return null;\n        }\n        return JsonSerializer.Deserialize(data.Span, BasketSerializationContext.Default.CustomerBasket);\n    }\n\n    public async Task<CustomerBasket> UpdateBasketAsync(CustomerBasket basket)\n    {\n        var json = JsonSerializer.SerializeToUtf8Bytes(basket, BasketSerializationContext.Default.CustomerBasket);\n        var created = await _database.StringSetAsync(GetBasketKey(basket.BuyerId), json);\n\n        if (!created)\n        {\n            logger.LogInformation(\"Problem occurred persisting the item.\");\n            return null;\n        }\n\n\n        logger.LogInformation(\"Basket item persisted successfully.\");\n        return await GetBasketAsync(basket.BuyerId);\n    }\n}\n\n[JsonSerializable(typeof(CustomerBasket))]\n[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]\npublic partial class BasketSerializationContext : JsonSerializerContext\n{\n\n}\n"
  },
  {
    "path": "src/Basket.API/appsettings.Development.json",
    "content": "{\n  \n}"
  },
  {
    "path": "src/Basket.API/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  },\n  \"Kestrel\": {\n    \"EndpointDefaults\": {\n      \"Protocols\": \"Http2\"\n    }\n  },\n  \"ConnectionStrings\": {\n    \"Redis\": \"localhost\",\n    \"EventBus\": \"amqp://localhost\"\n  },\n  \"Identity\": {\n    \"Audience\": \"basket\"\n  },\n  \"EventBus\": {\n    \"SubscriptionClientName\": \"Basket\"\n  }\n}\n"
  },
  {
    "path": "src/Catalog.API/Apis/CatalogApi.cs",
    "content": "using System.ComponentModel;\nusing System.ComponentModel.DataAnnotations;\nusing Microsoft.AspNetCore.Http.HttpResults;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.Infrastructure;\nusing Pgvector.EntityFrameworkCore;\n\nnamespace eShop.Catalog.API;\n\npublic static class CatalogApi\n{\n    public static IEndpointRouteBuilder MapCatalogApi(this IEndpointRouteBuilder app)\n    {\n        // RouteGroupBuilder for catalog endpoints\n        var vApi = app.NewVersionedApi(\"Catalog\");\n        var api = vApi.MapGroup(\"api/catalog\").HasApiVersion(1, 0).HasApiVersion(2, 0);\n        var v1 = vApi.MapGroup(\"api/catalog\").HasApiVersion(1, 0);\n        var v2 = vApi.MapGroup(\"api/catalog\").HasApiVersion(2, 0);\n\n        // Routes for querying catalog items.\n        v1.MapGet(\"/items\", GetAllItemsV1)\n            .WithName(\"ListItems\")\n            .WithSummary(\"List catalog items\")\n            .WithDescription(\"Get a paginated list of items in the catalog.\")\n            .WithTags(\"Items\");\n        v2.MapGet(\"/items\", GetAllItems)\n            .WithName(\"ListItems-V2\")\n            .WithSummary(\"List catalog items\")\n            .WithDescription(\"Get a paginated list of items in the catalog.\")\n            .WithTags(\"Items\");\n        api.MapGet(\"/items/by\", GetItemsByIds)\n            .WithName(\"BatchGetItems\")\n            .WithSummary(\"Batch get catalog items\")\n            .WithDescription(\"Get multiple items from the catalog\")\n            .WithTags(\"Items\");\n        api.MapGet(\"/items/{id:int}\", GetItemById)\n            .WithName(\"GetItem\")\n            .WithSummary(\"Get catalog item\")\n            .WithDescription(\"Get an item from the catalog\")\n            .WithTags(\"Items\");\n        v1.MapGet(\"/items/by/{name:minlength(1)}\", GetItemsByName)\n            .WithName(\"GetItemsByName\")\n            .WithSummary(\"Get catalog items by name\")\n            .WithDescription(\"Get a paginated list of catalog items with the specified name.\")\n            .WithTags(\"Items\");\n        api.MapGet(\"/items/{id:int}/pic\", GetItemPictureById)\n            .WithName(\"GetItemPicture\")\n            .WithSummary(\"Get catalog item picture\")\n            .WithDescription(\"Get the picture for a catalog item\")\n            .WithTags(\"Items\");\n\n        // Routes for resolving catalog items using AI.\n        v1.MapGet(\"/items/withsemanticrelevance/{text:minlength(1)}\", GetItemsBySemanticRelevanceV1)\n            .WithName(\"GetRelevantItems\")\n            .WithSummary(\"Search catalog for relevant items\")\n            .WithDescription(\"Search the catalog for items related to the specified text\")\n            .WithTags(\"Search\");\n\n                // Routes for resolving catalog items using AI.\n        v2.MapGet(\"/items/withsemanticrelevance\", GetItemsBySemanticRelevance)\n            .WithName(\"GetRelevantItems-V2\")\n            .WithSummary(\"Search catalog for relevant items\")\n            .WithDescription(\"Search the catalog for items related to the specified text\")\n            .WithTags(\"Search\");\n\n        // Routes for resolving catalog items by type and brand.\n        v1.MapGet(\"/items/type/{typeId}/brand/{brandId?}\", GetItemsByBrandAndTypeId)\n            .WithName(\"GetItemsByTypeAndBrand\")\n            .WithSummary(\"Get catalog items by type and brand\")\n            .WithDescription(\"Get catalog items of the specified type and brand\")\n            .WithTags(\"Types\");\n        v1.MapGet(\"/items/type/all/brand/{brandId:int?}\", GetItemsByBrandId)\n            .WithName(\"GetItemsByBrand\")\n            .WithSummary(\"List catalog items by brand\")\n            .WithDescription(\"Get a list of catalog items for the specified brand\")\n            .WithTags(\"Brands\");\n        api.MapGet(\"/catalogtypes\",\n            [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, \"application/problem+json\")]\n            async (CatalogContext context) => await context.CatalogTypes.OrderBy(x => x.Type).ToListAsync())\n            .WithName(\"ListItemTypes\")\n            .WithSummary(\"List catalog item types\")\n            .WithDescription(\"Get a list of the types of catalog items\")\n            .WithTags(\"Types\");\n        api.MapGet(\"/catalogbrands\",\n            [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, \"application/problem+json\")]\n            async (CatalogContext context) => await context.CatalogBrands.OrderBy(x => x.Brand).ToListAsync())\n            .WithName(\"ListItemBrands\")\n            .WithSummary(\"List catalog item brands\")\n            .WithDescription(\"Get a list of the brands of catalog items\")\n            .WithTags(\"Brands\");\n\n        // Routes for modifying catalog items.\n        v1.MapPut(\"/items\", UpdateItemV1)\n            .WithName(\"UpdateItem\")\n            .WithSummary(\"Create or replace a catalog item\")\n            .WithDescription(\"Create or replace a catalog item\")\n            .WithTags(\"Items\");\n        v2.MapPut(\"/items/{id:int}\", UpdateItem)\n            .WithName(\"UpdateItem-V2\")\n            .WithSummary(\"Create or replace a catalog item\")\n            .WithDescription(\"Create or replace a catalog item\")\n            .WithTags(\"Items\");\n        api.MapPost(\"/items\", CreateItem)\n            .WithName(\"CreateItem\")\n            .WithSummary(\"Create a catalog item\")\n            .WithDescription(\"Create a new item in the catalog\");\n        api.MapDelete(\"/items/{id:int}\", DeleteItemById)\n            .WithName(\"DeleteItem\")\n            .WithSummary(\"Delete catalog item\")\n            .WithDescription(\"Delete the specified catalog item\");\n\n        return app;\n    }\n\n    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, \"application/problem+json\")]\n    public static async Task<Ok<PaginatedItems<CatalogItem>>> GetAllItemsV1(\n        [AsParameters] PaginationRequest paginationRequest,\n        [AsParameters] CatalogServices services)\n    {\n        return await GetAllItems(paginationRequest, services, null, null, null);\n    }\n\n    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, \"application/problem+json\")]\n    public static async Task<Ok<PaginatedItems<CatalogItem>>> GetAllItems(\n        [AsParameters] PaginationRequest paginationRequest,\n        [AsParameters] CatalogServices services,\n        [Description(\"The name of the item to return\")] string? name,\n        [Description(\"The type of items to return\")] int? type,\n        [Description(\"The brand of items to return\")] int? brand)\n    {\n        var pageSize = paginationRequest.PageSize;\n        var pageIndex = paginationRequest.PageIndex;\n\n        var root = (IQueryable<CatalogItem>)services.Context.CatalogItems;\n\n        if (name is not null)\n        {\n            root = root.Where(c => c.Name.StartsWith(name));\n        }\n        if (type is not null)\n        {\n            root = root.Where(c => c.CatalogTypeId == type);\n        }\n        if (brand is not null)\n        {\n            root = root.Where(c => c.CatalogBrandId == brand);\n        }\n\n        var totalItems = await root\n            .LongCountAsync();\n\n        var itemsOnPage = await root\n            .OrderBy(c => c.Name)\n            .Skip(pageSize * pageIndex)\n            .Take(pageSize)\n            .ToListAsync();\n\n        return TypedResults.Ok(new PaginatedItems<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage));\n    }\n\n    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, \"application/problem+json\")]\n    public static async Task<Ok<List<CatalogItem>>> GetItemsByIds(\n        [AsParameters] CatalogServices services,\n        [Description(\"List of ids for catalog items to return\")] int[] ids)\n    {\n        var items = await services.Context.CatalogItems.Where(item => ids.Contains(item.Id)).ToListAsync();\n        return TypedResults.Ok(items);\n    }\n\n    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, \"application/problem+json\")]\n    public static async Task<Results<Ok<CatalogItem>, NotFound, BadRequest<ProblemDetails>>> GetItemById(\n        HttpContext httpContext,\n        [AsParameters] CatalogServices services,\n        [Description(\"The catalog item id\")] int id)\n    {\n        if (id <= 0)\n        {\n            return TypedResults.BadRequest<ProblemDetails>(new (){\n                Detail = \"Id is not valid\"\n            });\n        }\n\n        var item = await services.Context.CatalogItems.Include(ci => ci.CatalogBrand).SingleOrDefaultAsync(ci => ci.Id == id);\n\n        if (item == null)\n        {\n            return TypedResults.NotFound();\n        }\n\n        return TypedResults.Ok(item);\n    }\n\n    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, \"application/problem+json\")]\n    public static async Task<Ok<PaginatedItems<CatalogItem>>> GetItemsByName(\n        [AsParameters] PaginationRequest paginationRequest,\n        [AsParameters] CatalogServices services,\n        [Description(\"The name of the item to return\")] string name)\n    {\n        return await GetAllItems(paginationRequest, services, name, null, null);\n    }\n\n    [ProducesResponseType<byte[]>(StatusCodes.Status200OK, \"application/octet-stream\",\n        [ \"image/png\", \"image/gif\", \"image/jpeg\", \"image/bmp\", \"image/tiff\",\n          \"image/wmf\", \"image/jp2\", \"image/svg+xml\", \"image/webp\" ])]\n    public static async Task<Results<PhysicalFileHttpResult,NotFound>> GetItemPictureById(\n        CatalogContext context,\n        IWebHostEnvironment environment,\n        [Description(\"The catalog item id\")] int id)\n    {\n        var item = await context.CatalogItems.FindAsync(id);\n\n        if (item is null || item.PictureFileName is null)\n        {\n            return TypedResults.NotFound();\n        }\n\n        var path = GetFullPath(environment.ContentRootPath, item.PictureFileName);\n\n        string imageFileExtension = Path.GetExtension(item.PictureFileName) ?? string.Empty;\n        string mimetype = GetImageMimeTypeFromImageFileExtension(imageFileExtension);\n        DateTime lastModified = File.GetLastWriteTimeUtc(path);\n\n        return TypedResults.PhysicalFile(path, mimetype, lastModified: lastModified);\n    }\n\n    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, \"application/problem+json\")]\n    public static async Task<Results<Ok<PaginatedItems<CatalogItem>>, RedirectToRouteHttpResult>> GetItemsBySemanticRelevanceV1(\n        [AsParameters] PaginationRequest paginationRequest,\n        [AsParameters] CatalogServices services,\n        [Description(\"The text string to use when search for related items in the catalog\")] string text)\n\n    {\n        return await GetItemsBySemanticRelevance(paginationRequest, services, text);\n    }\n\n    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, \"application/problem+json\")]\n    public static async Task<Results<Ok<PaginatedItems<CatalogItem>>, RedirectToRouteHttpResult>> GetItemsBySemanticRelevance(\n        [AsParameters] PaginationRequest paginationRequest,\n        [AsParameters] CatalogServices services,\n        [Description(\"The text string to use when search for related items in the catalog\"), Required, MinLength(1)] string text)\n    {\n        var pageSize = paginationRequest.PageSize;\n        var pageIndex = paginationRequest.PageIndex;\n\n        if (!services.CatalogAI.IsEnabled)\n        {\n            return await GetItemsByName(paginationRequest, services, text);\n        }\n\n        // Create an embedding for the input search\n        var vector = await services.CatalogAI.GetEmbeddingAsync(text);\n\n        if (vector is null)\n        {\n            return await GetItemsByName(paginationRequest, services, text);\n        }\n\n        // Get the total number of items\n        var totalItems = await services.Context.CatalogItems\n            .LongCountAsync();\n\n        // Get the next page of items, ordered by most similar (smallest distance) to the input search\n        List<CatalogItem> itemsOnPage;\n        if (services.Logger.IsEnabled(LogLevel.Debug))\n        {\n            var itemsWithDistance = await services.Context.CatalogItems\n                .Where(c => c.Embedding != null)\n                .Select(c => new { Item = c, Distance = c.Embedding!.CosineDistance(vector) })\n                .OrderBy(c => c.Distance)\n                .Skip(pageSize * pageIndex)\n                .Take(pageSize)\n                .ToListAsync();\n\n            services.Logger.LogDebug(\"Results from {text}: {results}\", text, string.Join(\", \", itemsWithDistance.Select(i => $\"{i.Item.Name} => {i.Distance}\")));\n\n            itemsOnPage = itemsWithDistance.Select(i => i.Item).ToList();\n        }\n        else\n        {\n            itemsOnPage = await services.Context.CatalogItems\n                .Where(c => c.Embedding != null)\n                .OrderBy(c => c.Embedding!.CosineDistance(vector))\n                .Skip(pageSize * pageIndex)\n                .Take(pageSize)\n                .ToListAsync();\n        }\n\n        return TypedResults.Ok(new PaginatedItems<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage));\n    }\n\n    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, \"application/problem+json\")]\n    public static async Task<Ok<PaginatedItems<CatalogItem>>> GetItemsByBrandAndTypeId(\n        [AsParameters] PaginationRequest paginationRequest,\n        [AsParameters] CatalogServices services,\n        [Description(\"The type of items to return\")] int typeId,\n        [Description(\"The brand of items to return\")] int? brandId)\n    {\n        return await GetAllItems(paginationRequest, services, null, typeId, brandId);\n    }\n\n    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, \"application/problem+json\")]\n    public static async Task<Ok<PaginatedItems<CatalogItem>>> GetItemsByBrandId(\n        [AsParameters] PaginationRequest paginationRequest,\n        [AsParameters] CatalogServices services,\n        [Description(\"The brand of items to return\")] int? brandId)\n    {\n        return await GetAllItems(paginationRequest, services, null, null, brandId);\n    }\n\n    public static async Task<Results<Created, BadRequest<ProblemDetails>, NotFound<ProblemDetails>>> UpdateItemV1(\n        HttpContext httpContext,\n        [AsParameters] CatalogServices services,\n        CatalogItem productToUpdate)\n    {\n        if (productToUpdate?.Id == null)\n        {\n            return TypedResults.BadRequest<ProblemDetails>(new (){\n                Detail = \"Item id must be provided in the request body.\"\n            });\n        }\n        return await UpdateItem(httpContext, productToUpdate.Id, services, productToUpdate);\n    }\n\n    public static async Task<Results<Created, BadRequest<ProblemDetails>, NotFound<ProblemDetails>>> UpdateItem(\n        HttpContext httpContext,\n        [Description(\"The id of the catalog item to delete\")] int id,\n        [AsParameters] CatalogServices services,\n        CatalogItem productToUpdate)\n    {\n        var catalogItem = await services.Context.CatalogItems.SingleOrDefaultAsync(i => i.Id == id);\n\n        if (catalogItem == null)\n        {\n            return TypedResults.NotFound<ProblemDetails>(new (){\n                Detail = $\"Item with id {id} not found.\"\n            });\n        }\n\n        // Update current product\n        var catalogEntry = services.Context.Entry(catalogItem);\n        catalogEntry.CurrentValues.SetValues(productToUpdate);\n\n        catalogItem.Embedding = await services.CatalogAI.GetEmbeddingAsync(catalogItem);\n\n        var priceEntry = catalogEntry.Property(i => i.Price);\n\n        if (priceEntry.IsModified) // Save product's data and publish integration event through the Event Bus if price has changed\n        {\n            //Create Integration Event to be published through the Event Bus\n            var priceChangedEvent = new ProductPriceChangedIntegrationEvent(catalogItem.Id, productToUpdate.Price, priceEntry.OriginalValue);\n\n            // Achieving atomicity between original Catalog database operation and the IntegrationEventLog thanks to a local transaction\n            await services.EventService.SaveEventAndCatalogContextChangesAsync(priceChangedEvent);\n\n            // Publish through the Event Bus and mark the saved event as published\n            await services.EventService.PublishThroughEventBusAsync(priceChangedEvent);\n        }\n        else // Just save the updated product because the Product's Price hasn't changed.\n        {\n            await services.Context.SaveChangesAsync();\n        }\n        return TypedResults.Created($\"/api/catalog/items/{id}\");\n    }\n\n    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, \"application/problem+json\")]\n    public static async Task<Created> CreateItem(\n        [AsParameters] CatalogServices services,\n        CatalogItem product)\n    {\n        var item = new CatalogItem(product.Name)\n        {\n            Id = product.Id,\n            CatalogBrandId = product.CatalogBrandId,\n            CatalogTypeId = product.CatalogTypeId,\n            Description = product.Description,\n            PictureFileName = product.PictureFileName,\n            Price = product.Price,\n            AvailableStock = product.AvailableStock,\n            RestockThreshold = product.RestockThreshold,\n            MaxStockThreshold = product.MaxStockThreshold\n        };\n        item.Embedding = await services.CatalogAI.GetEmbeddingAsync(item);\n\n        services.Context.CatalogItems.Add(item);\n        await services.Context.SaveChangesAsync();\n\n        return TypedResults.Created($\"/api/catalog/items/{item.Id}\");\n    }\n\n    public static async Task<Results<NoContent, NotFound>> DeleteItemById(\n        [AsParameters] CatalogServices services,\n        [Description(\"The id of the catalog item to delete\")] int id)\n    {\n        var item = services.Context.CatalogItems.SingleOrDefault(x => x.Id == id);\n\n        if (item is null)\n        {\n            return TypedResults.NotFound();\n        }\n\n        services.Context.CatalogItems.Remove(item);\n        await services.Context.SaveChangesAsync();\n        return TypedResults.NoContent();\n    }\n\n    private static string GetImageMimeTypeFromImageFileExtension(string extension) => extension switch\n    {\n        \".png\" => \"image/png\",\n        \".gif\" => \"image/gif\",\n        \".jpg\" or \".jpeg\" => \"image/jpeg\",\n        \".bmp\" => \"image/bmp\",\n        \".tiff\" => \"image/tiff\",\n        \".wmf\" => \"image/wmf\",\n        \".jp2\" => \"image/jp2\",\n        \".svg\" => \"image/svg+xml\",\n        \".webp\" => \"image/webp\",\n        _ => \"application/octet-stream\",\n    };\n\n    public static string GetFullPath(string contentRootPath, string pictureFileName) =>\n        Path.Combine(contentRootPath, \"Pics\", pictureFileName);\n}\n"
  },
  {
    "path": "src/Catalog.API/Catalog.API.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <Nullable>enable</Nullable>\n    <UserSecretsId>d1b521ec-3411-4d39-98c6-8509466ed471</UserSecretsId>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <Compile Include=\"..\\Shared\\ActivityExtensions.cs\" Link=\"Extensions\\ActivityExtensions.cs\" />\n    <Compile Include=\"..\\Shared\\MigrateDbContextExtensions.cs\" Link=\"Extensions\\MigrateDbContextExtensions.cs\" />\n  </ItemGroup>\n\n  <PropertyGroup>\n    <OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)</OpenApiDocumentsDirectory>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Asp.Versioning.Http\" />\n    <PackageReference Include=\"Aspire.Npgsql.EntityFrameworkCore.PostgreSQL\" />\n    <PackageReference Include=\"CommunityToolkit.Aspire.OllamaSharp\" />\n    <PackageReference Include=\"Microsoft.EntityFrameworkCore.Tools\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <PackageReference Include=\"Microsoft.Extensions.ApiDescription.Server\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n  </ItemGroup>\n\n  <!-- AI -->\n  <ItemGroup>\n    <PackageReference Include=\"Aspire.Azure.AI.OpenAI\" />\n    <PackageReference Include=\"Pgvector\" />\n    <PackageReference Include=\"Pgvector.EntityFrameworkCore\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\EventBusRabbitMQ\\EventBusRabbitMQ.csproj\" />\n    <ProjectReference Include=\"..\\IntegrationEventLogEF\\IntegrationEventLogEF.csproj\" />\n    <ProjectReference Include=\"..\\eShop.ServiceDefaults\\eShop.ServiceDefaults.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Content Include=\"Pics\\*.webp\" CopyToOutputDirectory=\"PreserveNewest\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <InternalsVisibleTo Include=\"Catalog.FunctionalTests\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Catalog.API/Catalog.API.http",
    "content": "﻿@Catalog.API_HostAddress = http://localhost:5222\n@ApiVersion = 1.0\n\nGET {{Catalog.API_HostAddress}}/openapi/v1.json\n\n###\n\nGET {{Catalog.API_HostAddress}}/api/catalog/items?api-version={{ApiVersion}}\n\n###\n\nGET {{Catalog.API_HostAddress}}/api/catalog/items/type/1/brand/2?api-version={{ApiVersion}}\n\n###\n\n# A request with an unknown API version returns a 400 ProblemDetails response\n\nGET {{Catalog.API_HostAddress}}/api/catalog/items/463/pic?api-version=99\n\n###\n\n# A request with an unknown item id returns a 404 NotFound with empty response body\n\nGET {{Catalog.API_HostAddress}}/api/catalog/items/463/pic?api-version={{ApiVersion}}\n\n###\n\nPUT {{Catalog.API_HostAddress}}/api/catalog/items?api-version={{ApiVersion}}\ncontent-type: application/json\n\n{\n  \"id\": 999,\n  \"name\": \"Item1\",\n  \"price\": 100,\n  \"description\": \"Description1\",\n  \"pictureFileName\": \"item1.png\",\n  \"catalogTypeId\": 1,\n  \"catalogBrandId\": 2\n}\n"
  },
  {
    "path": "src/Catalog.API/Catalog.API.json",
    "content": "{\n  \"openapi\": \"3.1.1\",\n  \"info\": {\n    \"title\": \"eShop - Catalog HTTP API\",\n    \"description\": \"The Catalog Microservice HTTP API. This is a Data-Driven/CRUD microservice sample\",\n    \"version\": \"1.0\"\n  },\n  \"paths\": {\n    \"/api/catalog/items/by\": {\n      \"get\": {\n        \"tags\": [\n          \"Items\"\n        ],\n        \"summary\": \"Batch get catalog items\",\n        \"description\": \"Get multiple items from the catalog\",\n        \"operationId\": \"BatchGetItems\",\n        \"parameters\": [\n          {\n            \"name\": \"ids\",\n            \"in\": \"query\",\n            \"description\": \"List of ids for catalog items to return\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n                \"type\": [\n                  \"integer\",\n                  \"string\"\n                ],\n                \"format\": \"int32\"\n              }\n            }\n          },\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"1.0\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/CatalogItem\"\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\",\n            \"content\": {\n              \"application/problem+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProblemDetails\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/catalog/items/{id}\": {\n      \"get\": {\n        \"tags\": [\n          \"Items\"\n        ],\n        \"summary\": \"Get catalog item\",\n        \"description\": \"Get an item from the catalog\",\n        \"operationId\": \"GetItem\",\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"description\": \"The catalog item id\",\n            \"required\": true,\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"1.0\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/CatalogItem\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Not Found\"\n          },\n          \"400\": {\n            \"description\": \"Bad Request\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProblemDetails\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"tags\": [\n          \"Catalog\"\n        ],\n        \"summary\": \"Delete catalog item\",\n        \"description\": \"Delete the specified catalog item\",\n        \"operationId\": \"DeleteItem\",\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"description\": \"The id of the catalog item to delete\",\n            \"required\": true,\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"1.0\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"204\": {\n            \"description\": \"No Content\"\n          },\n          \"404\": {\n            \"description\": \"Not Found\"\n          }\n        }\n      }\n    },\n    \"/api/catalog/items/{id}/pic\": {\n      \"get\": {\n        \"tags\": [\n          \"Items\"\n        ],\n        \"summary\": \"Get catalog item picture\",\n        \"description\": \"Get the picture for a catalog item\",\n        \"operationId\": \"GetItemPicture\",\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"description\": \"The catalog item id\",\n            \"required\": true,\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"1.0\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"404\": {\n            \"description\": \"Not Found\"\n          },\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/octet-stream\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"byte\"\n                }\n              },\n              \"image/png\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"byte\"\n                }\n              },\n              \"image/gif\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"byte\"\n                }\n              },\n              \"image/jpeg\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"byte\"\n                }\n              },\n              \"image/bmp\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"byte\"\n                }\n              },\n              \"image/tiff\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"byte\"\n                }\n              },\n              \"image/wmf\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"byte\"\n                }\n              },\n              \"image/jp2\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"byte\"\n                }\n              },\n              \"image/svg+xml\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"byte\"\n                }\n              },\n              \"image/webp\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"byte\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/catalog/catalogtypes\": {\n      \"get\": {\n        \"tags\": [\n          \"Types\"\n        ],\n        \"summary\": \"List catalog item types\",\n        \"description\": \"Get a list of the types of catalog items\",\n        \"operationId\": \"ListItemTypes\",\n        \"parameters\": [\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"1.0\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/CatalogType\"\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\",\n            \"content\": {\n              \"application/problem+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProblemDetails\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/catalog/catalogbrands\": {\n      \"get\": {\n        \"tags\": [\n          \"Brands\"\n        ],\n        \"summary\": \"List catalog item brands\",\n        \"description\": \"Get a list of the brands of catalog items\",\n        \"operationId\": \"ListItemBrands\",\n        \"parameters\": [\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"1.0\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/CatalogBrand\"\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\",\n            \"content\": {\n              \"application/problem+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProblemDetails\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/catalog/items\": {\n      \"post\": {\n        \"tags\": [\n          \"Catalog\"\n        ],\n        \"summary\": \"Create a catalog item\",\n        \"description\": \"Create a new item in the catalog\",\n        \"operationId\": \"CreateItem\",\n        \"parameters\": [\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"1.0\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/CatalogItem\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"201\": {\n            \"description\": \"Created\"\n          },\n          \"400\": {\n            \"description\": \"Bad Request\",\n            \"content\": {\n              \"application/problem+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProblemDetails\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"get\": {\n        \"tags\": [\n          \"Items\"\n        ],\n        \"summary\": \"List catalog items\",\n        \"description\": \"Get a paginated list of items in the catalog.\",\n        \"operationId\": \"ListItems\",\n        \"parameters\": [\n          {\n            \"name\": \"PageSize\",\n            \"in\": \"query\",\n            \"description\": \"Number of items to return in a single page of results\",\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": [\n                \"integer\",\n                \"string\"\n              ],\n              \"format\": \"int32\",\n              \"default\": 10\n            }\n          },\n          {\n            \"name\": \"PageIndex\",\n            \"in\": \"query\",\n            \"description\": \"The index of the page of results to return\",\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": [\n                \"integer\",\n                \"string\"\n              ],\n              \"format\": \"int32\",\n              \"default\": 0\n            }\n          },\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"1.0\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PaginatedItemsOfCatalogItem\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\",\n            \"content\": {\n              \"application/problem+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProblemDetails\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"put\": {\n        \"tags\": [\n          \"Items\"\n        ],\n        \"summary\": \"Create or replace a catalog item\",\n        \"description\": \"Create or replace a catalog item\",\n        \"operationId\": \"UpdateItem\",\n        \"parameters\": [\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"1.0\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/CatalogItem\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"201\": {\n            \"description\": \"Created\"\n          },\n          \"400\": {\n            \"description\": \"Bad Request\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProblemDetails\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Not Found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProblemDetails\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/catalog/items/by/{name}\": {\n      \"get\": {\n        \"tags\": [\n          \"Items\"\n        ],\n        \"summary\": \"Get catalog items by name\",\n        \"description\": \"Get a paginated list of catalog items with the specified name.\",\n        \"operationId\": \"GetItemsByName\",\n        \"parameters\": [\n          {\n            \"name\": \"PageSize\",\n            \"in\": \"query\",\n            \"description\": \"Number of items to return in a single page of results\",\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": [\n                \"integer\",\n                \"string\"\n              ],\n              \"format\": \"int32\",\n              \"default\": 10\n            }\n          },\n          {\n            \"name\": \"PageIndex\",\n            \"in\": \"query\",\n            \"description\": \"The index of the page of results to return\",\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": [\n                \"integer\",\n                \"string\"\n              ],\n              \"format\": \"int32\",\n              \"default\": 0\n            }\n          },\n          {\n            \"name\": \"name\",\n            \"in\": \"path\",\n            \"description\": \"The name of the item to return\",\n            \"required\": true,\n            \"schema\": {\n              \"minLength\": 1,\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"1.0\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PaginatedItemsOfCatalogItem\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\",\n            \"content\": {\n              \"application/problem+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProblemDetails\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/catalog/items/withsemanticrelevance/{text}\": {\n      \"get\": {\n        \"tags\": [\n          \"Search\"\n        ],\n        \"summary\": \"Search catalog for relevant items\",\n        \"description\": \"Search the catalog for items related to the specified text\",\n        \"operationId\": \"GetRelevantItems\",\n        \"parameters\": [\n          {\n            \"name\": \"PageSize\",\n            \"in\": \"query\",\n            \"description\": \"Number of items to return in a single page of results\",\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": [\n                \"integer\",\n                \"string\"\n              ],\n              \"format\": \"int32\",\n              \"default\": 10\n            }\n          },\n          {\n            \"name\": \"PageIndex\",\n            \"in\": \"query\",\n            \"description\": \"The index of the page of results to return\",\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": [\n                \"integer\",\n                \"string\"\n              ],\n              \"format\": \"int32\",\n              \"default\": 0\n            }\n          },\n          {\n            \"name\": \"text\",\n            \"in\": \"path\",\n            \"description\": \"The text string to use when search for related items in the catalog\",\n            \"required\": true,\n            \"schema\": {\n              \"minLength\": 1,\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"1.0\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PaginatedItemsOfCatalogItem\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\",\n            \"content\": {\n              \"application/problem+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProblemDetails\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/catalog/items/type/{typeId}/brand/{brandId}\": {\n      \"get\": {\n        \"tags\": [\n          \"Types\"\n        ],\n        \"summary\": \"Get catalog items by type and brand\",\n        \"description\": \"Get catalog items of the specified type and brand\",\n        \"operationId\": \"GetItemsByTypeAndBrand\",\n        \"parameters\": [\n          {\n            \"name\": \"PageSize\",\n            \"in\": \"query\",\n            \"description\": \"Number of items to return in a single page of results\",\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": [\n                \"integer\",\n                \"string\"\n              ],\n              \"format\": \"int32\",\n              \"default\": 10\n            }\n          },\n          {\n            \"name\": \"PageIndex\",\n            \"in\": \"query\",\n            \"description\": \"The index of the page of results to return\",\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": [\n                \"integer\",\n                \"string\"\n              ],\n              \"format\": \"int32\",\n              \"default\": 0\n            }\n          },\n          {\n            \"name\": \"typeId\",\n            \"in\": \"path\",\n            \"description\": \"The type of items to return\",\n            \"required\": true,\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": [\n                \"integer\",\n                \"string\"\n              ],\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"name\": \"brandId\",\n            \"in\": \"path\",\n            \"description\": \"The brand of items to return\",\n            \"required\": true,\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": [\n                \"integer\",\n                \"string\"\n              ],\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"1.0\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PaginatedItemsOfCatalogItem\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\",\n            \"content\": {\n              \"application/problem+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProblemDetails\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/catalog/items/type/all/brand/{brandId}\": {\n      \"get\": {\n        \"tags\": [\n          \"Brands\"\n        ],\n        \"summary\": \"List catalog items by brand\",\n        \"description\": \"Get a list of catalog items for the specified brand\",\n        \"operationId\": \"GetItemsByBrand\",\n        \"parameters\": [\n          {\n            \"name\": \"PageSize\",\n            \"in\": \"query\",\n            \"description\": \"Number of items to return in a single page of results\",\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": [\n                \"integer\",\n                \"string\"\n              ],\n              \"format\": \"int32\",\n              \"default\": 10\n            }\n          },\n          {\n            \"name\": \"PageIndex\",\n            \"in\": \"query\",\n            \"description\": \"The index of the page of results to return\",\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": [\n                \"integer\",\n                \"string\"\n              ],\n              \"format\": \"int32\",\n              \"default\": 0\n            }\n          },\n          {\n            \"name\": \"brandId\",\n            \"in\": \"path\",\n            \"description\": \"The brand of items to return\",\n            \"required\": true,\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": [\n                \"integer\",\n                \"string\"\n              ],\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"1.0\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PaginatedItemsOfCatalogItem\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\",\n            \"content\": {\n              \"application/problem+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProblemDetails\"\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"schemas\": {\n      \"CatalogBrand\": {\n        \"required\": [\n          \"brand\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"brand\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"CatalogItem\": {\n        \"required\": [\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": [\n              \"null\",\n              \"string\"\n            ]\n          },\n          \"price\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)(?:\\\\.\\\\d+)?$\",\n            \"type\": [\n              \"number\",\n              \"string\"\n            ],\n            \"format\": \"double\"\n          },\n          \"pictureFileName\": {\n            \"type\": [\n              \"null\",\n              \"string\"\n            ]\n          },\n          \"catalogTypeId\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"catalogType\": {\n            \"oneOf\": [\n              {\n                \"type\": \"null\"\n              },\n              {\n                \"$ref\": \"#/components/schemas/CatalogType\"\n              }\n            ]\n          },\n          \"catalogBrandId\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"catalogBrand\": {\n            \"oneOf\": [\n              {\n                \"type\": \"null\"\n              },\n              {\n                \"$ref\": \"#/components/schemas/CatalogBrand\"\n              }\n            ]\n          },\n          \"availableStock\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"restockThreshold\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"maxStockThreshold\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"onReorder\": {\n            \"type\": \"boolean\"\n          }\n        }\n      },\n      \"CatalogType\": {\n        \"required\": [\n          \"type\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"type\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"PaginatedItemsOfCatalogItem\": {\n        \"required\": [\n          \"pageIndex\",\n          \"pageSize\",\n          \"count\",\n          \"data\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"pageIndex\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"pageSize\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"count\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int64\"\n          },\n          \"data\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/CatalogItem\"\n            }\n          }\n        }\n      },\n      \"ProblemDetails\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"type\": {\n            \"type\": [\n              \"null\",\n              \"string\"\n            ]\n          },\n          \"title\": {\n            \"type\": [\n              \"null\",\n              \"string\"\n            ]\n          },\n          \"status\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"null\",\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"detail\": {\n            \"type\": [\n              \"null\",\n              \"string\"\n            ]\n          },\n          \"instance\": {\n            \"type\": [\n              \"null\",\n              \"string\"\n            ]\n          }\n        }\n      }\n    }\n  },\n  \"tags\": [\n    {\n      \"name\": \"Items\"\n    },\n    {\n      \"name\": \"Catalog\"\n    },\n    {\n      \"name\": \"Types\"\n    },\n    {\n      \"name\": \"Brands\"\n    },\n    {\n      \"name\": \"Search\"\n    }\n  ]\n}"
  },
  {
    "path": "src/Catalog.API/Catalog.API_v2.json",
    "content": "{\n  \"openapi\": \"3.1.1\",\n  \"info\": {\n    \"title\": \"eShop - Catalog HTTP API\",\n    \"description\": \"The Catalog Microservice HTTP API. This is a Data-Driven/CRUD microservice sample\",\n    \"version\": \"2.0\"\n  },\n  \"paths\": {\n    \"/api/catalog/items/by\": {\n      \"get\": {\n        \"tags\": [\n          \"Items\"\n        ],\n        \"summary\": \"Batch get catalog items\",\n        \"description\": \"Get multiple items from the catalog\",\n        \"operationId\": \"BatchGetItems\",\n        \"parameters\": [\n          {\n            \"name\": \"ids\",\n            \"in\": \"query\",\n            \"description\": \"List of ids for catalog items to return\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n                \"type\": [\n                  \"integer\",\n                  \"string\"\n                ],\n                \"format\": \"int32\"\n              }\n            }\n          },\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"2.0\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/CatalogItem\"\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\",\n            \"content\": {\n              \"application/problem+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProblemDetails\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/catalog/items/{id}\": {\n      \"get\": {\n        \"tags\": [\n          \"Items\"\n        ],\n        \"summary\": \"Get catalog item\",\n        \"description\": \"Get an item from the catalog\",\n        \"operationId\": \"GetItem\",\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"description\": \"The catalog item id\",\n            \"required\": true,\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"2.0\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/CatalogItem\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Not Found\"\n          },\n          \"400\": {\n            \"description\": \"Bad Request\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProblemDetails\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"tags\": [\n          \"Catalog\"\n        ],\n        \"summary\": \"Delete catalog item\",\n        \"description\": \"Delete the specified catalog item\",\n        \"operationId\": \"DeleteItem\",\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"description\": \"The id of the catalog item to delete\",\n            \"required\": true,\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"2.0\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"204\": {\n            \"description\": \"No Content\"\n          },\n          \"404\": {\n            \"description\": \"Not Found\"\n          }\n        }\n      },\n      \"put\": {\n        \"tags\": [\n          \"Items\"\n        ],\n        \"summary\": \"Create or replace a catalog item\",\n        \"description\": \"Create or replace a catalog item\",\n        \"operationId\": \"UpdateItem-V2\",\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"description\": \"The id of the catalog item to delete\",\n            \"required\": true,\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"2.0\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/CatalogItem\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"201\": {\n            \"description\": \"Created\"\n          },\n          \"400\": {\n            \"description\": \"Bad Request\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProblemDetails\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Not Found\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProblemDetails\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/catalog/items/{id}/pic\": {\n      \"get\": {\n        \"tags\": [\n          \"Items\"\n        ],\n        \"summary\": \"Get catalog item picture\",\n        \"description\": \"Get the picture for a catalog item\",\n        \"operationId\": \"GetItemPicture\",\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"description\": \"The catalog item id\",\n            \"required\": true,\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": \"integer\",\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"2.0\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"404\": {\n            \"description\": \"Not Found\"\n          },\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/octet-stream\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"byte\"\n                }\n              },\n              \"image/png\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"byte\"\n                }\n              },\n              \"image/gif\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"byte\"\n                }\n              },\n              \"image/jpeg\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"byte\"\n                }\n              },\n              \"image/bmp\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"byte\"\n                }\n              },\n              \"image/tiff\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"byte\"\n                }\n              },\n              \"image/wmf\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"byte\"\n                }\n              },\n              \"image/jp2\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"byte\"\n                }\n              },\n              \"image/svg+xml\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"byte\"\n                }\n              },\n              \"image/webp\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"format\": \"byte\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/catalog/catalogtypes\": {\n      \"get\": {\n        \"tags\": [\n          \"Types\"\n        ],\n        \"summary\": \"List catalog item types\",\n        \"description\": \"Get a list of the types of catalog items\",\n        \"operationId\": \"ListItemTypes\",\n        \"parameters\": [\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"2.0\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/CatalogType\"\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\",\n            \"content\": {\n              \"application/problem+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProblemDetails\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/catalog/catalogbrands\": {\n      \"get\": {\n        \"tags\": [\n          \"Brands\"\n        ],\n        \"summary\": \"List catalog item brands\",\n        \"description\": \"Get a list of the brands of catalog items\",\n        \"operationId\": \"ListItemBrands\",\n        \"parameters\": [\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"2.0\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/CatalogBrand\"\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\",\n            \"content\": {\n              \"application/problem+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProblemDetails\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/catalog/items\": {\n      \"post\": {\n        \"tags\": [\n          \"Catalog\"\n        ],\n        \"summary\": \"Create a catalog item\",\n        \"description\": \"Create a new item in the catalog\",\n        \"operationId\": \"CreateItem\",\n        \"parameters\": [\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"2.0\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/CatalogItem\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"201\": {\n            \"description\": \"Created\"\n          },\n          \"400\": {\n            \"description\": \"Bad Request\",\n            \"content\": {\n              \"application/problem+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProblemDetails\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"get\": {\n        \"tags\": [\n          \"Items\"\n        ],\n        \"summary\": \"List catalog items\",\n        \"description\": \"Get a paginated list of items in the catalog.\",\n        \"operationId\": \"ListItems-V2\",\n        \"parameters\": [\n          {\n            \"name\": \"PageSize\",\n            \"in\": \"query\",\n            \"description\": \"Number of items to return in a single page of results\",\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": [\n                \"integer\",\n                \"string\"\n              ],\n              \"format\": \"int32\",\n              \"default\": 10\n            }\n          },\n          {\n            \"name\": \"PageIndex\",\n            \"in\": \"query\",\n            \"description\": \"The index of the page of results to return\",\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": [\n                \"integer\",\n                \"string\"\n              ],\n              \"format\": \"int32\",\n              \"default\": 0\n            }\n          },\n          {\n            \"name\": \"name\",\n            \"in\": \"query\",\n            \"description\": \"The name of the item to return\",\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"name\": \"type\",\n            \"in\": \"query\",\n            \"description\": \"The type of items to return\",\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": [\n                \"integer\",\n                \"string\"\n              ],\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"name\": \"brand\",\n            \"in\": \"query\",\n            \"description\": \"The brand of items to return\",\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": [\n                \"integer\",\n                \"string\"\n              ],\n              \"format\": \"int32\"\n            }\n          },\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"2.0\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PaginatedItemsOfCatalogItem\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\",\n            \"content\": {\n              \"application/problem+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProblemDetails\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/api/catalog/items/withsemanticrelevance\": {\n      \"get\": {\n        \"tags\": [\n          \"Search\"\n        ],\n        \"summary\": \"Search catalog for relevant items\",\n        \"description\": \"Search the catalog for items related to the specified text\",\n        \"operationId\": \"GetRelevantItems-V2\",\n        \"parameters\": [\n          {\n            \"name\": \"PageSize\",\n            \"in\": \"query\",\n            \"description\": \"Number of items to return in a single page of results\",\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": [\n                \"integer\",\n                \"string\"\n              ],\n              \"format\": \"int32\",\n              \"default\": 10\n            }\n          },\n          {\n            \"name\": \"PageIndex\",\n            \"in\": \"query\",\n            \"description\": \"The index of the page of results to return\",\n            \"schema\": {\n              \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n              \"type\": [\n                \"integer\",\n                \"string\"\n              ],\n              \"format\": \"int32\",\n              \"default\": 0\n            }\n          },\n          {\n            \"name\": \"text\",\n            \"in\": \"query\",\n            \"description\": \"The text string to use when search for related items in the catalog\",\n            \"required\": true,\n            \"schema\": {\n              \"minLength\": 1,\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"name\": \"api-version\",\n            \"in\": \"query\",\n            \"description\": \"The API version, in the format 'major.minor'.\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"example\": \"2.0\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PaginatedItemsOfCatalogItem\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\",\n            \"content\": {\n              \"application/problem+json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProblemDetails\"\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"schemas\": {\n      \"CatalogBrand\": {\n        \"required\": [\n          \"brand\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"brand\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"CatalogItem\": {\n        \"required\": [\n          \"name\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": [\n              \"null\",\n              \"string\"\n            ]\n          },\n          \"price\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)(?:\\\\.\\\\d+)?$\",\n            \"type\": [\n              \"number\",\n              \"string\"\n            ],\n            \"format\": \"double\"\n          },\n          \"pictureFileName\": {\n            \"type\": [\n              \"null\",\n              \"string\"\n            ]\n          },\n          \"catalogTypeId\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"catalogType\": {\n            \"oneOf\": [\n              {\n                \"type\": \"null\"\n              },\n              {\n                \"$ref\": \"#/components/schemas/CatalogType\"\n              }\n            ]\n          },\n          \"catalogBrandId\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"catalogBrand\": {\n            \"oneOf\": [\n              {\n                \"type\": \"null\"\n              },\n              {\n                \"$ref\": \"#/components/schemas/CatalogBrand\"\n              }\n            ]\n          },\n          \"availableStock\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"restockThreshold\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"maxStockThreshold\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"onReorder\": {\n            \"type\": \"boolean\"\n          }\n        }\n      },\n      \"CatalogType\": {\n        \"required\": [\n          \"type\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"type\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"PaginatedItemsOfCatalogItem\": {\n        \"required\": [\n          \"pageIndex\",\n          \"pageSize\",\n          \"count\",\n          \"data\"\n        ],\n        \"type\": \"object\",\n        \"properties\": {\n          \"pageIndex\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"pageSize\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"count\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int64\"\n          },\n          \"data\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/CatalogItem\"\n            }\n          }\n        }\n      },\n      \"ProblemDetails\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"type\": {\n            \"type\": [\n              \"null\",\n              \"string\"\n            ]\n          },\n          \"title\": {\n            \"type\": [\n              \"null\",\n              \"string\"\n            ]\n          },\n          \"status\": {\n            \"pattern\": \"^-?(?:0|[1-9]\\\\d*)$\",\n            \"type\": [\n              \"null\",\n              \"integer\",\n              \"string\"\n            ],\n            \"format\": \"int32\"\n          },\n          \"detail\": {\n            \"type\": [\n              \"null\",\n              \"string\"\n            ]\n          },\n          \"instance\": {\n            \"type\": [\n              \"null\",\n              \"string\"\n            ]\n          }\n        }\n      }\n    }\n  },\n  \"tags\": [\n    {\n      \"name\": \"Items\"\n    },\n    {\n      \"name\": \"Catalog\"\n    },\n    {\n      \"name\": \"Types\"\n    },\n    {\n      \"name\": \"Brands\"\n    },\n    {\n      \"name\": \"Search\"\n    }\n  ]\n}"
  },
  {
    "path": "src/Catalog.API/CatalogOptions.cs",
    "content": "﻿namespace eShop.Catalog.API;\n\npublic class CatalogOptions\n{\n    public string? PicBaseUrl { get; set; }\n    public bool UseCustomizationData { get; set; }\n}\n"
  },
  {
    "path": "src/Catalog.API/Extensions/Extensions.cs",
    "content": "﻿using eShop.Catalog.API.Services;\n\npublic static class Extensions\n{\n    public static void AddApplicationServices(this IHostApplicationBuilder builder)\n    {\n        // Avoid loading full database config and migrations if startup\n        // is being invoked from build-time OpenAPI generation\n        if (builder.Environment.IsBuild())\n        {\n            builder.Services.AddDbContext<CatalogContext>();\n            return;\n        }\n\n        builder.AddNpgsqlDbContext<CatalogContext>(\"catalogdb\", configureDbContextOptions: dbContextOptionsBuilder =>\n        {\n            dbContextOptionsBuilder.UseNpgsql(builder =>\n            {\n                builder.UseVector();\n            });\n        });\n\n        // REVIEW: This is done for development ease but shouldn't be here in production\n        builder.Services.AddMigration<CatalogContext, CatalogContextSeed>();\n\n        // Add the integration services that consume the DbContext\n        builder.Services.AddTransient<IIntegrationEventLogService, IntegrationEventLogService<CatalogContext>>();\n\n        builder.Services.AddTransient<ICatalogIntegrationEventService, CatalogIntegrationEventService>();\n\n        builder.AddRabbitMqEventBus(\"eventbus\")\n               .AddSubscription<OrderStatusChangedToAwaitingValidationIntegrationEvent, OrderStatusChangedToAwaitingValidationIntegrationEventHandler>()\n               .AddSubscription<OrderStatusChangedToPaidIntegrationEvent, OrderStatusChangedToPaidIntegrationEventHandler>();\n\n        builder.Services.AddOptions<CatalogOptions>()\n            .BindConfiguration(nameof(CatalogOptions));\n\n        if (builder.Configuration[\"OllamaEnabled\"] is string ollamaEnabled && bool.Parse(ollamaEnabled))\n        {\n            builder.AddOllamaApiClient(\"embedding\")\n                .AddEmbeddingGenerator();\n        }\n        else if (!string.IsNullOrWhiteSpace(builder.Configuration.GetConnectionString(\"textEmbeddingModel\")))\n        {\n            builder.AddOpenAIClientFromConfiguration(\"textEmbeddingModel\")\n                .AddEmbeddingGenerator();\n        }\n\n        builder.Services.AddScoped<ICatalogAI, CatalogAI>();\n    }\n}\n"
  },
  {
    "path": "src/Catalog.API/Extensions/HostEnvironmentExtensions.cs",
    "content": "using System.Reflection;\n\nnamespace Microsoft.Extensions.Hosting;\n\ninternal static class HostEnvironmentExtensions\n{\n    public static bool IsBuild(this IHostEnvironment hostEnvironment)\n    {\n        // Check if the environment is \"Build\" or the entry assembly is \"GetDocument.Insider\"\n        // to account for scenarios where app is launching via OpenAPI build-time generation\n        // via the GetDocument.Insider tool.\n        return hostEnvironment.IsEnvironment(\"Build\") || Assembly.GetEntryAssembly()?.GetName().Name == \"GetDocument.Insider\";\n    }\n}\n"
  },
  {
    "path": "src/Catalog.API/GlobalUsings.cs",
    "content": "﻿global using Asp.Versioning;\nglobal using Asp.Versioning.Conventions;\nglobal using eShop.Catalog.API;\nglobal using eShop.Catalog.API.Infrastructure;\nglobal using eShop.Catalog.API.Infrastructure.EntityConfigurations;\nglobal using eShop.Catalog.API.Infrastructure.Exceptions;\nglobal using eShop.Catalog.API.IntegrationEvents;\nglobal using eShop.Catalog.API.IntegrationEvents.EventHandling;\nglobal using eShop.Catalog.API.IntegrationEvents.Events;\nglobal using eShop.Catalog.API.Model;\nglobal using eShop.EventBus.Abstractions;\nglobal using eShop.EventBus.Events;\nglobal using eShop.IntegrationEventLogEF;\nglobal using eShop.IntegrationEventLogEF.Services;\nglobal using eShop.IntegrationEventLogEF.Utilities;\nglobal using eShop.ServiceDefaults;\nglobal using Microsoft.EntityFrameworkCore;\nglobal using Microsoft.EntityFrameworkCore.Metadata.Builders;\nglobal using Microsoft.Extensions.Options;\nglobal using Npgsql;\n"
  },
  {
    "path": "src/Catalog.API/Infrastructure/CatalogContext.cs",
    "content": "﻿namespace eShop.Catalog.API.Infrastructure;\n\n/// <remarks>\n/// Add migrations using the following command inside the 'Catalog.API' project directory:\n///\n/// dotnet ef migrations add --context CatalogContext [migration-name]\n/// </remarks>\npublic class CatalogContext : DbContext\n{\n    public CatalogContext(DbContextOptions<CatalogContext> options, IConfiguration configuration) : base(options)\n    {\n    }\n\n    public required DbSet<CatalogItem> CatalogItems { get; set; }\n    public required DbSet<CatalogBrand> CatalogBrands { get; set; }\n    public required DbSet<CatalogType> CatalogTypes { get; set; }\n\n    protected override void OnModelCreating(ModelBuilder builder)\n    {\n        builder.HasPostgresExtension(\"vector\");\n        builder.ApplyConfiguration(new CatalogBrandEntityTypeConfiguration());\n        builder.ApplyConfiguration(new CatalogTypeEntityTypeConfiguration());\n        builder.ApplyConfiguration(new CatalogItemEntityTypeConfiguration());\n\n        // Add the outbox table to this context\n        builder.UseIntegrationEventLogs();\n    }\n}\n"
  },
  {
    "path": "src/Catalog.API/Infrastructure/CatalogContextSeed.cs",
    "content": "using System.Text.Json;\nusing eShop.Catalog.API.Services;\nusing Pgvector;\n\nnamespace eShop.Catalog.API.Infrastructure;\n\npublic partial class CatalogContextSeed(\n    IWebHostEnvironment env,\n    IOptions<CatalogOptions> settings,\n    ICatalogAI catalogAI,\n    ILogger<CatalogContextSeed> logger) : IDbSeeder<CatalogContext>\n{\n    public async Task SeedAsync(CatalogContext context)\n    {\n        var useCustomizationData = settings.Value.UseCustomizationData;\n        var contentRootPath = env.ContentRootPath;\n        var picturePath = env.WebRootPath;\n\n        // Workaround from https://github.com/npgsql/efcore.pg/issues/292#issuecomment-388608426\n        context.Database.OpenConnection();\n        ((NpgsqlConnection)context.Database.GetDbConnection()).ReloadTypes();\n\n        if (!context.CatalogItems.Any())\n        {\n            var sourcePath = Path.Combine(contentRootPath, \"Setup\", \"catalog.json\");\n            var sourceJson = File.ReadAllText(sourcePath);\n            var sourceItems = JsonSerializer.Deserialize<CatalogSourceEntry[]>(sourceJson) ?? Array.Empty<CatalogSourceEntry>();\n\n            context.CatalogBrands.RemoveRange(context.CatalogBrands);\n            await context.CatalogBrands.AddRangeAsync(sourceItems.Select(x => x.Brand).Distinct()\n                .Where(brandName => brandName != null)\n                .Select(brandName => new CatalogBrand(brandName!)));\n            logger.LogInformation(\"Seeded catalog with {NumBrands} brands\", context.CatalogBrands.Count());\n\n            context.CatalogTypes.RemoveRange(context.CatalogTypes);\n            await context.CatalogTypes.AddRangeAsync(sourceItems.Select(x => x.Type).Distinct()\n                .Where(typeName => typeName != null)\n                .Select(typeName => new CatalogType(typeName!)));\n            logger.LogInformation(\"Seeded catalog with {NumTypes} types\", context.CatalogTypes.Count());\n\n            await context.SaveChangesAsync();\n\n            var brandIdsByName = await context.CatalogBrands.ToDictionaryAsync(x => x.Brand, x => x.Id);\n            var typeIdsByName = await context.CatalogTypes.ToDictionaryAsync(x => x.Type, x => x.Id);\n\n            var catalogItems = sourceItems\n                .Where(source => source.Name != null && source.Brand != null && source.Type != null)\n                .Select(source => new CatalogItem(source.Name!)\n            {\n                Id = source.Id,\n                Description = source.Description,\n                Price = source.Price,\n                CatalogBrandId = brandIdsByName[source.Brand!],\n                CatalogTypeId = typeIdsByName[source.Type!],\n                AvailableStock = 100,\n                MaxStockThreshold = 200,\n                RestockThreshold = 10,\n                PictureFileName = $\"{source.Id}.webp\",\n            }).ToArray();\n\n            if (catalogAI.IsEnabled)\n            {\n                logger.LogInformation(\"Generating {NumItems} embeddings\", catalogItems.Length);\n                IReadOnlyList<Vector>? embeddings = await catalogAI.GetEmbeddingsAsync(catalogItems);\n                for (int i = 0; i < catalogItems.Length; i++)\n                {\n                    catalogItems[i].Embedding = embeddings?[i];\n                }\n            }\n\n            await context.CatalogItems.AddRangeAsync(catalogItems);\n            logger.LogInformation(\"Seeded catalog with {NumItems} items\", context.CatalogItems.Count());\n            await context.SaveChangesAsync();\n        }\n    }\n\n    private class CatalogSourceEntry\n    {\n        public int Id { get; set; }\n        public string? Type { get; set; }\n        public string? Brand { get; set; }\n        public string? Name { get; set; }\n        public string? Description { get; set; }\n        public decimal Price { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Catalog.API/Infrastructure/EntityConfigurations/CatalogBrandEntityTypeConfiguration.cs",
    "content": "﻿namespace eShop.Catalog.API.Infrastructure.EntityConfigurations;\n\nclass CatalogBrandEntityTypeConfiguration\n    : IEntityTypeConfiguration<CatalogBrand>\n{\n    public void Configure(EntityTypeBuilder<CatalogBrand> builder)\n    {\n        builder.ToTable(\"CatalogBrand\");\n\n        builder.Property(cb => cb.Brand)\n            .HasMaxLength(100);\n    }\n}\n"
  },
  {
    "path": "src/Catalog.API/Infrastructure/EntityConfigurations/CatalogItemEntityTypeConfiguration.cs",
    "content": "﻿namespace eShop.Catalog.API.Infrastructure.EntityConfigurations;\n\nclass CatalogItemEntityTypeConfiguration\n    : IEntityTypeConfiguration<CatalogItem>\n{\n    public void Configure(EntityTypeBuilder<CatalogItem> builder)\n    {\n        builder.ToTable(\"Catalog\");\n\n        builder.Property(ci => ci.Name)\n            .HasMaxLength(50);\n\n        builder.Property(ci => ci.Embedding)\n            .HasColumnType(\"vector(384)\");\n\n        builder.HasOne(ci => ci.CatalogBrand)\n            .WithMany();\n\n        builder.HasOne(ci => ci.CatalogType)\n            .WithMany();\n\n        builder.HasIndex(ci => ci.Name);\n    }\n}\n"
  },
  {
    "path": "src/Catalog.API/Infrastructure/EntityConfigurations/CatalogTypeEntityTypeConfiguration.cs",
    "content": "﻿namespace eShop.Catalog.API.Infrastructure.EntityConfigurations;\n\nclass CatalogTypeEntityTypeConfiguration\n    : IEntityTypeConfiguration<CatalogType>\n{\n    public void Configure(EntityTypeBuilder<CatalogType> builder)\n    {\n        builder.ToTable(\"CatalogType\");\n\n        builder.Property(cb => cb.Type)\n            .HasMaxLength(100);\n    }\n}\n"
  },
  {
    "path": "src/Catalog.API/Infrastructure/Exceptions/CatalogDomainException.cs",
    "content": "﻿namespace eShop.Catalog.API.Infrastructure.Exceptions;\n\n/// <summary>\n/// Exception type for app exceptions\n/// </summary>\npublic class CatalogDomainException : Exception\n{\n    public CatalogDomainException()\n    { }\n\n    public CatalogDomainException(string message)\n        : base(message)\n    { }\n\n    public CatalogDomainException(string message, Exception innerException)\n        : base(message, innerException)\n    { }\n}\n"
  },
  {
    "path": "src/Catalog.API/Infrastructure/Migrations/20231009153249_Initial.Designer.cs",
    "content": "﻿// <auto-generated />\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Infrastructure;\nusing Microsoft.EntityFrameworkCore.Migrations;\nusing Microsoft.EntityFrameworkCore.Storage.ValueConversion;\nusing eShop.Catalog.API.Infrastructure;\nusing Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;\nusing Pgvector;\n\n#nullable disable\n\nnamespace eShop.Catalog.API.Infrastructure.Migrations\n{\n    [DbContext(typeof(CatalogContext))]\n    [Migration(\"20231009153249_Initial\")]\n    partial class Initial\n    {\n        /// <inheritdoc />\n        protected override void BuildTargetModel(ModelBuilder modelBuilder)\n        {\n#pragma warning disable 612, 618\n            modelBuilder\n                .HasAnnotation(\"ProductVersion\", \"7.0.11\")\n                .HasAnnotation(\"Relational:MaxIdentifierLength\", 63);\n\n            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, \"vector\");\n            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);\n\n            modelBuilder.HasSequence(\"catalog_brand_hilo\")\n                .IncrementsBy(10);\n\n            modelBuilder.HasSequence(\"catalog_hilo\")\n                .IncrementsBy(10);\n\n            modelBuilder.HasSequence(\"catalog_type_hilo\")\n                .IncrementsBy(10);\n\n            modelBuilder.Entity(\"eShop.Catalog.API.Model.CatalogBrand\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"catalog_brand_hilo\");\n\n                    b.Property<string>(\"Brand\")\n                        .IsRequired()\n                        .HasMaxLength(100)\n                        .HasColumnType(\"character varying(100)\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"CatalogBrand\", (string)null);\n                });\n\n            modelBuilder.Entity(\"eShop.Catalog.API.Model.CatalogItem\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"catalog_hilo\");\n\n                    b.Property<int>(\"AvailableStock\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<int>(\"CatalogBrandId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<int>(\"CatalogTypeId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"Description\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<Vector>(\"Embedding\")\n                        .HasColumnType(\"vector(384)\");\n\n                    b.Property<int>(\"MaxStockThreshold\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"Name\")\n                        .IsRequired()\n                        .HasMaxLength(50)\n                        .HasColumnType(\"character varying(50)\");\n\n                    b.Property<bool>(\"OnReorder\")\n                        .HasColumnType(\"boolean\");\n\n                    b.Property<string>(\"PictureFileName\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<decimal>(\"Price\")\n                        .HasColumnType(\"numeric\");\n\n                    b.Property<int>(\"RestockThreshold\")\n                        .HasColumnType(\"integer\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"CatalogBrandId\");\n\n                    b.HasIndex(\"CatalogTypeId\");\n\n                    b.ToTable(\"Catalog\", (string)null);\n                });\n\n            modelBuilder.Entity(\"eShop.Catalog.API.Model.CatalogType\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"catalog_type_hilo\");\n\n                    b.Property<string>(\"Type\")\n                        .IsRequired()\n                        .HasMaxLength(100)\n                        .HasColumnType(\"character varying(100)\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"CatalogType\", (string)null);\n                });\n\n            modelBuilder.Entity(\"eShop.Catalog.API.Model.CatalogItem\", b =>\n                {\n                    b.HasOne(\"eShop.Catalog.API.Model.CatalogBrand\", \"CatalogBrand\")\n                        .WithMany()\n                        .HasForeignKey(\"CatalogBrandId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.HasOne(\"eShop.Catalog.API.Model.CatalogType\", \"CatalogType\")\n                        .WithMany()\n                        .HasForeignKey(\"CatalogTypeId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"CatalogBrand\");\n\n                    b.Navigation(\"CatalogType\");\n                });\n#pragma warning restore 612, 618\n        }\n    }\n}\n"
  },
  {
    "path": "src/Catalog.API/Infrastructure/Migrations/20231009153249_Initial.cs",
    "content": "﻿using Microsoft.EntityFrameworkCore.Migrations;\nusing Pgvector;\n\n#nullable disable\n\nnamespace eShop.Catalog.API.Infrastructure.Migrations\n{\n    /// <inheritdoc />\n    public partial class Initial : Migration\n    {\n        /// <inheritdoc />\n        protected override void Up(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.AlterDatabase()\n                .Annotation(\"Npgsql:PostgresExtension:vector\", \",,\");\n\n            migrationBuilder.CreateSequence(\n                name: \"catalog_brand_hilo\",\n                incrementBy: 10);\n\n            migrationBuilder.CreateSequence(\n                name: \"catalog_hilo\",\n                incrementBy: 10);\n\n            migrationBuilder.CreateSequence(\n                name: \"catalog_type_hilo\",\n                incrementBy: 10);\n\n            migrationBuilder.CreateTable(\n                name: \"CatalogBrand\",\n                columns: table => new\n                {\n                    Id = table.Column<int>(type: \"integer\", nullable: false),\n                    Brand = table.Column<string>(type: \"character varying(100)\", maxLength: 100, nullable: false)\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_CatalogBrand\", x => x.Id);\n                });\n\n            migrationBuilder.CreateTable(\n                name: \"CatalogType\",\n                columns: table => new\n                {\n                    Id = table.Column<int>(type: \"integer\", nullable: false),\n                    Type = table.Column<string>(type: \"character varying(100)\", maxLength: 100, nullable: false)\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_CatalogType\", x => x.Id);\n                });\n\n            migrationBuilder.CreateTable(\n                name: \"Catalog\",\n                columns: table => new\n                {\n                    Id = table.Column<int>(type: \"integer\", nullable: false),\n                    Name = table.Column<string>(type: \"character varying(50)\", maxLength: 50, nullable: false),\n                    Description = table.Column<string>(type: \"text\", nullable: true),\n                    Price = table.Column<decimal>(type: \"numeric\", nullable: false),\n                    PictureFileName = table.Column<string>(type: \"text\", nullable: true),\n                    CatalogTypeId = table.Column<int>(type: \"integer\", nullable: false),\n                    CatalogBrandId = table.Column<int>(type: \"integer\", nullable: false),\n                    AvailableStock = table.Column<int>(type: \"integer\", nullable: false),\n                    RestockThreshold = table.Column<int>(type: \"integer\", nullable: false),\n                    MaxStockThreshold = table.Column<int>(type: \"integer\", nullable: false),\n                    Embedding = table.Column<Vector>(type: \"vector(384)\", nullable: true),\n                    OnReorder = table.Column<bool>(type: \"boolean\", nullable: false)\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_Catalog\", x => x.Id);\n                    table.ForeignKey(\n                        name: \"FK_Catalog_CatalogBrand_CatalogBrandId\",\n                        column: x => x.CatalogBrandId,\n                        principalTable: \"CatalogBrand\",\n                        principalColumn: \"Id\",\n                        onDelete: ReferentialAction.Cascade);\n                    table.ForeignKey(\n                        name: \"FK_Catalog_CatalogType_CatalogTypeId\",\n                        column: x => x.CatalogTypeId,\n                        principalTable: \"CatalogType\",\n                        principalColumn: \"Id\",\n                        onDelete: ReferentialAction.Cascade);\n                });\n\n            migrationBuilder.CreateIndex(\n                name: \"IX_Catalog_CatalogBrandId\",\n                table: \"Catalog\",\n                column: \"CatalogBrandId\");\n\n            migrationBuilder.CreateIndex(\n                name: \"IX_Catalog_CatalogTypeId\",\n                table: \"Catalog\",\n                column: \"CatalogTypeId\");\n        }\n\n        /// <inheritdoc />\n        protected override void Down(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.DropTable(\n                name: \"Catalog\");\n\n            migrationBuilder.DropTable(\n                name: \"CatalogBrand\");\n\n            migrationBuilder.DropTable(\n                name: \"CatalogType\");\n\n            migrationBuilder.DropSequence(\n                name: \"catalog_brand_hilo\");\n\n            migrationBuilder.DropSequence(\n                name: \"catalog_hilo\");\n\n            migrationBuilder.DropSequence(\n                name: \"catalog_type_hilo\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Catalog.API/Infrastructure/Migrations/20231018163051_RemoveHiLoAndIndexCatalogName.Designer.cs",
    "content": "﻿// <auto-generated />\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Infrastructure;\nusing Microsoft.EntityFrameworkCore.Migrations;\nusing Microsoft.EntityFrameworkCore.Storage.ValueConversion;\nusing eShop.Catalog.API.Infrastructure;\nusing Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;\nusing Pgvector;\n\n#nullable disable\n\nnamespace eShop.Catalog.API.Infrastructure.Migrations\n{\n    [DbContext(typeof(CatalogContext))]\n    [Migration(\"20231018163051_RemoveHiLoAndIndexCatalogName\")]\n    partial class RemoveHiLoAndIndexCatalogName\n    {\n        /// <inheritdoc />\n        protected override void BuildTargetModel(ModelBuilder modelBuilder)\n        {\n#pragma warning disable 612, 618\n            modelBuilder\n                .HasAnnotation(\"ProductVersion\", \"8.0.0-rc.2.23480.1\")\n                .HasAnnotation(\"Relational:MaxIdentifierLength\", 63);\n\n            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, \"vector\");\n            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);\n\n            modelBuilder.Entity(\"eShop.Catalog.API.Model.CatalogBrand\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>(\"Id\"));\n\n                    b.Property<string>(\"Brand\")\n                        .IsRequired()\n                        .HasMaxLength(100)\n                        .HasColumnType(\"character varying(100)\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"CatalogBrand\", (string)null);\n                });\n\n            modelBuilder.Entity(\"eShop.Catalog.API.Model.CatalogItem\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>(\"Id\"));\n\n                    b.Property<int>(\"AvailableStock\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<int>(\"CatalogBrandId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<int>(\"CatalogTypeId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"Description\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<Vector>(\"Embedding\")\n                        .HasColumnType(\"vector(384)\");\n\n                    b.Property<int>(\"MaxStockThreshold\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"Name\")\n                        .IsRequired()\n                        .HasMaxLength(50)\n                        .HasColumnType(\"character varying(50)\");\n\n                    b.Property<bool>(\"OnReorder\")\n                        .HasColumnType(\"boolean\");\n\n                    b.Property<string>(\"PictureFileName\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<decimal>(\"Price\")\n                        .HasColumnType(\"numeric\");\n\n                    b.Property<int>(\"RestockThreshold\")\n                        .HasColumnType(\"integer\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"CatalogBrandId\");\n\n                    b.HasIndex(\"CatalogTypeId\");\n\n                    b.HasIndex(\"Name\");\n\n                    b.ToTable(\"Catalog\", (string)null);\n                });\n\n            modelBuilder.Entity(\"eShop.Catalog.API.Model.CatalogType\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>(\"Id\"));\n\n                    b.Property<string>(\"Type\")\n                        .IsRequired()\n                        .HasMaxLength(100)\n                        .HasColumnType(\"character varying(100)\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"CatalogType\", (string)null);\n                });\n\n            modelBuilder.Entity(\"eShop.Catalog.API.Model.CatalogItem\", b =>\n                {\n                    b.HasOne(\"eShop.Catalog.API.Model.CatalogBrand\", \"CatalogBrand\")\n                        .WithMany()\n                        .HasForeignKey(\"CatalogBrandId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.HasOne(\"eShop.Catalog.API.Model.CatalogType\", \"CatalogType\")\n                        .WithMany()\n                        .HasForeignKey(\"CatalogTypeId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"CatalogBrand\");\n\n                    b.Navigation(\"CatalogType\");\n                });\n#pragma warning restore 612, 618\n        }\n    }\n}\n"
  },
  {
    "path": "src/Catalog.API/Infrastructure/Migrations/20231018163051_RemoveHiLoAndIndexCatalogName.cs",
    "content": "﻿using Microsoft.EntityFrameworkCore.Migrations;\nusing Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;\n\n#nullable disable\n\nnamespace eShop.Catalog.API.Infrastructure.Migrations\n{\n    /// <inheritdoc />\n    public partial class RemoveHiLoAndIndexCatalogName : Migration\n    {\n        /// <inheritdoc />\n        protected override void Up(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.DropSequence(\n                name: \"catalog_brand_hilo\");\n\n            migrationBuilder.DropSequence(\n                name: \"catalog_hilo\");\n\n            migrationBuilder.DropSequence(\n                name: \"catalog_type_hilo\");\n\n            migrationBuilder.AlterColumn<int>(\n                name: \"Id\",\n                table: \"CatalogType\",\n                type: \"integer\",\n                nullable: false,\n                oldClrType: typeof(int),\n                oldType: \"integer\")\n                .Annotation(\"Npgsql:ValueGenerationStrategy\", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);\n\n            migrationBuilder.AlterColumn<int>(\n                name: \"Id\",\n                table: \"CatalogBrand\",\n                type: \"integer\",\n                nullable: false,\n                oldClrType: typeof(int),\n                oldType: \"integer\")\n                .Annotation(\"Npgsql:ValueGenerationStrategy\", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);\n\n            migrationBuilder.AlterColumn<int>(\n                name: \"Id\",\n                table: \"Catalog\",\n                type: \"integer\",\n                nullable: false,\n                oldClrType: typeof(int),\n                oldType: \"integer\")\n                .Annotation(\"Npgsql:ValueGenerationStrategy\", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);\n\n            migrationBuilder.CreateIndex(\n                name: \"IX_Catalog_Name\",\n                table: \"Catalog\",\n                column: \"Name\");\n        }\n\n        /// <inheritdoc />\n        protected override void Down(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.DropIndex(\n                name: \"IX_Catalog_Name\",\n                table: \"Catalog\");\n\n            migrationBuilder.CreateSequence(\n                name: \"catalog_brand_hilo\",\n                incrementBy: 10);\n\n            migrationBuilder.CreateSequence(\n                name: \"catalog_hilo\",\n                incrementBy: 10);\n\n            migrationBuilder.CreateSequence(\n                name: \"catalog_type_hilo\",\n                incrementBy: 10);\n\n            migrationBuilder.AlterColumn<int>(\n                name: \"Id\",\n                table: \"CatalogType\",\n                type: \"integer\",\n                nullable: false,\n                oldClrType: typeof(int),\n                oldType: \"integer\")\n                .OldAnnotation(\"Npgsql:ValueGenerationStrategy\", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);\n\n            migrationBuilder.AlterColumn<int>(\n                name: \"Id\",\n                table: \"CatalogBrand\",\n                type: \"integer\",\n                nullable: false,\n                oldClrType: typeof(int),\n                oldType: \"integer\")\n                .OldAnnotation(\"Npgsql:ValueGenerationStrategy\", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);\n\n            migrationBuilder.AlterColumn<int>(\n                name: \"Id\",\n                table: \"Catalog\",\n                type: \"integer\",\n                nullable: false,\n                oldClrType: typeof(int),\n                oldType: \"integer\")\n                .OldAnnotation(\"Npgsql:ValueGenerationStrategy\", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Catalog.API/Infrastructure/Migrations/20231026091140_Outbox.Designer.cs",
    "content": "﻿// <auto-generated />\nusing System;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Infrastructure;\nusing Microsoft.EntityFrameworkCore.Migrations;\nusing Microsoft.EntityFrameworkCore.Storage.ValueConversion;\nusing eShop.Catalog.API.Infrastructure;\nusing Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;\nusing Pgvector;\n\n#nullable disable\n\nnamespace eShop.Catalog.API.Infrastructure.Migrations\n{\n    [DbContext(typeof(CatalogContext))]\n    [Migration(\"20231026091140_Outbox\")]\n    partial class Outbox\n    {\n        /// <inheritdoc />\n        protected override void BuildTargetModel(ModelBuilder modelBuilder)\n        {\n#pragma warning disable 612, 618\n            modelBuilder\n                .HasAnnotation(\"ProductVersion\", \"8.0.0-rtm.23512.13\")\n                .HasAnnotation(\"Relational:MaxIdentifierLength\", 63);\n\n            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, \"vector\");\n            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);\n\n            modelBuilder.Entity(\"eShop.Catalog.API.Model.CatalogBrand\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>(\"Id\"));\n\n                    b.Property<string>(\"Brand\")\n                        .IsRequired()\n                        .HasMaxLength(100)\n                        .HasColumnType(\"character varying(100)\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"CatalogBrand\", (string)null);\n                });\n\n            modelBuilder.Entity(\"eShop.Catalog.API.Model.CatalogItem\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>(\"Id\"));\n\n                    b.Property<int>(\"AvailableStock\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<int>(\"CatalogBrandId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<int>(\"CatalogTypeId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"Description\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<Vector>(\"Embedding\")\n                        .HasColumnType(\"vector(384)\");\n\n                    b.Property<int>(\"MaxStockThreshold\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"Name\")\n                        .IsRequired()\n                        .HasMaxLength(50)\n                        .HasColumnType(\"character varying(50)\");\n\n                    b.Property<bool>(\"OnReorder\")\n                        .HasColumnType(\"boolean\");\n\n                    b.Property<string>(\"PictureFileName\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<decimal>(\"Price\")\n                        .HasColumnType(\"numeric\");\n\n                    b.Property<int>(\"RestockThreshold\")\n                        .HasColumnType(\"integer\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"CatalogBrandId\");\n\n                    b.HasIndex(\"CatalogTypeId\");\n\n                    b.HasIndex(\"Name\");\n\n                    b.ToTable(\"Catalog\", (string)null);\n                });\n\n            modelBuilder.Entity(\"eShop.Catalog.API.Model.CatalogType\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>(\"Id\"));\n\n                    b.Property<string>(\"Type\")\n                        .IsRequired()\n                        .HasMaxLength(100)\n                        .HasColumnType(\"character varying(100)\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"CatalogType\", (string)null);\n                });\n\n            modelBuilder.Entity(\"eShop.IntegrationEventLogEF.IntegrationEventLogEntry\", b =>\n                {\n                    b.Property<Guid>(\"EventId\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"uuid\");\n\n                    b.Property<string>(\"Content\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<DateTime>(\"CreationTime\")\n                        .HasColumnType(\"timestamp with time zone\");\n\n                    b.Property<string>(\"EventTypeName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<int>(\"State\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<int>(\"TimesSent\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<Guid>(\"TransactionId\")\n                        .HasColumnType(\"uuid\");\n\n                    b.HasKey(\"EventId\");\n\n                    b.ToTable(\"IntegrationEventLog\", (string)null);\n                });\n\n            modelBuilder.Entity(\"eShop.Catalog.API.Model.CatalogItem\", b =>\n                {\n                    b.HasOne(\"eShop.Catalog.API.Model.CatalogBrand\", \"CatalogBrand\")\n                        .WithMany()\n                        .HasForeignKey(\"CatalogBrandId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.HasOne(\"eShop.Catalog.API.Model.CatalogType\", \"CatalogType\")\n                        .WithMany()\n                        .HasForeignKey(\"CatalogTypeId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"CatalogBrand\");\n\n                    b.Navigation(\"CatalogType\");\n                });\n#pragma warning restore 612, 618\n        }\n    }\n}\n"
  },
  {
    "path": "src/Catalog.API/Infrastructure/Migrations/20231026091140_Outbox.cs",
    "content": "﻿using System;\nusing Microsoft.EntityFrameworkCore.Migrations;\n\n#nullable disable\n\nnamespace eShop.Catalog.API.Infrastructure.Migrations\n{\n    /// <inheritdoc />\n    public partial class Outbox : Migration\n    {\n        /// <inheritdoc />\n        protected override void Up(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.CreateTable(\n                name: \"IntegrationEventLog\",\n                columns: table => new\n                {\n                    EventId = table.Column<Guid>(type: \"uuid\", nullable: false),\n                    EventTypeName = table.Column<string>(type: \"text\", nullable: false),\n                    State = table.Column<int>(type: \"integer\", nullable: false),\n                    TimesSent = table.Column<int>(type: \"integer\", nullable: false),\n                    CreationTime = table.Column<DateTime>(type: \"timestamp with time zone\", nullable: false),\n                    Content = table.Column<string>(type: \"text\", nullable: false),\n                    TransactionId = table.Column<Guid>(type: \"uuid\", nullable: false)\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_IntegrationEventLog\", x => x.EventId);\n                });\n        }\n\n        /// <inheritdoc />\n        protected override void Down(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.DropTable(\n                name: \"IntegrationEventLog\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Catalog.API/Infrastructure/Migrations/CatalogContextModelSnapshot.cs",
    "content": "﻿// <auto-generated />\nusing System;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Infrastructure;\nusing Microsoft.EntityFrameworkCore.Storage.ValueConversion;\nusing eShop.Catalog.API.Infrastructure;\nusing Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;\nusing Pgvector;\n\n#nullable disable\n\nnamespace eShop.Catalog.API.Infrastructure.Migrations\n{\n    [DbContext(typeof(CatalogContext))]\n    partial class CatalogContextModelSnapshot : ModelSnapshot\n    {\n        protected override void BuildModel(ModelBuilder modelBuilder)\n        {\n#pragma warning disable 612, 618\n            modelBuilder\n                .HasAnnotation(\"ProductVersion\", \"8.0.0-rtm.23512.13\")\n                .HasAnnotation(\"Relational:MaxIdentifierLength\", 63);\n\n            NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, \"vector\");\n            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);\n\n            modelBuilder.Entity(\"eShop.Catalog.API.Model.CatalogBrand\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>(\"Id\"));\n\n                    b.Property<string>(\"Brand\")\n                        .IsRequired()\n                        .HasMaxLength(100)\n                        .HasColumnType(\"character varying(100)\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"CatalogBrand\", (string)null);\n                });\n\n            modelBuilder.Entity(\"eShop.Catalog.API.Model.CatalogItem\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>(\"Id\"));\n\n                    b.Property<int>(\"AvailableStock\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<int>(\"CatalogBrandId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<int>(\"CatalogTypeId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"Description\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<Vector>(\"Embedding\")\n                        .HasColumnType(\"vector(384)\");\n\n                    b.Property<int>(\"MaxStockThreshold\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"Name\")\n                        .IsRequired()\n                        .HasMaxLength(50)\n                        .HasColumnType(\"character varying(50)\");\n\n                    b.Property<bool>(\"OnReorder\")\n                        .HasColumnType(\"boolean\");\n\n                    b.Property<string>(\"PictureFileName\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<decimal>(\"Price\")\n                        .HasColumnType(\"numeric\");\n\n                    b.Property<int>(\"RestockThreshold\")\n                        .HasColumnType(\"integer\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"CatalogBrandId\");\n\n                    b.HasIndex(\"CatalogTypeId\");\n\n                    b.HasIndex(\"Name\");\n\n                    b.ToTable(\"Catalog\", (string)null);\n                });\n\n            modelBuilder.Entity(\"eShop.Catalog.API.Model.CatalogType\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>(\"Id\"));\n\n                    b.Property<string>(\"Type\")\n                        .IsRequired()\n                        .HasMaxLength(100)\n                        .HasColumnType(\"character varying(100)\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"CatalogType\", (string)null);\n                });\n\n            modelBuilder.Entity(\"eShop.IntegrationEventLogEF.IntegrationEventLogEntry\", b =>\n                {\n                    b.Property<Guid>(\"EventId\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"uuid\");\n\n                    b.Property<string>(\"Content\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<DateTime>(\"CreationTime\")\n                        .HasColumnType(\"timestamp with time zone\");\n\n                    b.Property<string>(\"EventTypeName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<int>(\"State\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<int>(\"TimesSent\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<Guid>(\"TransactionId\")\n                        .HasColumnType(\"uuid\");\n\n                    b.HasKey(\"EventId\");\n\n                    b.ToTable(\"IntegrationEventLog\", (string)null);\n                });\n\n            modelBuilder.Entity(\"eShop.Catalog.API.Model.CatalogItem\", b =>\n                {\n                    b.HasOne(\"eShop.Catalog.API.Model.CatalogBrand\", \"CatalogBrand\")\n                        .WithMany()\n                        .HasForeignKey(\"CatalogBrandId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.HasOne(\"eShop.Catalog.API.Model.CatalogType\", \"CatalogType\")\n                        .WithMany()\n                        .HasForeignKey(\"CatalogTypeId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"CatalogBrand\");\n\n                    b.Navigation(\"CatalogType\");\n                });\n#pragma warning restore 612, 618\n        }\n    }\n}\n"
  },
  {
    "path": "src/Catalog.API/IntegrationEvents/CatalogIntegrationEventService.cs",
    "content": "﻿namespace eShop.Catalog.API.IntegrationEvents;\n\npublic sealed class CatalogIntegrationEventService(ILogger<CatalogIntegrationEventService> logger,\n    IEventBus eventBus,\n    CatalogContext catalogContext,\n    IIntegrationEventLogService integrationEventLogService)\n    : ICatalogIntegrationEventService, IDisposable\n{\n    private volatile bool disposedValue;\n\n    public async Task PublishThroughEventBusAsync(IntegrationEvent evt)\n    {\n        try\n        {\n            logger.LogInformation(\"Publishing integration event: {IntegrationEventId_published} - ({@IntegrationEvent})\", evt.Id, evt);\n\n            await integrationEventLogService.MarkEventAsInProgressAsync(evt.Id);\n            await eventBus.PublishAsync(evt);\n            await integrationEventLogService.MarkEventAsPublishedAsync(evt.Id);\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(ex, \"Error Publishing integration event: {IntegrationEventId} - ({@IntegrationEvent})\", evt.Id, evt);\n            await integrationEventLogService.MarkEventAsFailedAsync(evt.Id);\n        }\n    }\n\n    public async Task SaveEventAndCatalogContextChangesAsync(IntegrationEvent evt)\n    {\n        logger.LogInformation(\"CatalogIntegrationEventService - Saving changes and integrationEvent: {IntegrationEventId}\", evt.Id);\n\n        //Use of an EF Core resiliency strategy when using multiple DbContexts within an explicit BeginTransaction():\n        //See: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency            \n        await ResilientTransaction.New(catalogContext).ExecuteAsync(async () =>\n        {\n            // Achieving atomicity between original catalog database operation and the IntegrationEventLog thanks to a local transaction\n            await catalogContext.SaveChangesAsync();\n            await integrationEventLogService.SaveEventAsync(evt, catalogContext.Database.CurrentTransaction);\n        });\n    }\n\n    private void Dispose(bool disposing)\n    {\n        if (!disposedValue)\n        {\n            if (disposing)\n            {\n                (integrationEventLogService as IDisposable)?.Dispose();\n            }\n\n            disposedValue = true;\n        }\n    }\n\n    public void Dispose()\n    {\n        Dispose(disposing: true);\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "src/Catalog.API/IntegrationEvents/EventHandling/AnyFutureIntegrationEventHandler.cs.txt",
    "content": "﻿\n\n// To implement ProductPriceChangedEvent.cs here\n"
  },
  {
    "path": "src/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingValidationIntegrationEventHandler.cs",
    "content": "﻿namespace eShop.Catalog.API.IntegrationEvents.EventHandling;\n\npublic class OrderStatusChangedToAwaitingValidationIntegrationEventHandler(\n    CatalogContext catalogContext,\n    ICatalogIntegrationEventService catalogIntegrationEventService,\n    ILogger<OrderStatusChangedToAwaitingValidationIntegrationEventHandler> logger) :\n    IIntegrationEventHandler<OrderStatusChangedToAwaitingValidationIntegrationEvent>\n{\n    public async Task Handle(OrderStatusChangedToAwaitingValidationIntegrationEvent @event)\n    {\n        logger.LogInformation(\"Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})\", @event.Id, @event);\n\n        var confirmedOrderStockItems = new List<ConfirmedOrderStockItem>();\n\n        foreach (var orderStockItem in @event.OrderStockItems)\n        {\n            var catalogItem = catalogContext.CatalogItems.Find(orderStockItem.ProductId);\n            if (catalogItem is not null)\n            {\n                var hasStock = catalogItem.AvailableStock >= orderStockItem.Units;\n                var confirmedOrderStockItem = new ConfirmedOrderStockItem(catalogItem.Id, hasStock);\n\n                confirmedOrderStockItems.Add(confirmedOrderStockItem);\n            }\n        }\n\n        var confirmedIntegrationEvent = confirmedOrderStockItems.Any(c => !c.HasStock)\n            ? (IntegrationEvent)new OrderStockRejectedIntegrationEvent(@event.OrderId, confirmedOrderStockItems)\n            : new OrderStockConfirmedIntegrationEvent(@event.OrderId);\n\n        await catalogIntegrationEventService.SaveEventAndCatalogContextChangesAsync(confirmedIntegrationEvent);\n        await catalogIntegrationEventService.PublishThroughEventBusAsync(confirmedIntegrationEvent);\n    }\n}\n"
  },
  {
    "path": "src/Catalog.API/IntegrationEvents/EventHandling/OrderStatusChangedToPaidIntegrationEventHandler.cs",
    "content": "﻿namespace eShop.Catalog.API.IntegrationEvents.EventHandling;\n\npublic class OrderStatusChangedToPaidIntegrationEventHandler(\n    CatalogContext catalogContext,\n    ILogger<OrderStatusChangedToPaidIntegrationEventHandler> logger) :\n    IIntegrationEventHandler<OrderStatusChangedToPaidIntegrationEvent>\n{\n    public async Task Handle(OrderStatusChangedToPaidIntegrationEvent @event)\n    {\n        logger.LogInformation(\"Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})\", @event.Id, @event);\n\n        //we're not blocking stock/inventory\n        foreach (var orderStockItem in @event.OrderStockItems)\n        {\n            var catalogItem = catalogContext.CatalogItems.Find(orderStockItem.ProductId);\n\n            catalogItem?.RemoveStock(orderStockItem.Units);\n        }\n\n        await catalogContext.SaveChangesAsync();\n    }\n}\n"
  },
  {
    "path": "src/Catalog.API/IntegrationEvents/Events/ConfirmedOrderStockItem.cs",
    "content": "﻿namespace eShop.Catalog.API.IntegrationEvents.Events;\n\npublic record ConfirmedOrderStockItem(int ProductId, bool HasStock);\n"
  },
  {
    "path": "src/Catalog.API/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs",
    "content": "﻿namespace eShop.Catalog.API.IntegrationEvents.Events;\n\npublic record OrderStatusChangedToAwaitingValidationIntegrationEvent(int OrderId, IEnumerable<OrderStockItem> OrderStockItems) : IntegrationEvent;\n"
  },
  {
    "path": "src/Catalog.API/IntegrationEvents/Events/OrderStatusChangedToPaidIntegrationEvent.cs",
    "content": "﻿namespace eShop.Catalog.API.IntegrationEvents.Events;\n\npublic record OrderStatusChangedToPaidIntegrationEvent(int OrderId, IEnumerable<OrderStockItem> OrderStockItems) : IntegrationEvent;\n"
  },
  {
    "path": "src/Catalog.API/IntegrationEvents/Events/OrderStockConfirmedIntegrationEvent.cs",
    "content": "﻿namespace eShop.Catalog.API.IntegrationEvents.Events;\n\npublic record OrderStockConfirmedIntegrationEvent(int OrderId) : IntegrationEvent;\n"
  },
  {
    "path": "src/Catalog.API/IntegrationEvents/Events/OrderStockItem.cs",
    "content": "﻿namespace eShop.Catalog.API.IntegrationEvents.Events;\n\npublic record OrderStockItem(int ProductId, int Units);\n"
  },
  {
    "path": "src/Catalog.API/IntegrationEvents/Events/OrderStockRejectedIntegrationEvent.cs",
    "content": "﻿namespace eShop.Catalog.API.IntegrationEvents.Events;\n\npublic record OrderStockRejectedIntegrationEvent(int OrderId, List<ConfirmedOrderStockItem> OrderStockItems) : IntegrationEvent;\n"
  },
  {
    "path": "src/Catalog.API/IntegrationEvents/Events/ProductPriceChangedIntegrationEvent.cs",
    "content": "﻿namespace eShop.Catalog.API.IntegrationEvents.Events;\n\n// Integration Events notes: \n// An Event is “something that has happened in the past”, therefore its name has to be past tense\n// An Integration Event is an event that can cause side effects to other microservices, Bounded-Contexts or external systems.\npublic record ProductPriceChangedIntegrationEvent(int ProductId, decimal NewPrice, decimal OldPrice) : IntegrationEvent;\n"
  },
  {
    "path": "src/Catalog.API/IntegrationEvents/ICatalogIntegrationEventService.cs",
    "content": "﻿namespace eShop.Catalog.API.IntegrationEvents;\n\npublic interface ICatalogIntegrationEventService\n{\n    Task SaveEventAndCatalogContextChangesAsync(IntegrationEvent evt);\n    Task PublishThroughEventBusAsync(IntegrationEvent evt);\n}\n"
  },
  {
    "path": "src/Catalog.API/Model/CatalogBrand.cs",
    "content": "using System.ComponentModel.DataAnnotations;\n\nnamespace eShop.Catalog.API.Model;\n\npublic class CatalogBrand\n{\n    public CatalogBrand(string brand) {\n        Brand = brand;\n    }\n\n    public int Id { get; set; }\n\n    [Required]\n    public string Brand { get; set; }\n}\n"
  },
  {
    "path": "src/Catalog.API/Model/CatalogItem.cs",
    "content": "using System.ComponentModel.DataAnnotations;\nusing System.Text.Json.Serialization;\nusing Pgvector;\n\nnamespace eShop.Catalog.API.Model;\n\npublic class CatalogItem\n{\n    public int Id { get; set; }\n\n    [Required]\n    public string Name { get; set; }\n\n    public string? Description { get; set; }\n\n    public decimal Price { get; set; }\n\n    public string? PictureFileName { get; set; }\n\n    public int CatalogTypeId { get; set; }\n\n    public CatalogType? CatalogType { get; set; }\n\n    public int CatalogBrandId { get; set; }\n\n    public CatalogBrand? CatalogBrand { get; set; }\n\n    // Quantity in stock\n    public int AvailableStock { get; set; }\n\n    // Available stock at which we should reorder\n    public int RestockThreshold { get; set; }\n\n\n    // Maximum number of units that can be in-stock at any time (due to physicial/logistical constraints in warehouses)\n    public int MaxStockThreshold { get; set; }\n\n    /// <summary>Optional embedding for the catalog item's description.</summary>\n    [JsonIgnore]\n    public Vector? Embedding { get; set; }\n\n    /// <summary>\n    /// True if item is on reorder\n    /// </summary>\n    public bool OnReorder { get; set; }\n\n    public CatalogItem(string name) { Name = name; }\n\n\n    /// <summary>\n    /// Decrements the quantity of a particular item in inventory and ensures the restockThreshold hasn't\n    /// been breached. If so, a RestockRequest is generated in CheckThreshold. \n    /// \n    /// If there is sufficient stock of an item, then the integer returned at the end of this call should be the same as quantityDesired. \n    /// In the event that there is not sufficient stock available, the method will remove whatever stock is available and return that quantity to the client.\n    /// In this case, it is the responsibility of the client to determine if the amount that is returned is the same as quantityDesired.\n    /// It is invalid to pass in a negative number. \n    /// </summary>\n    /// <param name=\"quantityDesired\"></param>\n    /// <returns>int: Returns the number actually removed from stock. </returns>\n    /// \n    public int RemoveStock(int quantityDesired)\n    {\n        if (AvailableStock == 0)\n        {\n            throw new CatalogDomainException($\"Empty stock, product item {Name} is sold out\");\n        }\n\n        if (quantityDesired <= 0)\n        {\n            throw new CatalogDomainException($\"Item units desired should be greater than zero\");\n        }\n\n        int removed = Math.Min(quantityDesired, this.AvailableStock);\n\n        this.AvailableStock -= removed;\n\n        return removed;\n    }\n\n    /// <summary>\n    /// Increments the quantity of a particular item in inventory.\n    /// <param name=\"quantity\"></param>\n    /// <returns>int: Returns the quantity that has been added to stock</returns>\n    /// </summary>\n    public int AddStock(int quantity)\n    {\n        int original = this.AvailableStock;\n\n        // The quantity that the client is trying to add to stock is greater than what can be physically accommodated in the Warehouse\n        if ((this.AvailableStock + quantity) > this.MaxStockThreshold)\n        {\n            // For now, this method only adds new units up maximum stock threshold. In an expanded version of this application, we\n            //could include tracking for the remaining units and store information about overstock elsewhere. \n            this.AvailableStock += (this.MaxStockThreshold - this.AvailableStock);\n        }\n        else\n        {\n            this.AvailableStock += quantity;\n        }\n\n        this.OnReorder = false;\n\n        return this.AvailableStock - original;\n    }\n}\n"
  },
  {
    "path": "src/Catalog.API/Model/CatalogServices.cs",
    "content": "﻿using eShop.Catalog.API.Services;\nusing Microsoft.AspNetCore.Mvc;\n\npublic class CatalogServices(\n    CatalogContext context,\n    [FromServices] ICatalogAI catalogAI,\n    IOptions<CatalogOptions> options,\n    ILogger<CatalogServices> logger,\n    [FromServices] ICatalogIntegrationEventService eventService)\n{\n    public CatalogContext Context { get; } = context;\n    public ICatalogAI CatalogAI { get; } = catalogAI;\n    public IOptions<CatalogOptions> Options { get; } = options;\n    public ILogger<CatalogServices> Logger { get; } = logger;\n    public ICatalogIntegrationEventService EventService { get; } = eventService;\n};\n"
  },
  {
    "path": "src/Catalog.API/Model/CatalogType.cs",
    "content": "using System.ComponentModel.DataAnnotations;\n\nnamespace eShop.Catalog.API.Model;\n\npublic class CatalogType\n{\n    public CatalogType(string type) {\n        Type = type;\n    }\n\n    public int Id { get; set; }\n\n    [Required]\n    public string Type { get; set; }\n}\n"
  },
  {
    "path": "src/Catalog.API/Model/PaginatedItems.cs",
    "content": "﻿using System.Text.Json.Serialization;\n\nnamespace eShop.Catalog.API.Model;\n\npublic class PaginatedItems<TEntity>(int pageIndex, int pageSize, long count, IEnumerable<TEntity> data) where TEntity : class\n{\n    public int PageIndex { get; } = pageIndex;\n\n    public int PageSize { get; } = pageSize;\n\n    public long Count { get; } = count;\n\n    public IEnumerable<TEntity> Data { get;} = data;\n}\n"
  },
  {
    "path": "src/Catalog.API/Model/PaginationRequest.cs",
    "content": "﻿using System.ComponentModel;\n\nnamespace eShop.Catalog.API.Model;\n\npublic record PaginationRequest(\n    [property: Description(\"Number of items to return in a single page of results\")]\n    [property: DefaultValue(10)]\n    int PageSize = 10,\n\n    [property: Description(\"The index of the page of results to return\")]\n    [property: DefaultValue(0)]\n    int PageIndex = 0\n);\n"
  },
  {
    "path": "src/Catalog.API/Program.Testing.cs",
    "content": "// Require a public Program class to implement the\n// fixture for the WebApplicationFactory in the\n// integration tests. Using IVT is not sufficient\n// in this case, because the accessibility of the\n// `Program` type is checked. \npublic partial class Program { }\n"
  },
  {
    "path": "src/Catalog.API/Program.cs",
    "content": "﻿using Asp.Versioning.Builder;\nusing System.Reflection;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.AddServiceDefaults();\nbuilder.AddApplicationServices();\nbuilder.Services.AddProblemDetails();\n\nvar withApiVersioning = builder.Services.AddApiVersioning();\n\nbuilder.AddDefaultOpenApi(withApiVersioning);\n\nvar app = builder.Build();\n\napp.MapDefaultEndpoints();\n\napp.UseStatusCodePages();\n\napp.MapCatalogApi();\n\napp.UseDefaultOpenApi();\napp.Run();\n"
  },
  {
    "path": "src/Catalog.API/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"http\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"applicationUrl\": \"http://localhost:5222/\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "src/Catalog.API/Services/CatalogAI.cs",
    "content": "using System.Diagnostics;\nusing Microsoft.Extensions.AI;\nusing Pgvector;\n\nnamespace eShop.Catalog.API.Services;\n\npublic sealed class CatalogAI : ICatalogAI\n{\n    private const int EmbeddingDimensions = 384;\n    private readonly IEmbeddingGenerator<string, Embedding<float>>? _embeddingGenerator;\n\n    /// <summary>The web host environment.</summary>\n    private readonly IWebHostEnvironment _environment;\n    /// <summary>Logger for use in AI operations.</summary>\n    private readonly ILogger _logger;\n\n    public CatalogAI(IWebHostEnvironment environment, ILogger<CatalogAI> logger, IEmbeddingGenerator<string, Embedding<float>>? embeddingGenerator = null)\n    {\n        _embeddingGenerator = embeddingGenerator;\n        _environment = environment;\n        _logger = logger;\n    }\n\n    /// <inheritdoc/>\n    public bool IsEnabled => _embeddingGenerator is not null;\n\n    /// <inheritdoc/>\n    public ValueTask<Vector?> GetEmbeddingAsync(CatalogItem item) =>\n        IsEnabled ?\n            GetEmbeddingAsync(CatalogItemToString(item)) :\n            ValueTask.FromResult<Vector?>(null);\n\n    /// <inheritdoc/>\n    public async ValueTask<IReadOnlyList<Vector>?> GetEmbeddingsAsync(IEnumerable<CatalogItem> items)\n    {\n        if (IsEnabled)\n        {\n            long timestamp = Stopwatch.GetTimestamp();\n\n            GeneratedEmbeddings<Embedding<float>> embeddings = await _embeddingGenerator!.GenerateAsync(items.Select(CatalogItemToString));\n            var results = embeddings.Select(m => new Vector(m.Vector[0..EmbeddingDimensions])).ToList();\n\n            if (_logger.IsEnabled(LogLevel.Trace))\n            {\n                _logger.LogTrace(\"Generated {EmbeddingsCount} embeddings in {ElapsedMilliseconds}s\", results.Count, Stopwatch.GetElapsedTime(timestamp).TotalSeconds);\n            }\n\n            return results;\n        }\n\n        return null;\n    }\n\n    /// <inheritdoc/>\n    public async ValueTask<Vector?> GetEmbeddingAsync(string text)\n    {\n        if (IsEnabled)\n        {\n            long timestamp = Stopwatch.GetTimestamp();\n\n            var embedding = await _embeddingGenerator!.GenerateVectorAsync(text);\n            embedding = embedding[0..EmbeddingDimensions];\n\n            if (_logger.IsEnabled(LogLevel.Trace))\n            {\n                _logger.LogTrace(\"Generated embedding in {ElapsedMilliseconds}s: '{Text}'\", Stopwatch.GetElapsedTime(timestamp).TotalSeconds, text);\n            }\n\n            return new Vector(embedding);\n        }\n\n        return null;\n    }\n\n    private static string CatalogItemToString(CatalogItem item) => $\"{item.Name} {item.Description}\";\n}\n"
  },
  {
    "path": "src/Catalog.API/Services/ICatalogAI.cs",
    "content": "﻿using Pgvector;\n\nnamespace eShop.Catalog.API.Services;\n\npublic interface ICatalogAI\n{\n    /// <summary>Gets whether the AI system is enabled.</summary>\n    bool IsEnabled { get; }\n\n    /// <summary>Gets an embedding vector for the specified text.</summary>\n    ValueTask<Vector?> GetEmbeddingAsync(string text);\n    \n    /// <summary>Gets an embedding vector for the specified catalog item.</summary>\n    ValueTask<Vector?> GetEmbeddingAsync(CatalogItem item);\n\n    /// <summary>Gets embedding vectors for the specified catalog items.</summary>\n    ValueTask<IReadOnlyList<Vector>?> GetEmbeddingsAsync(IEnumerable<CatalogItem> item);\n}\n"
  },
  {
    "path": "src/Catalog.API/Setup/catalog.json",
    "content": "[\n  {\n    \"Id\": 1,\n    \"Type\": \"Footwear\",\n    \"Brand\": \"Daybird\",\n    \"Name\": \"Wanderer Black Hiking Boots\",\n    \"Description\": \"Daybird's Wanderer Hiking Boots in sleek black are perfect for all your outdoor adventures. These boots are made with a waterproof leather upper and a durable rubber sole for superior traction. With their cushioned insole and padded collar, these boots will keep you comfortable all day long.\",\n    \"Price\": 109.99\n  },\n  {\n    \"Id\": 2,\n    \"Type\": \"Climbing\",\n    \"Brand\": \"Gravitator\",\n    \"Name\": \"Summit Pro Harness\",\n    \"Description\": \"Conquer new heights with the Summit Pro Harness by Gravitator. This lightweight and durable climbing harness features adjustable leg loops and waist belt for a customized fit. With its vibrant blue color, you'll look stylish while maneuvering difficult routes. Safety is a top priority with a reinforced tie-in point and strong webbing loops.\",\n    \"Price\": 89.99\n  },\n  {\n    \"Id\": 3,\n    \"Type\": \"Ski/boarding\",\n    \"Brand\": \"WildRunner\",\n    \"Name\": \"Alpine Fusion Goggles\",\n    \"Description\": \"Enhance your skiing experience with the Alpine Fusion Goggles from WildRunner. These goggles offer full UV protection and anti-fog lenses to keep your vision clear on the slopes. With their stylish silver frame and orange lenses, you'll stand out from the crowd. Adjustable straps ensure a secure fit, while the soft foam padding provides comfort all day long.\",\n    \"Price\": 79.99\n  },\n  {\n    \"Id\": 4,\n    \"Type\": \"Bags\",\n    \"Brand\": \"Quester\",\n    \"Name\": \"Expedition Backpack\",\n    \"Description\": \"The Expedition Backpack by Quester is a must-have for every outdoor enthusiast. With its spacious interior and multiple pockets, you can easily carry all your gear and essentials. Made with durable nylon fabric, this backpack is built to withstand the toughest conditions. The orange accents add a touch of style to this functional backpack.\",\n    \"Price\": 129.99\n  },\n  {\n    \"Id\": 5,\n    \"Type\": \"Ski/boarding\",\n    \"Brand\": \"B&R\",\n    \"Name\": \"Blizzard Rider Snowboard\",\n    \"Description\": \"Get ready to ride the slopes with the Blizzard Rider Snowboard by B&R. This versatile snowboard is perfect for riders of all levels with its medium flex and twin shape. Its black and blue color scheme gives it a sleek and cool look. Whether you're carving turns or hitting the terrain park, this snowboard will help you shred with confidence.\",\n    \"Price\": 299.99\n  },\n  {\n    \"Id\": 6,\n    \"Type\": \"Trekking\",\n    \"Brand\": \"Raptor Elite\",\n    \"Name\": \"Carbon Fiber Trekking Poles\",\n    \"Description\": \"The Carbon Fiber Trekking Poles by Raptor Elite are the ultimate companion for your hiking adventures. Designed with lightweight carbon fiber shafts, these poles provide excellent support and durability. The comfortable and adjustable cork grips ensure a secure hold, while the blue accents add a stylish touch. Compact and collapsible, these trekking poles are easy to transport and store.\",\n    \"Price\": 69.99\n  },\n  {\n    \"Id\": 7,\n    \"Type\": \"Bags\",\n    \"Brand\": \"Solstix\",\n    \"Name\": \"Explorer 45L Backpack\",\n    \"Description\": \"The Explorer 45L Backpack by Solstix is perfect for your next outdoor expedition. Made with waterproof and tear-resistant materials, this backpack can withstand even the harshest weather conditions. With its spacious main compartment and multiple pockets, you can easily organize your gear. The green and black color scheme adds a rugged and adventurous edge.\",\n    \"Price\": 149.99\n  },\n  {\n    \"Id\": 8,\n    \"Type\": \"Jackets\",\n    \"Brand\": \"Grolltex\",\n    \"Name\": \"Frostbite Insulated Jacket\",\n    \"Description\": \"Stay warm and stylish with the Frostbite Insulated Jacket by Grolltex. Featuring a water-resistant outer shell and lightweight insulation, this jacket is perfect for cold weather adventures. The black and gray color combination and Grolltex logo add a touch of sophistication. With its adjustable hood and multiple pockets, this jacket offers both style and functionality.\",\n    \"Price\": 179.99\n  },\n  {\n    \"Id\": 9,\n    \"Type\": \"Navigation\",\n    \"Brand\": \"AirStrider\",\n    \"Name\": \"VenturePro GPS Watch\",\n    \"Description\": \"Navigate with confidence using the VenturePro GPS Watch by AirStrider. This rugged and durable watch features a built-in GPS, altimeter, and compass, allowing you to track your progress and find your way in any terrain. With its sleek black design and easy-to-read display, this watch is both stylish and practical. The VenturePro GPS Watch is a must-have for every adventurer.\",\n    \"Price\": 199.99\n  },\n  {\n    \"Id\": 10,\n    \"Type\": \"Cycling\",\n    \"Brand\": \"Green Equipment\",\n    \"Name\": \"Trailblazer Bike Helmet\",\n    \"Description\": \"Stay safe on your cycling adventures with the Trailblazer Bike Helmet by Green Equipment. This lightweight and durable helmet features an adjustable fit system and ventilation for added comfort. With its vibrant green color and sleek design, you'll stand out on the road. The Trailblazer Bike Helmet is perfect for all types of cycling, from mountain biking to road cycling.\",\n    \"Price\": 59.99\n  },\n  {\n    \"Id\": 11,\n    \"Type\": \"Climbing\",\n    \"Brand\": \"WildRunner\",\n    \"Name\": \"Vertical Journey Climbing Shoes\",\n    \"Description\": \"The Vertical Journey Climbing Shoes from WildRunner in sleek black are the perfect companion for any climbing enthusiast. With an aggressive down-turned toe, sticky rubber outsole, and reinforced heel cup for added support, these shoes offer ultimate performance on even the most challenging routes.\",\n    \"Price\": 129.99\n  },\n  {\n    \"Id\": 12,\n    \"Type\": \"Ski/boarding\",\n    \"Brand\": \"Zephyr\",\n    \"Name\": \"Powder Pro Snowboard\",\n    \"Description\": \"The Powder Pro Snowboard by Zephyr is designed for the ultimate ride through deep snow. Its floating camber allows for effortless turns and smooth maneuverability, while the lightweight carbon fiber construction ensures maximum control at high speeds. This board, available in vibrant turquoise, is a must-have for any backcountry shredder.\",\n    \"Price\": 399.00\n  },\n  {\n    \"Id\": 13,\n    \"Type\": \"Bags\",\n    \"Brand\": \"Daybird\",\n    \"Name\": \"Trailblaze hiking backpack\",\n    \"Description\": \"The Daybird Trailblaze backpack in forest green is a reliable and spacious bag for all your outdoor adventures. With a 40-liter capacity and durable ripstop fabric, this backpack provides ample storage and protection for your gear. Its ergonomic design and adjustable straps ensure a comfortable fit no matter the length of the hike.\",\n    \"Price\": 89.99\n  },\n  {\n    \"Id\": 14,\n    \"Type\": \"Bags\",\n    \"Brand\": \"Gravitator\",\n    \"Name\": \"Stellar Duffle Bag\",\n    \"Description\": \"The Stellar Duffle Bag from Gravitator is perfect for weekend getaways or short trips. Made from waterproof nylon and available in sleek black, it features multiple internal pockets and a separate shoe compartment to keep your belongings organized. With its adjustable shoulder strap and reinforced handles, this bag is as functional as it is stylish.\",\n    \"Price\": 59.99\n  },\n  {\n    \"Id\": 15,\n    \"Type\": \"Jackets\",\n    \"Brand\": \"Raptor Elite\",\n    \"Name\": \"Summit Pro Insulated Jacket\",\n    \"Description\": \"The Summit Pro Insulated Jacket by Raptor Elite is designed to keep you warm and dry in extreme conditions. With its waterproof and breathable construction, heat-sealed seams, and insulation made from recycled materials, this jacket is both eco-friendly and high-performance. Available in vibrant red, it also features a removable hood and plenty of storage pockets.\",\n    \"Price\": 249.99\n  },\n  {\n    \"Id\": 16,\n    \"Type\": \"Ski/boarding\",\n    \"Brand\": \"Solstix\",\n    \"Name\": \"Expedition 2022 Goggles\",\n    \"Description\": \"Solstix Expedition 2022 Goggles provide clear vision and optimal protection on the slopes. With an anti-fog lens, UV protection, and a comfortable foam lining, these goggles ensure a great fit and unrestricted vision even in challenging conditions. The matte black frame gives them a sleek and modern look.\",\n    \"Price\": 89.00\n  },\n  {\n    \"Id\": 17,\n    \"Type\": \"Climbing\",\n    \"Brand\": \"Legend\",\n    \"Name\": \"Apex Climbing Harness\",\n    \"Description\": \"The Apex Climbing Harness by Legend is a lightweight and durable harness designed for maximum comfort and safety. With adjustable leg loops, a contoured waistbelt, and a secure buckle system, this harness provides a secure fit for all-day climbing sessions. Available in bold orange, it also features gear loops for easy access to your equipment.\",\n    \"Price\": 89.99\n  },\n  {\n    \"Id\": 18,\n    \"Type\": \"Climbing\",\n    \"Brand\": \"Grolltex\",\n    \"Name\": \"Alpine Tech Crampons\",\n    \"Description\": \"The Alpine Tech Crampons by Grolltex are essential for icy and challenging mountain terrains. Made from strong and lightweight stainless steel, these crampons provide excellent traction and stability. Their simple adjustment system allows for easy fitting and quick attachment to most hiking boots. Available in silver, they are suitable for both beginners and experienced mountaineers.\",\n    \"Price\": 149.00\n  },\n  {\n    \"Id\": 19,\n    \"Type\": \"Footwear\",\n    \"Brand\": \"Green Equipment\",\n    \"Name\": \"EcoTrail Running Shoes\",\n    \"Description\": \"Experience the great outdoors while reducing your carbon footprint with the Green Equipment EcoTrail Running Shoes. Made from recycled materials, these shoes offer a lightweight, breathable, and flexible design in an earthy green color. With their durable Vibram outsole and cushioned midsole, they provide optimal comfort and grip on any trail.\",\n    \"Price\": 119.99\n  },\n  {\n    \"Id\": 20,\n    \"Type\": \"Navigation\",\n    \"Brand\": \"B&R\",\n    \"Name\": \"Explorer Biking Computer\",\n    \"Description\": \"The Explorer Biking Computer by B&R is the ultimate accessory for cyclists seeking data and navigation assistance. With its intuitive touchscreen display and GPS capabilities, it allows you to track your route, monitor performance metrics, and receive turn-by-turn directions. Its sleek black design and waterproof construction make it a reliable companion on all your cycling adventures.\",\n    \"Price\": 199.99\n  },\n  {\n    \"Id\": 21,\n    \"Type\": \"Footwear\",\n    \"Brand\": \"Legend\",\n    \"Name\": \"Trailblazer Black Hiking Shoes\",\n    \"Description\": \"The Legend Trailblazer is a versatile hiking shoe designed to provide unparalleled durability and comfort on any adventure. With its black color, these shoes offer a sleek and minimalist style. The shoes feature a waterproof GORE-TEX lining, Vibram rubber outsole for enhanced traction, and a reinforced toe cap for added protection. Conquer any trail with confidence in the Legend Trailblazer Black Hiking Shoes.\",\n    \"Price\": 129.99\n  },\n  {\n    \"Id\": 22,\n    \"Type\": \"Ski/boarding\",\n    \"Brand\": \"Raptor Elite\",\n    \"Name\": \"Venture 2022 Snowboard\",\n    \"Description\": \"The Raptor Elite Venture 2022 Snowboard is a true all-mountain performer, perfect for riders of all levels. Its sleek design, combined with the vibrant blue color, makes it stand out on the slopes. The snowboard features a responsive camber profile, carbon fiber laminates for enhanced stability, and a sintered base for maximum speed. Take your snowboarding skills to new heights with the Raptor Elite Venture 2022 Snowboard.\",\n    \"Price\": 499.00\n  },\n  {\n    \"Id\": 23,\n    \"Type\": \"Climbing\",\n    \"Brand\": \"Zephyr\",\n    \"Name\": \"Summit Pro Climbing Harness\",\n    \"Description\": \"The Zephyr Summit Pro Climbing Harness is designed for professional climbers who demand the utmost in reliability and performance. Available in a striking orange color, this harness features 30kN rated webbing, speed-adjust buckles, and multiple gear loops for easy organization. With its lightweight design, the Summit Pro Harness offers unmatched comfort and freedom of movement. Reach new heights of confidence with the Zephyr Summit Pro Climbing Harness.\",\n    \"Price\": 189.99\n  },\n  {\n    \"Id\": 24,\n    \"Type\": \"Bags\",\n    \"Brand\": \"WildRunner\",\n    \"Name\": \"Ridgevent Stealth Hiking Backpack\",\n    \"Description\": \"The WildRunner Ridgevent Stealth Hiking Backpack is the ultimate companion for your outdoor adventures. With its stealthy red color, this backpack combines style with functionality. Made from durable nylon and featuring multiple compartments, this backpack offers ample storage space for all your essentials. Whether you're venturing into the mountains or exploring hidden trails, the Ridgevent Stealth Hiking Backpack has got you covered.\",\n    \"Price\": 69.99\n  },\n  {\n    \"Id\": 25,\n    \"Type\": \"Cycling\",\n    \"Brand\": \"Daybird\",\n    \"Name\": \"Stealth Lite Bike Helmet\",\n    \"Description\": \"The Daybird Stealth Lite Bike Helmet is designed for cyclists who value both safety and style. With its sleek matte silver color, this helmet will make you stand out on the road. The helmet features a lightweight in-mold construction, adjustable retention system, and multiple ventilation channels for optimal airflow. Stay protected and look cool with the Daybird Stealth Lite Bike Helmet.\",\n    \"Price\": 89.99\n  },\n  {\n    \"Id\": 26,\n    \"Type\": \"Climbing\",\n    \"Brand\": \"Gravitator\",\n    \"Name\": \"Gravity Beam Climbing Rope\",\n    \"Description\": \"The Gravitator Gravity Beam Climbing Rope is the perfect companion for vertical endeavors. This high-quality climbing rope features a kernmantle construction, providing excellent strength and durability. With its vibrant yellow color, the Gravity Beam Rope is highly visible and easy to work with. Whether you're tackling steep rock faces or conquering frozen waterfalls, trust the Gravitator Gravity Beam Climbing Rope to get you to the top.\",\n    \"Price\": 179.99\n  },\n  {\n    \"Id\": 27,\n    \"Type\": \"Bags\",\n    \"Brand\": \"Green Equipment\",\n    \"Name\": \"EcoLodge 45L Travel Backpack\",\n    \"Description\": \"The Green Equipment EcoLodge 45L Travel Backpack is a sustainable and versatile option for all your travel needs. With its earth-inspired green color, this backpack is not only stylish but also environmentally friendly. Made from recycled materials, this backpack features multiple compartments, a padded laptop sleeve, and durable zippers. Explore the world with the Green Equipment EcoLodge 45L Travel Backpack.\",\n    \"Price\": 129.00\n  },\n  {\n    \"Id\": 28,\n    \"Type\": \"Jackets\",\n    \"Brand\": \"Solstix\",\n    \"Name\": \"Alpine Peak Down Jacket\",\n    \"Description\": \"The Solstix Alpine Peak Down Jacket is crafted for extreme cold conditions. With its bold red color and sleek design, this jacket combines style with functionality. Made with high-quality goose down insulation, the Alpine Peak Jacket provides exceptional warmth and comfort. The jacket features a removable hood, adjustable cuffs, and multiple zippered pockets for storage. Conquer the harshest weather with the Solstix Alpine Peak Down Jacket.\",\n    \"Price\": 249.99\n  },\n  {\n    \"Id\": 29,\n    \"Type\": \"Navigation\",\n    \"Brand\": \"B&R\",\n    \"Name\": \"Pulse Recon Tactical GPS Watch\",\n    \"Description\": \"The B&R Pulse Recon Tactical GPS Watch is a must-have for outdoor enthusiasts. This reliable navigation tool features a built-in GPS, altimeter, compass, and multiple sports modes. With its military green color and durable construction, the Pulse Recon watch is built to withstand your toughest adventures. Stay on track and keep track of your performance with the B&R Pulse Recon Tactical GPS Watch.\",\n    \"Price\": 169.00\n  },\n  {\n    \"Id\": 30,\n    \"Type\": \"Ski/boarding\",\n    \"Brand\": \"Gravitator\",\n    \"Name\": \"Zero Gravity Ski Goggles\",\n    \"Description\": \"The Gravitator Zero Gravity Ski Goggles combine style, performance, and comfort for the ultimate ski experience. With their sleek white frame and red mirrored lenses, these goggles offer a futuristic look on the slopes. The goggles feature an anti-fog coating, 100% UV protection, and an adjustable strap for a secure fit. Enhance your vision and carve your way down the slopes with the Gravitator Zero Gravity Ski Goggles.\",\n    \"Price\": 79.99\n  },\n  {\n    \"Id\": 31,\n    \"Type\": \"Climbing\",\n    \"Brand\": \"Legend\",\n    \"Name\": \"Guardian Blue Chalk Bag\",\n    \"Description\": \"Stay focused on your route with the Guardian Blue Chalk Bag by Legend. This durable bag features a spacious compartment for your chalk, a drawstring closure, and a waist belt for easy access while climbing. The vibrant blue color adds a stylish touch to your climbing gear.\",\n    \"Price\": 21.99\n  },\n  {\n    \"Id\": 32,\n    \"Type\": \"Ski/boarding\",\n    \"Brand\": \"Gravitator\",\n    \"Name\": \"Cosmic Purple Snowboard\",\n    \"Description\": \"Conquer the slopes with the Cosmic Purple Snowboard by Gravitator. This freestyle board delivers a perfect balance of control and maneuverability. Its bright purple design is complemented by the Gravitator emblem, sure to turn heads on the mountain.\",\n    \"Price\": 419.99\n  },\n  {\n    \"Id\": 33,\n    \"Type\": \"Footwear\",\n    \"Brand\": \"WildRunner\",\n    \"Name\": \"Venture Grey Trail Shoes\",\n    \"Description\": \"Hit the trails in style and comfort with the Venture Grey Trail Shoes by WildRunner. Constructed with breathable mesh and a rugged outsole, these shoes provide excellent traction and long-lasting durability. The versatile grey color makes them suitable for any adventure.\",\n    \"Price\": 79.99\n  },\n  {\n    \"Id\": 34,\n    \"Type\": \"Cycling\",\n    \"Brand\": \"AirStrider\",\n    \"Name\": \"Velocity Red Bike Helmet\",\n    \"Description\": \"Protect yourself while cycling in style with the Velocity Red Bike Helmet by AirStrider. This lightweight helmet features a streamlined design, adjustable straps, and ventilation channels for optimal airflow. Stay safe on the road or trails with this vibrant red helmet.\",\n    \"Price\": 54.99\n  },\n  {\n    \"Id\": 35,\n    \"Type\": \"Trekking\",\n    \"Brand\": \"Raptor Elite\",\n    \"Name\": \"Carbon Fiber Trekking Poles\",\n    \"Description\": \"Hike with confidence using the Raptor Elite Carbon Fiber Trekking Poles. These lightweight and durable poles provide stability on various terrains and reduce strain on your joints. With an ergonomic grip and adjustable length, these poles are a must-have for your outdoor adventures.\",\n    \"Price\": 99.00\n  },\n  {\n    \"Id\": 36,\n    \"Type\": \"Bags\",\n    \"Brand\": \"B&R\",\n    \"Name\": \"Excursion 20L Daypack\",\n    \"Description\": \"The Excursion 20L Daypack by B&R is the perfect companion for your hiking or camping trips. Made from durable waterproof nylon, this spacious pack features multiple pockets, adjustable straps, and a padded back for enhanced comfort. The sleek design and versatile white color make it a stylish choice.\",\n    \"Price\": 64.99\n  },\n  {\n    \"Id\": 37,\n    \"Type\": \"Jackets\",\n    \"Brand\": \"Zephyr\",\n    \"Name\": \"Stormbreaker Waterproof Jacket\",\n    \"Description\": \"Take on any weather with the Stormbreaker Waterproof Jacket by Zephyr. This jacket offers superior protection with its fully waterproof and windproof design. The bold red color, coupled with the Zephyr logo, adds a stylish touch to your outdoor look. Stay dry and comfortable during your adventures.\",\n    \"Price\": 139.99\n  },\n  {\n    \"Id\": 38,\n    \"Type\": \"Navigation\",\n    \"Brand\": \"Solstix\",\n    \"Name\": \"Pathfinder Portable GPS\",\n    \"Description\": \"Never lose your way with the Pathfinder Portable GPS by Solstix. This compact and reliable navigation device features a color display, preloaded maps, and advanced tracking capabilities. With its intuitive interface and long battery life, you can explore confidently wherever you go.\",\n    \"Price\": 199.00\n  },\n  {\n    \"Id\": 39,\n    \"Type\": \"Ski/boarding\",\n    \"Brand\": \"Daybird\",\n    \"Name\": \"Midnight Blue Goggles\",\n    \"Description\": \"Enhance your snowboarding experience with the Midnight Blue Goggles by Daybird. These goggles offer a wide field of vision, anti-fog coating, and UV protection to keep your eyes protected on the slopes. The sleek design and blue tinted lens add a touch of style to your riding gear.\",\n    \"Price\": 89.99\n  },\n  {\n    \"Id\": 40,\n    \"Type\": \"Footwear\",\n    \"Brand\": \"Green Equipment\",\n    \"Name\": \"EcoTrek Trail Running Shoes\",\n    \"Description\": \"Hit the trails with the EcoTrek Trail Running Shoes by Green Equipment. Designed with eco-friendly materials, these shoes feature a comfortable fit, responsive cushioning, and a durable outsole for optimal grip on rugged terrains. The forest green color is inspired by nature and adds a refreshing touch to your outdoor look.\",\n    \"Price\": 99.99\n  },\n  {\n    \"Id\": 41,\n    \"Type\": \"Footwear\",\n    \"Brand\": \"WildRunner\",\n    \"Name\": \"Trekker Clear Hiking Shoes\",\n    \"Description\": \"The Trekker Clear Hiking Shoes from WildRunner are designed for the adventurous hiker who seeks both comfort and durability. The transparent shoes feature a waterproof and breathable upper fabric, a rugged carbon-infused sole for excellent traction, and a shock-absorbing midsole for enhanced comfort on long hikes.\",\n    \"Price\": 84.99\n  },\n  {\n    \"Id\": 42,\n    \"Type\": \"Ski/boarding\",\n    \"Brand\": \"Gravitator\",\n    \"Name\": \"Gravity 5000 All-Mountain Skis\",\n    \"Description\": \"Take on any slope confidently with the Gravity 5000 All-Mountain Skis by Gravitator. These skis feature a versatile design that excels in all conditions, from powder to hardpack. They are equipped with a lightweight wood core, carbon inserts for responsiveness, and ABS sidewalls for added durability. These skis come in a striking blue color.\",\n    \"Price\": 699.00\n  },\n  {\n    \"Id\": 43,\n    \"Type\": \"Ski/boarding\",\n    \"Brand\": \"Legend\",\n    \"Name\": \"Glacier Frost Snowboard\",\n    \"Description\": \"Tame the snow-covered peaks with the Glacier Frost Snowboard from Legend. This high-performance board is constructed with a hybrid camber profile for excellent edge control and superior maneuverability. The board is built with a carbon fiber composite core for lightweight strength and optimal flex, enabling you to take your snowboarding skills to new heights. Available in a cool white color with vibrant frost graphic.\",\n    \"Price\": 419.99\n  },\n  {\n    \"Id\": 44,\n    \"Type\": \"Climbing\",\n    \"Brand\": \"Raptor Elite\",\n    \"Name\": \"Summit Pro Climbing Harness\",\n    \"Description\": \"Conquer the highest peaks with the Summit Pro Climbing Harness by Raptor Elite. This harness features a lightweight and breathable construction, complete with adjustable leg loops for a personalized fit. It has reinforced tie-in points for maximum safety and durability. The vivid green color adds a touch of style to your climbing gear.\",\n    \"Price\": 109.99\n  },\n  {\n    \"Id\": 45,\n    \"Type\": \"Trekking\",\n    \"Brand\": \"Solstix\",\n    \"Name\": \"Elemental 3-Season Tent\",\n    \"Description\": \"Experience the great outdoors with the Elemental 3-Season Tent by Solstix. This lightweight and compact tent is perfect for backpacking adventures. It offers ample space for two people and features a durable waterproof fabric, sturdy aluminum poles, and ventilation panels for optimal airflow. The vibrant green color adds a touch of visibility to your camping setup.\",\n    \"Price\": 189.99\n  },\n  {\n    \"Id\": 46,\n    \"Type\": \"Cycling\",\n    \"Brand\": \"B&R\",\n    \"Name\": \"Zenith Cycling Jersey\",\n    \"Description\": \"Get ready to hit the road with the Zenith Cycling Jersey by B&R. This high-performance jersey is made from moisture-wicking fabric to keep you cool and dry during intense rides. It features a full-length zipper, three rear pockets for storage, and reflective accents for increased visibility in low-light conditions. Available in a vibrant red color.\",\n    \"Price\": 64.99\n  },\n  {\n    \"Id\": 47,\n    \"Type\": \"Climbing\",\n    \"Brand\": \"Grolltex\",\n    \"Name\": \"Edge Pro Ice Axe\",\n    \"Description\": \"Take your ice climbing adventures to the next level with the Edge Pro Ice Axe from Grolltex. This axe features a lightweight aluminum shaft, a durable stainless steel pick, and a comfortable hand grip for maximum control. Perfect for tackling steep ice walls and mixed alpine terrain. The sleek orange color adds a touch of sophistication to your gear.\",\n    \"Price\": 129.00\n  },\n  {\n    \"Id\": 48,\n    \"Type\": \"Bags\",\n    \"Brand\": \"Zephyr\",\n    \"Name\": \"Trailblazer 45L Backpack\",\n    \"Description\": \"Take everything you need for your next adventure with the Trailblazer 45L Backpack by Zephyr. This spacious backpack features multiple compartments for easy organization, adjustable shoulder straps and hip belt for a customized fit, and durable waterproof construction for ultimate protection against the elements. The classic yellow color is timeless and versatile.\",\n    \"Price\": 124.99\n  },\n  {\n    \"Id\": 49,\n    \"Type\": \"Jackets\",\n    \"Brand\": \"Daybird\",\n    \"Name\": \"Arctic Shield Insulated Jacket\",\n    \"Description\": \"Stay warm and stylish in the Arctic Shield Insulated Jacket by Daybird. This jacket features a water-resistant outer shell, insulated fill for exceptional warmth, and a detachable hood for added versatility. The sleek pink color is perfect for any outdoor occasion.\",\n    \"Price\": 169.99\n  },\n  {\n    \"Id\": 50,\n    \"Type\": \"Navigation\",\n    \"Brand\": \"AirStrider\",\n    \"Name\": \"Astro GPS Navigator\",\n    \"Description\": \"Never get lost on your outdoor adventures with the Astro GPS Navigator by AirStrider. This compact and rugged device comes loaded with topographic maps, GPS tracking, waypoint storage, and a long-lasting battery. It is equipped with a high-resolution color display for easy navigation in any lighting condition. Available in a sleek gray color.\",\n    \"Price\": 249.99\n  },\n  {\n    \"Id\": 51,\n    \"Type\": \"Climbing\",\n    \"Brand\": \"Grolltex\",\n    \"Name\": \"SummitStone Chalk Bag\",\n    \"Description\": \"The SummitStone Chalk Bag in forest green is a must-have for climbers seeking adventure. Keep your hands dry and have easy access to chalk with this durable and compact bag. It features a drawstring closure, adjustable waist strap, and a Loop-Slider buckle for easy attachment to harnesses.\",\n    \"Price\": 29.99\n  },\n  {\n    \"Id\": 52,\n    \"Type\": \"Bags\",\n    \"Brand\": \"Legend\",\n    \"Name\": \"TrailHug 50L Backpack\",\n    \"Description\": \"The TrailHug 50L Backpack in navy blue is a perfect companion for all your hiking adventures. Made from lightweight and water-resistant nylon, this backpack features adjustable padded straps, multiple compartments for organized storage, and a breathable back panel for added comfort. It also comes with a handy built-in rain cover for unexpected showers.\",\n    \"Price\": 129.99\n  },\n  {\n    \"Id\": 53,\n    \"Type\": \"Ski/boarding\",\n    \"Brand\": \"Daybird\",\n    \"Name\": \"Raven Swift Snowboard\",\n    \"Description\": \"The Raven Swift Snowboard is ready to take you on thrilling rides down the slopes. With its striking white design and black logo, this all-mountain board is perfect for riders of all skill levels. It features a camber profile for stability and pop, and a medium flex for smooth turns and responsive control. Get ready to fly and carve like never before!\",\n    \"Price\": 349.00\n  },\n  {\n    \"Id\": 54,\n    \"Type\": \"Trekking\",\n    \"Brand\": \"Gravitator\",\n    \"Name\": \"Nebula Pro Headlamp\",\n    \"Description\": \"Illuminate your outdoor adventures with the Nebula Pro Headlamp. Its sleek design and water-resistant construction in neon color make it perfect for night hikes, camping trips, and emergencies. With 500 lumens of bright white light, adjustable brightness modes, and a rechargeable battery, this headlamp will light up the darkness and keep you safe.\",\n    \"Price\": 59.99\n  },\n  {\n    \"Id\": 55,\n    \"Type\": \"Jackets\",\n    \"Brand\": \"Solstix\",\n    \"Name\": \"Vigor 2.0 Insulated Jacket\",\n    \"Description\": \"Stay warm and stylish on the slopes with the Vigor 2.0 Insulated Jacket in vibrant red. This waterproof and breathable jacket is made with a 2-layer technical shell and features a detachable hood, adjustable cuffs, and multiple pockets for storage. With its modern design and ergonomic fit, it's the perfect outer layer for your winter adventures.\",\n    \"Price\": 189.99\n  },\n  {\n    \"Id\": 56,\n    \"Type\": \"Bags\",\n    \"Brand\": \"B&R\",\n    \"Name\": \"Traveler's Companion Duffel Bag\",\n    \"Description\": \"Whether you're embarking on a weekend getaway or a month-long expedition, the Traveler's Companion Duffel Bag has you covered. Made from durable waxed canvas in earthy brown, this versatile bag features multiple carry options, including shoulder straps and handles, and multiple compartments for organized packing. It even has a padded laptop sleeve, making it great for both adventure and work.\",\n    \"Price\": 79.99\n  },\n  {\n    \"Id\": 57,\n    \"Type\": \"Footwear\",\n    \"Brand\": \"Zephyr\",\n    \"Name\": \"Ascend XT Trail Running Shoes\",\n    \"Description\": \"Take on any trail with confidence in the Ascend XT Trail Running Shoes in charcoal gray. These lightweight yet rugged shoes offer excellent grip and support, thanks to their durable rubber outsole and advanced cushioning technology. The breathable mesh upper keeps your feet cool when the adventure heats up. It's time to push your limits and conquer the great outdoors.\",\n    \"Price\": 109.99\n  },\n  {\n    \"Id\": 58,\n    \"Type\": \"Cycling\",\n    \"Brand\": \"Raptor Elite\",\n    \"Name\": \"VelociX 2000 Bike Helmet\",\n    \"Description\": \"Protect your head in style with the VelociX 2000 Bike Helmet in glossy black. This aerodynamic helmet features an adjustable fit system, detachable visor, and 14 ventilation channels to keep you cool during intense rides. With its sleek design and lightweight construction, it's the perfect choice for road cycling, mountain biking, and everything in between.\",\n    \"Price\": 79.99\n  },\n  {\n    \"Id\": 59,\n    \"Type\": \"Navigation\",\n    \"Brand\": \"Quester\",\n    \"Name\": \"TrailSeeker GPS Watch\",\n    \"Description\": \"Stay on track and explore new trails with the TrailSeeker GPS Watch. With its durable design in stealth black, this watch is packed with features like GPS navigation, heart rate monitoring, and activity tracking. It also offers a long battery life, so you can keep going without worrying about recharging. The TrailSeeker is the ultimate companion for outdoor enthusiasts who love to explore.\",\n    \"Price\": 149.99\n  },\n  {\n    \"Id\": 60,\n    \"Type\": \"Ski/boarding\",\n    \"Brand\": \"WildRunner\",\n    \"Name\": \"SummitRider Snowboard Boots\",\n    \"Description\": \"Conquer the mountains in style with the SummitRider Snowboard Boots in matte black. These high-performance boots combine comfort, durability, and response to enhance your riding experience. Featuring a heat-moldable liner, dual-zone lacing system, and impact-resistant outsole, they provide a precise and snug fit, ensuring maximum control on any terrain. Get ready to take on the slopes like a pro!\",\n    \"Price\": 249.00\n  },\n  {\n    \"Id\": 61,\n    \"Type\": \"Footwear\",\n    \"Brand\": \"WildRunner\",\n    \"Name\": \"Trailblaze Steel-Blue Hiking Shoes\",\n    \"Description\": \"Explore the great outdoors with the Trailblaze Steel-Blue Hiking Shoes by WildRunner. These rugged and durable shoes feature a steel-blue color, a waterproof membrane, and a high-traction rubber outsole for superior grip on any terrain. The breathable upper keeps your feet cool and comfortable, while the reinforced toe cap adds extra protection. Perfect for hiking, camping, and other outdoor adventures.\",\n    \"Price\": 129.99\n  },\n  {\n    \"Id\": 62,\n    \"Type\": \"Ski/boarding\",\n    \"Brand\": \"Daybird\",\n    \"Name\": \"Shadow Black Snowboard\",\n    \"Description\": \"Conquer the slopes with the Daybird Shadow Black Snowboard. This sleek and stylish snowboard features a black colorway, a camber profile for maximum stability, and a medium-flex rating for responsive turns and tricks. Its lightweight construction ensures easy maneuverability, while the sintered base provides excellent speed and durability. Whether you're shredding on groomed runs or exploring the backcountry, this snowboard is designed to deliver peak performance.\",\n    \"Price\": 379.00\n  },\n  {\n    \"Id\": 63,\n    \"Type\": \"Climbing\",\n    \"Brand\": \"Raptor Elite\",\n    \"Name\": \"Razor Climbing Harness\",\n    \"Description\": \"Reach new heights with the Raptor Elite Razor Climbing Harness. This lightweight and breathable harness is designed for maximum comfort and performance. With its adjustable waist and leg loops, it offers a secure and customized fit. The razor-shaped webbing adds a stylish touch to the blue color of the harness. Featuring durable construction and reinforced tie-in points, this harness is a must-have for climbers of all levels.\",\n    \"Price\": 94.99\n  },\n  {\n    \"Id\": 64,\n    \"Type\": \"Bags\",\n    \"Brand\": \"Green Equipment\",\n    \"Name\": \"EcoVenture Olive Green Backpack\",\n    \"Description\": \"Embark on your next adventure with the Green Equipment EcoVenture Olive Green Backpack. Made from recycled materials, this sustainable backpack is as eco-friendly as it is functional. It features a spacious main compartment, multiple pockets for organizing your gear, and adjustable padded shoulder straps for comfortable carrying. The olive green color adds a touch of nature to your outdoor excursions.\",\n    \"Price\": 69.99\n  },\n  {\n    \"Id\": 65,\n    \"Type\": \"Cycling\",\n    \"Brand\": \"Solstix\",\n    \"Name\": \"Sprint PRO Carbon Cycling Helmet\",\n    \"Description\": \"Stay safe while cycling with the Solstix Sprint PRO Carbon Cycling Helmet. This high-performance helmet is crafted from carbon fiber for optimal impact protection and durability. It features an aerodynamic design, adjustable fit system, and ventilation channels to keep you cool on long rides. The rainbow color with the Solstix emblem adds a touch of style to your cycling adventures.\",\n    \"Price\": 179.99\n  },\n  {\n    \"Id\": 66,\n    \"Type\": \"Navigation\",\n    \"Brand\": \"B&R\",\n    \"Name\": \"Compass Pro A-320 Professional Compass\",\n    \"Description\": \"Navigate with precision using the B&R Compass Pro A-320 Professional Compass. Designed for outdoor enthusiasts and professionals alike, this compass features a liquid-filled housing for accurate readings, a rotating bezel for easy navigation, and a lanyard for convenient carrying. The gunmetal color with white markings ensures clarity and visibility even in low-light conditions. Get ready to explore the wilderness with confidence.\",\n    \"Price\": 59.99\n  },\n  {\n    \"Id\": 67,\n    \"Type\": \"Bags\",\n    \"Brand\": \"Quester\",\n    \"Name\": \"Venture 2.0 40L Waterproof Duffel Bag\",\n    \"Description\": \"Pack your gear in the Quester Venture 2.0 40L Waterproof Duffel Bag. This versatile duffel bag is made from waterproof nylon material and features taped seams to keep your belongings safe from the elements. It offers a spacious main compartment, external zippered pockets, and adjustable shoulder straps for easy carrying. The vibrant orange color adds a pop of excitement to your outdoor adventures.\",\n    \"Price\": 79.99\n  },\n  {\n    \"Id\": 68,\n    \"Type\": \"Jackets\",\n    \"Brand\": \"Grolltex\",\n    \"Name\": \"Mens Horizon 80s Softshell Jacket\",\n    \"Description\": \"Stay protected from the elements in the Grolltex Mens Horizon 80s Softshell Jacket. Made from a water-resistant and breathable fabric in retro 1980s style, this jacket keeps you dry and comfortable in any weather. It features multiple colors, a detachable hood, adjustable cuffs, and multiple pockets for storing your essentials. Whether you're hiking, skiing, or exploring the city, this jacket combines style and functionality.\",\n    \"Price\": 169.99\n  },\n  {\n    \"Id\": 69,\n    \"Type\": \"Navigation\",\n    \"Brand\": \"Gravitator\",\n    \"Name\": \"Expedition 200 GPS Navigator\",\n    \"Description\": \"Navigate with confidence using the Gravitator Expedition 200 GPS Navigator. This rugged and reliable navigator features a built-in GPS antenna for accurate positioning, preloaded maps, and a user-friendly interface with intuitive controls. The black color with the Gravitator logo complements the sleek design. With its long battery life and durable construction, this navigator is your ultimate outdoor companion.\",\n    \"Price\": 299.00\n  },\n  {\n    \"Id\": 70,\n    \"Type\": \"Trekking\",\n    \"Brand\": \"WildRunner\",\n    \"Name\": \"GripTrek Hiking Poles\",\n    \"Description\": \"The GripTrek hiking poles by WildRunner are a must-have for adventurers. With their durable aluminum construction and anti-slip handles, these poles provide stability and support on any terrain. Available in sleek yellow, these hiking poles are perfect for tackling steep inclines and rough trails.\",\n    \"Price\": 79.99\n  },\n  {\n    \"Id\": 71,\n    \"Type\": \"Footwear\",\n    \"Brand\": \"Daybird\",\n    \"Name\": \"Explorer Frost Boots\",\n    \"Description\": \"The Explorer Frost Boots by Daybird are the perfect companion for cold-weather adventures. These premium boots are designed with a waterproof and insulated shell, keeping your feet warm and protected in icy conditions. The sleek black design with blue accents adds a touch of style to your outdoor gear.\",\n    \"Price\": 149.99\n  },\n  {\n    \"Id\": 72,\n    \"Type\": \"Ski/boarding\",\n    \"Brand\": \"Gravitator\",\n    \"Name\": \"GravityZone All-Mountain Skis\",\n    \"Description\": \"Take your skiing to new heights with the GravityZone All-Mountain Skis by Gravitator. These high-performance skis are designed for precision and control on all types of terrain. The sleek metallic blue design will make you stand out on the slopes while the carbon fiber construction ensures lightweight durability.\",\n    \"Price\": 699.00\n  },\n  {\n    \"Id\": 73,\n    \"Type\": \"Ski/boarding\",\n    \"Brand\": \"WildRunner\",\n    \"Name\": \"Omni-Snow Dual Snowboard\",\n    \"Description\": \"Unleash your snowboarding skills with the Omni-Snow Dual Snowboard by WildRunner. This innovative design combines the maneuverability of a skateboard with the speed and stability of a snowboard. The vibrant red and black color scheme adds a dash of excitement to your snowboarding adventures.\",\n    \"Price\": 289.99\n  },\n  {\n    \"Id\": 74,\n    \"Type\": \"Climbing\",\n    \"Brand\": \"Raptor Elite\",\n    \"Name\": \"Apex Climbing Harness\",\n    \"Description\": \"Conquer the heights with the Apex Climbing Harness by Raptor Elite. This harness is constructed with high-strength nylon webbing and features adjustable leg loops for a secure and comfortable fit. The sleek white design with a vibrant orange emblem ensures you'll look good while tackling challenging routes.\",\n    \"Price\": 99.99\n  },\n  {\n    \"Id\": 75,\n    \"Type\": \"Footwear\",\n    \"Brand\": \"AirStrider\",\n    \"Name\": \"TrailTracker Hiking Shoes\",\n    \"Description\": \"The TrailTracker Hiking Shoes by AirStrider are built to handle any terrain. These lightweight and breathable shoes feature a rugged rubber sole for excellent traction and stability. The cool gray color with green accents adds a touch of style to your hiking ensemble.\",\n    \"Price\": 89.99\n  },\n  {\n    \"Id\": 76,\n    \"Type\": \"Cycling\",\n    \"Brand\": \"B&R\",\n    \"Name\": \"Fusion Carbon Cycling Helmet\",\n    \"Description\": \"Protect yourself on two wheels with the Fusion Carbon Cycling Helmet by B&R. This helmet is made from lightweight carbon fiber and features an aerodynamic design for maximum speed. The colorful finish with a bold blue stripe will make you stand out on the road.\",\n    \"Price\": 159.00\n  },\n  {\n    \"Id\": 77,\n    \"Type\": \"Trekking\",\n    \"Brand\": \"XE\",\n    \"Name\": \"Survivor 2-Person Tent\",\n    \"Description\": \"Gear up for your next adventure with the Survivor 2-Person Tent by XE. This rugged tent is made from durable ripstop nylon and features a waterproof coating to keep you dry in any weather. The vibrant orange color ensures high visibility in the wild.\",\n    \"Price\": 249.99\n  },\n  {\n    \"Id\": 78,\n    \"Type\": \"Bags\",\n    \"Brand\": \"Solstix\",\n    \"Name\": \"Basecamp Duffle Bag\",\n    \"Description\": \"The Basecamp Duffle Bag by Solstix is the ultimate adventure companion. This spacious bag is made from durable nylon and features multiple compartments for optimal organization. Its sleek red design with gray accents exudes both style and functionality.\",\n    \"Price\": 129.00\n  },\n  {\n    \"Id\": 79,\n    \"Type\": \"Jackets\",\n    \"Brand\": \"Legend\",\n    \"Name\": \"Everest Insulated Jacket\",\n    \"Description\": \"Conquer the cold with the Everest Insulated Jacket by Legend. This jacket combines warmth and style with its insulated design and sleek grey color. The water-resistant shell will keep you dry during unexpected showers while the cozy fleece lining adds extra comfort.\",\n    \"Price\": 179.99\n  },\n  {\n    \"Id\": 80,\n    \"Type\": \"Navigation\",\n    \"Brand\": \"XE\",\n    \"Name\": \"Pathfinder GPS Watch\",\n    \"Description\": \"Navigate with confidence using the Pathfinder GPS Watch by XE. This feature-packed watch includes GPS tracking, altimeter, barometer, and compass functions to guide you on your outdoor adventures. The sleek pink design with a vibrant green dial adds a sporty touch to your wrist.\",\n    \"Price\": 199.00\n  },\n  {\n    \"Id\": 81,\n    \"Type\": \"Footwear\",\n    \"Brand\": \"AirStrider\",\n    \"Name\": \"Trail Breeze Hiking Shoes\",\n    \"Description\": \"Experience the ultimate comfort and stability with the Trail Breeze hiking shoes by AirStrider. These lightweight shoes feature a breathable mesh upper in vivid blue, providing excellent airflow on hot summer hikes. The durable rubber outsole offers exceptional grip, ensuring you stay steady on any terrain.\",\n    \"Price\": 109.99\n  },\n  {\n    \"Id\": 82,\n    \"Type\": \"Ski/boarding\",\n    \"Brand\": \"WildRunner\",\n    \"Name\": \"Maverick Pro Ski Goggles\",\n    \"Description\": \"Conquer the slopes in style with the Maverick Pro ski goggles by WildRunner. Designed for maximum performance, these goggles feature a sleek black frame and mirrored, polarized lenses that reduce glare, enhancing your visibility. With a comfortable foam lining and adjustable strap, these goggles provide a secure and snug fit.\",\n    \"Price\": 139.99\n  },\n  {\n    \"Id\": 83,\n    \"Type\": \"Ski/boarding\",\n    \"Brand\": \"Zephyr\",\n    \"Name\": \"Blizzard Freestyle Snowboard\",\n    \"Description\": \"Unleash your freestyle skills on the slopes with the Blizzard snowboard from Zephyr. Featuring a vibrant orange and black design, this snowboard is perfect for riders who crave speed and control. Constructed with a durable bamboo core and carbon fiber reinforcement, the Blizzard offers an optimal blend of flexibility and responsiveness.\",\n    \"Price\": 379.00\n  },\n  {\n    \"Id\": 84,\n    \"Type\": \"Climbing\",\n    \"Brand\": \"Gravitator\",\n    \"Name\": \"Gravity Harness\",\n    \"Description\": \"Reach new heights with the Gravitator Gravity harness. With its innovative design and sturdy construction, this harness ensures your safety while climbing. The sleek black and red color scheme adds a touch of style. It offers maximum comfort and freedom of movement, giving you the confidence to conquer any climbing challenge.\",\n    \"Price\": 89.99\n  },\n  {\n    \"Id\": 85,\n    \"Type\": \"Trekking\",\n    \"Brand\": \"Daybird\",\n    \"Name\": \"LumenHead Headlamp\",\n    \"Description\": \"Illuminate your outdoor adventures with the Daybird LumenHead headlamp. This compact yet powerful headlamp features a bright LED light in a vibrant green housing. With multiple lighting modes, including a red light for preserving night vision, the LumenHead provides exceptional visibility in any conditions.\",\n    \"Price\": 49.99\n  },\n  {\n    \"Id\": 86,\n    \"Type\": \"Cycling\",\n    \"Brand\": \"Raptor Elite\",\n    \"Name\": \"ProVent Bike Helmet\",\n    \"Description\": \"Stay safe and stylish on your cycling adventures with the Raptor Elite ProVent bike helmet. This sleek helmet features a matte black finish with striking red accents. The ProVent technology ensures optimal airflow, keeping you cool and comfortable. With its adjustable fit system and removable visor, this helmet is perfect for both casual and professional riders.\",\n    \"Price\": 79.99\n  },\n  {\n    \"Id\": 87,\n    \"Type\": \"Trekking\",\n    \"Brand\": \"XE\",\n    \"Name\": \"Nomad 2-Person Tent\",\n    \"Description\": \"Embark on your next camping expedition with the XE Nomad 2-person tent. Designed for rugged outdoor conditions, this tent features a durable waterproof fabric in earthy tones. The spacious interior and easy-to-use setup make it ideal for comfortable camping. With its innovative ventilation system, you'll stay cool and dry throughout the night.\",\n    \"Price\": 229.00\n  },\n  {\n    \"Id\": 88,\n    \"Type\": \"Bags\",\n    \"Brand\": \"Green Equipment\",\n    \"Name\": \"Alpine AlpinePack Backpack\",\n    \"Description\": \"The AlpinePack backpack by Green Equipment is your ultimate companion for outdoor adventures. This versatile and durable backpack features a sleek navy design with reinforced straps. With a capacity of 45 liters, multiple compartments, and a hydration pack sleeve, it offers ample storage and organization. The ergonomic back panel ensures maximum comfort, even on the most challenging treks.\",\n    \"Price\": 129.00\n  },\n  {\n    \"Id\": 89,\n    \"Type\": \"Jackets\",\n    \"Brand\": \"Legend\",\n    \"Name\": \"Summit Pro Down Jacket\",\n    \"Description\": \"Defy the coldest temperatures with the Legend Summit Pro down jacket. This high-performance jacket is filled with premium down insulation for exceptional warmth. The sleek design in deep navy blue is complemented by contrasting silver zippers and emblems. Equipped with weather-resistant fabric and a removable hood, this jacket is your ultimate companion for extreme winter adventures.\",\n    \"Price\": 239.99\n  },\n  {\n    \"Id\": 90,\n    \"Type\": \"Navigation\",\n    \"Brand\": \"B&R\",\n    \"Name\": \"TrailTracker GPS Watch\",\n    \"Description\": \"Navigate the trails like a pro with the B&R TrailTracker GPS watch. This rugged and reliable watch features a built-in GPS that tracks your location, speed, and distance accurately. The sleek camo design with a vivid orange strap adds a sporty touch. With its long battery life and water-resistant construction, you can trust this watch to guide you through any outdoor expedition.\",\n    \"Price\": 199.00\n  },\n  {\n    \"Id\": 91,\n    \"Type\": \"Footwear\",\n    \"Brand\": \"WildRunner\",\n    \"Name\": \"Trailblazer Trail Running Shoes\",\n    \"Description\": \"Conquer any terrain in the Trailblazer Trail Running Shoes by WildRunner. These lightweight shoes come in vibrant blue and feature a rugged outsole for excellent traction, a breathable mesh upper for maximum comfort, and quick-drying materials to keep you dry on your adventures.\",\n    \"Price\": 89.99\n  },\n  {\n    \"Id\": 92,\n    \"Type\": \"Ski/boarding\",\n    \"Brand\": \"Daybird\",\n    \"Name\": \"Blizzard Snowboard\",\n    \"Description\": \"Take on the slopes with the Daybird Blizzard Snowboard. This powerful board features a sleek design in icy white, with a durable wood core, a versatile medium flex, and a precision base that allows for smooth rides and easy turns. Strap on and carve your way to glory.\",\n    \"Price\": 449.99\n  },\n  {\n    \"Id\": 93,\n    \"Type\": \"Climbing\",\n    \"Brand\": \"Raptor Elite\",\n    \"Name\": \"Summit Climbing Harness\",\n    \"Description\": \"Conquer the highest peaks with the Raptor Elite Summit Climbing Harness. This durable and lightweight harness, available in bold red, provides maximum comfort and safety while scaling tricky routes. Its adjustable waistband and leg loops ensure a snug fit, while the gear loops provide easy access to your equipment.\",\n    \"Price\": 109.99\n  },\n  {\n    \"Id\": 94,\n    \"Type\": \"Bags\",\n    \"Brand\": \"Gravitator\",\n    \"Name\": \"Gravity Hiking Backpack\",\n    \"Description\": \"Embark on unforgettable hikes with the Gravitator Gravity Hiking Backpack. Available in tiger stripes, this backpack offers a spacious main compartment, multiple pockets, and a hydration system compatible design. The lightweight and durable construction ensures maximum comfort on the trails.\",\n    \"Price\": 79.99\n  },\n  {\n    \"Id\": 95,\n    \"Type\": \"Cycling\",\n    \"Brand\": \"AirStrider\",\n    \"Name\": \"AeroLite Cycling Helmet\",\n    \"Description\": \"Stay safe and stylish on your cycling adventures with the AirStrider AeroLite Cycling Helmet. This helmet, in a glossy grey, features a lightweight design, adjustable straps, and excellent ventilation to keep you cool. The aerodynamic shape reduces air resistance, enabling you to pick up speed with confidence.\",\n    \"Price\": 129.99\n  },\n  {\n    \"Id\": 96,\n    \"Type\": \"Trekking\",\n    \"Brand\": \"B&R\",\n    \"Name\": \"Explorer Camping Tent\",\n    \"Description\": \"Experience the great outdoors with the B&R Explorer Camping Tent. This spacious tent, available in forest green, comfortably fits up to six people with a separate sleeping area and a generous living space. Its sturdy construction and weather-resistant materials ensure durability and protection from the elements.\",\n    \"Price\": 279.99\n  },\n  {\n    \"Id\": 97,\n    \"Type\": \"Bags\",\n    \"Brand\": \"Quester\",\n    \"Name\": \"Gravity Waterproof Dry Bag\",\n    \"Description\": \"Keep your essentials dry and secure with the Quester Gravity Waterproof Dry Bag. This versatile bag, in vibrant orange, features a roll-top closure system, adjustable shoulder straps, and durable PVC-coated fabric to withstand water, sand, and dirt. Ideal for adventures on land or water.\",\n    \"Price\": 49.99\n  },\n  {\n    \"Id\": 98,\n    \"Type\": \"Jackets\",\n    \"Brand\": \"Legend\",\n    \"Name\": \"Element Outdoor Jacket\",\n    \"Description\": \"Gear up for any adventure with the Legend Element Outdoor Jacket. Available in charcoal gray, this jacket offers ultimate protection with its waterproof and windproof shell. The breathable fabric and adjustable cuffs ensure comfort, allowing you to explore in any weather condition.\",\n    \"Price\": 179.99\n  },\n  {\n    \"Id\": 99,\n    \"Type\": \"Navigation\",\n    \"Brand\": \"Solstix\",\n    \"Name\": \"Adventurer GPS Watch\",\n    \"Description\": \"Take navigation to the next level with the Solstix Adventurer GPS Watch. This sleek and durable watch, in midnight blue, features a built-in GPS, altimeter, and compass, allowing you to track your routes and monitor your progress. With multiple sport modes, it's the ideal companion for outdoor enthusiasts.\",\n    \"Price\": 199.99\n  },\n  {\n    \"Id\": 100,\n    \"Type\": \"Trekking\",\n    \"Brand\": \"Green Equipment\",\n    \"Name\": \"EcoLite Trekking Poles\",\n    \"Description\": \"Tackle challenging trails with the Green Equipment EcoLite Trekking Poles. These lightweight poles, in vibrant green, feature adjustable height, shock-absorbing capabilities, and ergonomic cork handles for a comfortable grip. Whether ascending or descending, these poles provide stability and support.\",\n    \"Price\": 79.99\n  },\n  {\n    \"Id\": 101,\n    \"Type\": \"Footwear\",\n    \"Brand\": \"Raptor Elite\",\n    \"Name\": \"Trek Xtreme Hiking Shoes\",\n    \"Description\": \"The Trek Xtreme hiking shoes by Raptor Elite are built to endure any trail. With their durable leather upper and rugged rubber sole, they offer excellent traction and protection. These shoes come in a timeless brown color that adds a touch of style to your outdoor adventures.\",\n    \"Price\": 135.99\n  }\n]\n"
  },
  {
    "path": "src/Catalog.API/appsettings.Development.json",
    "content": "{\n  \"ConnectionStrings\": {\n    \"CatalogDB\": \"Host=localhost;Database=CatalogDB;Username=postgres;Password=yourWeak(!)Password\"\n  }\n}"
  },
  {
    "path": "src/Catalog.API/appsettings.json",
    "content": "﻿{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  },\n  \"OpenApi\": {\n    \"Endpoint\": {\n      \"Name\": \"Catalog.API V1\"\n    },\n    \"Document\": {\n      \"Description\": \"The Catalog Microservice HTTP API. This is a Data-Driven/CRUD microservice sample\",\n      \"Title\": \"eShop - Catalog HTTP API\",\n      \"Version\": \"v1\"\n    }\n  },\n  \"ConnectionStrings\": {\n    \"EventBus\": \"amqp://localhost\"\n  },\n  \"EventBus\": {\n    \"SubscriptionClientName\": \"Catalog\"\n  },\n  \"CatalogOptions\": {\n    \"UseCustomizationData\": false\n  }\n}\n"
  },
  {
    "path": "src/ClientApp/Animations/Base/AnimationBase.cs",
    "content": "﻿using System.Diagnostics;\n\nnamespace eShop.ClientApp.Animations.Base;\n\npublic abstract class AnimationBase : BindableObject\n{\n    public static readonly BindableProperty TargetProperty = BindableProperty.Create(nameof(Target),\n        typeof(VisualElement), typeof(AnimationBase),\n        propertyChanged: (bindable, oldValue, newValue) => ((AnimationBase)bindable).Target = (VisualElement)newValue);\n\n    public static readonly BindableProperty DurationProperty = BindableProperty.Create(nameof(Duration), typeof(string),\n        typeof(AnimationBase), \"1000\",\n        propertyChanged: (bindable, oldValue, newValue) => ((AnimationBase)bindable).Duration = (string)newValue);\n\n    public static readonly BindableProperty EasingProperty = BindableProperty.Create(nameof(Easing), typeof(EasingType),\n        typeof(AnimationBase), EasingType.Linear,\n        propertyChanged: (bindable, oldValue, newValue) => ((AnimationBase)bindable).Easing = (EasingType)newValue);\n\n    public static readonly BindableProperty RepeatForeverProperty = BindableProperty.Create(nameof(RepeatForever),\n        typeof(bool), typeof(AnimationBase), false,\n        propertyChanged: (bindable, oldValue, newValue) => ((AnimationBase)bindable).RepeatForever = (bool)newValue);\n\n    public static readonly BindableProperty DelayProperty = BindableProperty.Create(nameof(Delay), typeof(int),\n        typeof(AnimationBase), 0,\n        propertyChanged: (bindable, oldValue, newValue) => ((AnimationBase)bindable).Delay = (int)newValue);\n\n    private bool _isRunning;\n\n    public VisualElement Target\n    {\n        get => (VisualElement)GetValue(TargetProperty);\n        set => SetValue(TargetProperty, value);\n    }\n\n    public string Duration\n    {\n        get => (string)GetValue(DurationProperty);\n        set => SetValue(DurationProperty, value);\n    }\n\n    public EasingType Easing\n    {\n        get => (EasingType)GetValue(EasingProperty);\n        set => SetValue(EasingProperty, value);\n    }\n\n    public bool RepeatForever\n    {\n        get => (bool)GetValue(RepeatForeverProperty);\n        set => SetValue(RepeatForeverProperty, value);\n    }\n\n    public int Delay\n    {\n        get => (int)GetValue(DelayProperty);\n        set => SetValue(DelayProperty, value);\n    }\n\n    protected abstract Task BeginAnimation();\n\n    public async Task Begin()\n    {\n        try\n        {\n            if (!_isRunning)\n            {\n                _isRunning = true;\n\n                await InternalBegin()\n                    .ContinueWith(t => t.Exception, TaskContinuationOptions.OnlyOnFaulted)\n                    .ConfigureAwait(false);\n            }\n        }\n        catch (TaskCanceledException)\n        {\n        }\n        catch (Exception ex)\n        {\n            Debug.WriteLine($\"Exception in animation {ex}\");\n        }\n    }\n\n    protected abstract Task ResetAnimation();\n\n    public async Task Reset()\n    {\n        _isRunning = false;\n        await ResetAnimation();\n    }\n\n    private async Task InternalBegin()\n    {\n        if (Delay > 0)\n        {\n            await Task.Delay(Delay);\n        }\n\n        if (!RepeatForever)\n        {\n            await BeginAnimation();\n        }\n        else\n        {\n            do\n            {\n                await BeginAnimation();\n                await ResetAnimation();\n            } while (RepeatForever);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Animations/Base/EasingType.cs",
    "content": "﻿namespace eShop.ClientApp.Animations.Base;\n\npublic enum EasingType\n{\n    BounceIn,\n    BounceOut,\n    CubicIn,\n    CubicInOut,\n    CubicOut,\n    Linear,\n    SinIn,\n    SinInOut,\n    SinOut,\n    SpringIn,\n    SpringOut\n}\n"
  },
  {
    "path": "src/ClientApp/Animations/FadeToAnimation.cs",
    "content": "using eShop.ClientApp.Animations.Base;\nusing eShop.ClientApp.Helpers;\n\nnamespace eShop.ClientApp.Animations;\n\npublic class FadeToAnimation : AnimationBase\n{\n    public static readonly BindableProperty OpacityProperty =\n        BindableProperty.Create(nameof(Opacity), typeof(double), typeof(FadeToAnimation), 0.0d,\n            propertyChanged: (bindable, oldValue, newValue) =>\n                ((FadeToAnimation)bindable).Opacity = (double)newValue);\n\n    public double Opacity\n    {\n        get => (double)GetValue(OpacityProperty);\n        set => SetValue(OpacityProperty, value);\n    }\n\n    protected override Task BeginAnimation()\n    {\n        if (Target == null)\n        {\n            throw new NullReferenceException(\"Null Target property.\");\n        }\n\n        return Target.FadeTo(Opacity, Convert.ToUInt32(Duration), EasingHelper.GetEasing(Easing));\n    }\n\n    protected override Task ResetAnimation()\n    {\n        if (Target == null)\n        {\n            throw new NullReferenceException(\"Null Target property.\");\n        }\n\n        return Target.FadeTo(0, 0);\n    }\n}\n\npublic class FadeInAnimation : AnimationBase\n{\n    public enum FadeDirection\n    {\n        Up,\n        Down\n    }\n\n    public static readonly BindableProperty DirectionProperty =\n        BindableProperty.Create(nameof(Direction), typeof(FadeDirection), typeof(FadeInAnimation), FadeDirection.Up,\n            propertyChanged: (bindable, oldValue, newValue) =>\n                ((FadeInAnimation)bindable).Direction = (FadeDirection)newValue);\n\n    public FadeDirection Direction\n    {\n        get => (FadeDirection)GetValue(DirectionProperty);\n        set => SetValue(DirectionProperty, value);\n    }\n\n    protected override Task BeginAnimation()\n    {\n        if (Target == null)\n        {\n            throw new NullReferenceException(\"Null Target property.\");\n        }\n\n        Target.Dispatcher.Dispatch(() => Target.Animate(\"FadeIn\", FadeIn(), 16, Convert.ToUInt32(Duration)));\n\n        return Task.CompletedTask;\n    }\n\n    protected override Task ResetAnimation()\n    {\n        if (Target == null)\n        {\n            throw new NullReferenceException(\"Null Target property.\");\n        }\n\n        Target.Dispatcher.Dispatch(() => Target.FadeTo(0, 0));\n\n        return Task.CompletedTask;\n    }\n\n    internal Animation FadeIn()\n    {\n        var animation = new Animation();\n\n        animation.WithConcurrent(f => Target.Opacity = f, 0, 1, Microsoft.Maui.Easing.CubicOut);\n\n        animation.WithConcurrent(\n            f => Target.TranslationY = f,\n            Target.TranslationY + (Direction == FadeDirection.Up ? 50 : -50), Target.TranslationY,\n            Microsoft.Maui.Easing.CubicOut);\n\n        return animation;\n    }\n}\n\npublic class FadeOutAnimation : AnimationBase\n{\n    public enum FadeDirection\n    {\n        Up,\n        Down\n    }\n\n    public static readonly BindableProperty DirectionProperty =\n        BindableProperty.Create(nameof(Direction), typeof(FadeDirection), typeof(FadeOutAnimation), FadeDirection.Up,\n            propertyChanged: (bindable, oldValue, newValue) =>\n                ((FadeOutAnimation)bindable).Direction = (FadeDirection)newValue);\n\n    public FadeDirection Direction\n    {\n        get => (FadeDirection)GetValue(DirectionProperty);\n        set => SetValue(DirectionProperty, value);\n    }\n\n    protected override Task BeginAnimation()\n    {\n        if (Target == null)\n        {\n            throw new NullReferenceException(\"Null Target property.\");\n        }\n\n        Target.Dispatcher.Dispatch(() => Target.Animate(\"FadeOut\", FadeOut(), 16, Convert.ToUInt32(Duration)));\n\n        return Task.CompletedTask;\n    }\n\n    protected override Task ResetAnimation()\n    {\n        if (Target == null)\n        {\n            throw new NullReferenceException(\"Null Target property.\");\n        }\n\n        Target.Dispatcher.Dispatch(() => Target.FadeTo(0, 0));\n\n        return Task.CompletedTask;\n    }\n\n    internal Animation FadeOut()\n    {\n        Animation animation = new();\n\n        animation.WithConcurrent(\n            f => Target.Opacity = f,\n            1, 0);\n\n        animation.WithConcurrent(\n            f => Target.TranslationY = f,\n            Target.TranslationY, Target.TranslationY + (Direction == FadeDirection.Up ? 50 : -50));\n\n        return animation;\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Animations/StoryBoard.cs",
    "content": "﻿using eShop.ClientApp.Animations.Base;\n\nnamespace eShop.ClientApp.Animations;\n\n[ContentProperty(\"Animations\")]\npublic class StoryBoard : AnimationBase\n{\n    public StoryBoard()\n    {\n        Animations = new List<AnimationBase>();\n    }\n\n    public StoryBoard(List<AnimationBase> animations)\n    {\n        Animations = animations;\n    }\n\n    public List<AnimationBase> Animations { get; }\n\n    protected override async Task BeginAnimation()\n    {\n        foreach (var animation in Animations)\n        {\n            if (animation.Target == null)\n            {\n                animation.Target = Target;\n            }\n\n            await animation.Begin();\n        }\n    }\n\n    protected override async Task ResetAnimation()\n    {\n        foreach (var animation in Animations)\n        {\n            if (animation.Target == null)\n            {\n                animation.Target = Target;\n            }\n\n            await animation.Reset();\n        }\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/App.xaml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n\n<Application\n    x:Class=\"eShop.ClientApp.App\"\n    xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\">\n    <Application.Resources>\n        <ResourceDictionary>\n            <ResourceDictionary.MergedDictionaries>\n                <ResourceDictionary Source=\"Resources/Styles/Colors.xaml\" />\n                <ResourceDictionary Source=\"Resources/Styles/Styles.xaml\" />\n            </ResourceDictionary.MergedDictionaries>\n        </ResourceDictionary>\n    </Application.Resources>\n</Application>"
  },
  {
    "path": "src/ClientApp/App.xaml.cs",
    "content": "﻿using System.Diagnostics;\nusing System.Globalization;\nusing eShop.ClientApp.Services;\nusing eShop.ClientApp.Services.AppEnvironment;\nusing eShop.ClientApp.Services.Location;\nusing eShop.ClientApp.Services.Settings;\nusing eShop.ClientApp.Services.Theme;\nusing Location = eShop.ClientApp.Models.Location.Location;\n\nnamespace eShop.ClientApp;\n\npublic partial class App : Application\n{\n    private readonly IAppEnvironmentService _appEnvironmentService;\n    private readonly ILocationService _locationService;\n    private readonly INavigationService _navigationService;\n    private readonly ISettingsService _settingsService;\n    private readonly ITheme _theme;\n\n    public App(\n        ISettingsService settingsService, IAppEnvironmentService appEnvironmentService,\n        INavigationService navigationService, ILocationService locationService,\n        ITheme theme)\n    {\n        _settingsService = settingsService;\n        _appEnvironmentService = appEnvironmentService;\n        _navigationService = navigationService;\n        _locationService = locationService;\n        _theme = theme;\n\n        InitializeComponent();\n\n        InitApp();\n\n\n        Current.UserAppTheme = AppTheme.Light;\n    }\n\n    protected override Window CreateWindow(IActivationState activationState)\n    {\n        return new Window(new AppShell(_navigationService));\n    }\n\n    private void InitApp()\n    {\n        if (VersionTracking.IsFirstLaunchEver)\n        {\n            _settingsService.UseMocks = true;\n        }\n\n        if (!_settingsService.UseMocks)\n        {\n            _appEnvironmentService.UpdateDependencies(_settingsService.UseMocks);\n        }\n    }\n\n    protected override async void OnStart()\n    {\n        base.OnStart();\n\n        if (_settingsService.AllowGpsLocation && !_settingsService.UseFakeLocation)\n        {\n            await GetGpsLocation();\n        }\n\n        if (!_settingsService.UseMocks)\n        {\n            await SendCurrentLocation();\n        }\n\n        OnResume();\n    }\n\n    protected override void OnSleep()\n    {\n        SetStatusBar();\n        RequestedThemeChanged -= App_RequestedThemeChanged;\n    }\n\n    protected override void OnResume()\n    {\n        SetStatusBar();\n        RequestedThemeChanged += App_RequestedThemeChanged;\n    }\n\n    private void App_RequestedThemeChanged(object sender, AppThemeChangedEventArgs e)\n    {\n        Dispatcher.Dispatch(() => SetStatusBar());\n    }\n\n    private void SetStatusBar()\n    {\n        var nav = Windows[0].Page as NavigationPage;\n\n        if (Current.RequestedTheme == AppTheme.Dark)\n        {\n            _theme?.SetStatusBarColor(Colors.Black, false);\n            if (nav != null)\n            {\n                nav.BarBackgroundColor = Colors.Black;\n                nav.BarTextColor = Colors.White;\n            }\n        }\n        else\n        {\n            _theme?.SetStatusBarColor(Colors.White, true);\n            if (nav != null)\n            {\n                nav.BarBackgroundColor = Colors.White;\n                nav.BarTextColor = Colors.Black;\n            }\n        }\n    }\n\n    private async Task GetGpsLocation()\n    {\n        try\n        {\n            var request = new GeolocationRequest(GeolocationAccuracy.High);\n            var location = await Geolocation.GetLocationAsync(request, CancellationToken.None).ConfigureAwait(false);\n\n            if (location != null)\n            {\n                _settingsService.Latitude = location.Latitude.ToString();\n                _settingsService.Longitude = location.Longitude.ToString();\n            }\n        }\n        catch (Exception ex)\n        {\n            if (ex is FeatureNotEnabledException || ex is FeatureNotEnabledException || ex is PermissionException)\n            {\n                _settingsService.AllowGpsLocation = false;\n            }\n\n            // Unable to get location\n            Debug.WriteLine(ex);\n        }\n    }\n\n    private async Task SendCurrentLocation()\n    {\n        var location = new Location\n        {\n            Latitude = double.Parse(_settingsService.Latitude, CultureInfo.InvariantCulture),\n            Longitude = double.Parse(_settingsService.Longitude, CultureInfo.InvariantCulture)\n        };\n\n        await _locationService.UpdateUserLocation(location);\n    }\n\n    public static void HandleAppActions(AppAction appAction)\n    {\n        if (Current is not App app)\n        {\n            return;\n        }\n\n        app.Dispatcher.Dispatch(\n            async () =>\n            {\n                if (appAction.Id.Equals(AppActions.ViewProfileAction.Id))\n                {\n                    await app._navigationService.NavigateToAsync(\"//Main/Profile\");\n                }\n            });\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/AppActions.cs",
    "content": "﻿namespace eShop.ClientApp;\n\npublic static class AppActions\n{\n    public static readonly AppAction\n        ViewProfileAction = new(\"view_profile\", \"View Profile\", \"View your user profile\");\n}\n"
  },
  {
    "path": "src/ClientApp/AppShell.xaml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n\n<Shell\n    x:Class=\"eShop.ClientApp.AppShell\"\n    xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n    xmlns:toolkit=\"http://schemas.microsoft.com/dotnet/2022/maui/toolkit\"\n    xmlns:views=\"clr-namespace:eShop.ClientApp.Views\">\n\n    <Shell.Resources>\n        <ResourceDictionary>\n            <Style x:Key=\"BaseStyle\" TargetType=\"Element\">\n                <Setter Property=\"Shell.BackgroundColor\" Value=\"White\" />\n                <Setter Property=\"Shell.ForegroundColor\" Value=\"{AppThemeBinding Dark={StaticResource DarkForegroundColor}, Light={StaticResource LightForegroundColor}}\" />\n                <Setter Property=\"Shell.TitleColor\" Value=\"{AppThemeBinding Dark={StaticResource DarkForegroundColor}, Light={StaticResource LightForegroundColor}}\" />\n                <Setter Property=\"Shell.DisabledColor\" Value=\"#B4FFFFFF\" />\n                <Setter Property=\"Shell.UnselectedColor\" Value=\"#95FFFFFF\" />\n                <Setter Property=\"Shell.TabBarBackgroundColor\" Value=\"{AppThemeBinding Dark={StaticResource DarkBackgroundColor}, Light={StaticResource LightBackgroundColor}}\" />\n                <Setter Property=\"Shell.TabBarForegroundColor\" Value=\"{AppThemeBinding Dark={StaticResource DarkForegroundColor}, Light={StaticResource LightForegroundColor}}\" />\n                <Setter Property=\"Shell.TabBarUnselectedColor\" Value=\"{AppThemeBinding Dark=#E7E7E7, Light=#CCCCCC}\" />\n                <Setter Property=\"Shell.TabBarTitleColor\" Value=\"{AppThemeBinding Dark={StaticResource DarkForegroundColor}, Light={StaticResource LightForegroundColor}}\" />\n            </Style>\n            <Style BasedOn=\"{StaticResource BaseStyle}\" TargetType=\"TabBar\" />\n            <Style BasedOn=\"{StaticResource BaseStyle}\" TargetType=\"FlyoutItem\" />\n        </ResourceDictionary>\n    </Shell.Resources>\n    <Shell.BackButtonBehavior>\n        <BackButtonBehavior TextOverride=\"\" />\n    </Shell.BackButtonBehavior>\n\n    <Shell.Behaviors>\n        <toolkit:StatusBarBehavior StatusBarColor=\"White\" StatusBarStyle=\"DarkContent\" />\n    </Shell.Behaviors>\n\n    <FlyoutItem>\n        <ShellContent ContentTemplate=\"{DataTemplate views:LoginView}\" Route=\"Login\" />\n    </FlyoutItem>\n\n    <TabBar Route=\"Main\">\n        <ShellContent\n            Title=\"CATALOG\"\n            ContentTemplate=\"{DataTemplate views:CatalogView}\"\n            Icon=\"{StaticResource CatalogIconImageSource}\"\n            Route=\"Catalog\" />\n        <ShellContent\n            Title=\"MAP\"\n            ContentTemplate=\"{DataTemplate views:MapView}\"\n            Icon=\"{StaticResource MapIconImageSource}\"\n            IsVisible=\"{OnPlatform Android=True,\n                                   iOS=True,\n                                   MacCatalyst=True,\n                                   Default=False}\"\n            Route=\"Map\" />\n        <ShellContent\n            Title=\"ORDERS\"\n            ContentTemplate=\"{DataTemplate views:ProfileView}\"\n            Icon=\"{StaticResource BasketIconImageSource}\"\n            Route=\"Profile\" />\n    </TabBar>\n</Shell>"
  },
  {
    "path": "src/ClientApp/AppShell.xaml.cs",
    "content": "﻿using eShop.ClientApp.Services;\nusing eShop.ClientApp.Views;\n\nnamespace eShop.ClientApp;\n\npublic partial class AppShell : Shell\n{\n    private readonly INavigationService _navigationService;\n\n    public AppShell(INavigationService navigationService)\n    {\n        _navigationService = navigationService;\n\n        InitializeRouting();\n        InitializeComponent();\n    }\n\n    protected override async void OnHandlerChanged()\n    {\n        base.OnHandlerChanged();\n\n        if (Handler is not null)\n        {\n            await _navigationService.InitializeAsync();\n        }\n    }\n\n    private static void InitializeRouting()\n    {\n        //Routing.RegisterRoute(\"Login\", typeof(LoginView));\n        Routing.RegisterRoute(\"Filter\", typeof(FiltersView));\n        Routing.RegisterRoute(\"ViewCatalogItem\", typeof(CatalogItemView));\n        Routing.RegisterRoute(\"Basket\", typeof(BasketView));\n        Routing.RegisterRoute(\"Settings\", typeof(SettingsView));\n        Routing.RegisterRoute(\"OrderDetail\", typeof(OrderDetailView));\n        Routing.RegisterRoute(\"Checkout\", typeof(CheckoutView));\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/ClientApp.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst;net10.0</TargetFrameworks>\n    <TargetFrameworks Condition=\"$([MSBuild]::IsOSPlatform('windows'))\">$(TargetFrameworks);net10.0-windows10.0.19041.0</TargetFrameworks>\n    <!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->\n    <!-- <TargetFrameworks>$(TargetFrameworks);net10.0-tizen</TargetFrameworks> -->\n\n    <!-- Note for MacCatalyst:\n    The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.\n    When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifer>.\n    The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated;\n    either BOTH runtimes must be indicated or ONLY macatalyst-x64. -->\n    <!-- ex. <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->\n\n    <OutputType Condition=\"'$(TargetFramework)' != 'net10.0'\">Exe</OutputType>\n    <RootNamespace>eShop.ClientApp</RootNamespace>\n    <UseMaui>true</UseMaui>\n    <SingleProject>true</SingleProject>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <MauiEnableXamlCBindingWithSourceCompilation>true</MauiEnableXamlCBindingWithSourceCompilation>\n    <ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>\n    <NoWarn>$(NoWarn);XC0103</NoWarn>\n\n    <!-- Display name -->\n    <ApplicationTitle>AdventureWorks</ApplicationTitle>\n\n    <!-- App Identifier -->\n    <ApplicationId>com.companyname.eshop</ApplicationId>\n    <ApplicationId Condition=\"$(TargetFramework.Contains('-windows'))\">9a85b8a9-4da5-4a12-8e7f-43c05ab266d6</ApplicationId>\n\n    <!-- Versions -->\n    <ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>\n    <ApplicationVersion>1</ApplicationVersion>\n    <SupportedOSPlatformVersion Condition=\"$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'\">15.0</SupportedOSPlatformVersion>\n    <SupportedOSPlatformVersion Condition=\"$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'\">15.0</SupportedOSPlatformVersion>\n    <SupportedOSPlatformVersion Condition=\"$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'\">21.0</SupportedOSPlatformVersion>\n    <SupportedOSPlatformVersion Condition=\"$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'\">10.0.17763.0</SupportedOSPlatformVersion>\n    <TargetPlatformMinVersion Condition=\"$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'\">10.0.17763.0</TargetPlatformMinVersion>\n    <SupportedOSPlatformVersion Condition=\"$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'\">6.5</SupportedOSPlatformVersion>\n  </PropertyGroup>\n  <PropertyGroup Condition=\"'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net10.0-ios|AnyCPU'\">\n    <CreatePackage>false</CreatePackage>\n  </PropertyGroup>\n  <ItemGroup>\n    <!-- App Icon -->\n    <MauiIcon Include=\"Resources\\AppIcon\\appicon.svg\" ForegroundFile=\"Resources\\AppIcon\\appiconfg.svg\" Color=\"#edeafb\" />\n\n    <!-- Splash Screen -->\n    <MauiSplashScreen Include=\"Resources\\Splash\\splash.svg\">\n      <Color>#edeafb</Color>\n      <BaseSize>128,128</BaseSize>\n    </MauiSplashScreen>\n\n    <!-- Images -->\n    <MauiImage Include=\"Resources\\Images\\*\" />\n    <MauiFont Include=\"Resources\\Fonts\\*\" />\n\n    <!-- Raw Assets (also remove the \"Resources\\Raw\" prefix) -->\n    <MauiAsset Include=\"Resources\\Raw\\**\" LogicalName=\"%(RecursiveDir)%(Filename)%(Extension)\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Google.Protobuf\" Version=\"3.29.3\" />\n    <PackageReference Include=\"Grpc.Net.Client\" Version=\"2.67.0\" />\n    <PackageReference Include=\"Grpc.Tools\" Version=\"2.69.0\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <PackageReference Include=\"IdentityModel.OidcClient\" Version=\"6.0.0\" />\n    <PackageReference Include=\"Microsoft.Maui.Controls\" Version=\"9.0.30\" />\n    <PackageReference Include=\"Microsoft.Maui.Controls.Compatibility\" Version=\"9.0.30\" />\n    <PackageReference Include=\"Microsoft.Maui.Controls.Maps\" Version=\"9.0.30\" />\n    <PackageReference Include=\"Microsoft.Extensions.Logging.Debug\" Version=\"9.0.0\" />\n    <PackageReference Include=\"CommunityToolkit.Maui\" Version=\"9.1.1\" />\n    <PackageReference Include=\"IdentityModel\" Version=\"7.0.0\" />\n    <PackageReference Include=\"CommunityToolkit.Mvvm\" Version=\"8.3.2\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Protobuf Include=\"Services\\Basket\\Protos\\basket.proto\" CompileOutputs=\"false\" OutputDir=\"\" GrpcServices=\"Client\">\n      <Generator>MSBuild:Compile</Generator>\n    </Protobuf>\n  </ItemGroup>\n\n  <PropertyGroup Condition=\"$(TargetFramework.Contains('-android'))\">\n    <RuntimeIdentifiers>$(RuntimeIdentifiers);android-arm64</RuntimeIdentifiers>\n  </PropertyGroup>\n\n  <!--<PropertyGroup Condition=\"$(TargetFramework.Contains('-android')) and '$(Configuration)' == 'Debug'\">\n    <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>\n  </PropertyGroup>-->\n\n  <!-- Build Properties must be defined within these property groups to ensure successful publishing\n       to the Mac App Store. See: https://aka.ms/maui-publish-app-store#define-build-properties-in-your-project-file -->\n  <PropertyGroup Condition=\"$(TargetFramework.Contains('-maccatalyst')) and '$(Configuration)' == 'Debug'\">\n    <CodesignEntitlements>Platforms/MacCatalyst/Entitlements.Debug.plist</CodesignEntitlements>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\"$(TargetFramework.Contains('-maccatalyst')) and '$(Configuration)' == 'Release'\">\n    <CodesignEntitlements>Platforms/MacCatalyst/Entitlements.Release.plist</CodesignEntitlements>\n    <UseHardenedRuntime>true</UseHardenedRuntime>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\"$(TargetFramework.Contains('-ios'))\">\n    <CodesignEntitlements>Platforms\\iOS\\Entitlements.plist</CodesignEntitlements>\n  </PropertyGroup>\n\n</Project>\n"
  },
  {
    "path": "src/ClientApp/ClientApp.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.5.002.0\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"ClientApp\", \"ClientApp.csproj\", \"{A70E29B3-9C96-40F7-839E-7350508D4F33}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"ClientApp.UnitTests\", \"..\\..\\tests\\ClientApp.UnitTests\\ClientApp.UnitTests.csproj\", \"{E260CC6D-1695-43AA-918A-B5EF4049A3E9}\"\nEndProject\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"Solution Items\", \"Solution Items\", \"{96432572-F84E-43FE-99BC-F23638D2D7A5}\"\n\tProjectSection(SolutionItems) = preProject\n\t\t..\\..\\.github\\workflows\\client-app-validation.yml = ..\\..\\.github\\workflows\\client-app-validation.yml\n\tEndProjectSection\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{A70E29B3-9C96-40F7-839E-7350508D4F33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{A70E29B3-9C96-40F7-839E-7350508D4F33}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{A70E29B3-9C96-40F7-839E-7350508D4F33}.Debug|Any CPU.Deploy.0 = Debug|Any CPU\n\t\t{A70E29B3-9C96-40F7-839E-7350508D4F33}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{A70E29B3-9C96-40F7-839E-7350508D4F33}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{E260CC6D-1695-43AA-918A-B5EF4049A3E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{E260CC6D-1695-43AA-918A-B5EF4049A3E9}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{E260CC6D-1695-43AA-918A-B5EF4049A3E9}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{E260CC6D-1695-43AA-918A-B5EF4049A3E9}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {F27E00AF-AB53-4F7B-B80B-85A22DE6E7A6}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "src/ClientApp/Controls/AddBasketButton.xaml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n\n<Grid\n    x:Class=\"eShop.ClientApp.Controls.AddBasketButton\"\n    xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\">\n    <Ellipse\n        Fill=\"#000\"\n        HeightRequest=\"48\"\n        HorizontalOptions=\"Center\"\n        StrokeThickness=\"0\"\n        VerticalOptions=\"Center\"\n        WidthRequest=\"48\" />\n    <Image\n        Aspect=\"AspectFit\"\n        HeightRequest=\"24\"\n        HorizontalOptions=\"Center\"\n        Source=\"{StaticResource AddIconLightImageSource}\"\n        VerticalOptions=\"Center\"\n        WidthRequest=\"24\" />\n</Grid>"
  },
  {
    "path": "src/ClientApp/Controls/AddBasketButton.xaml.cs",
    "content": "﻿namespace eShop.ClientApp.Controls;\n\npublic partial class AddBasketButton : Grid\n{\n    public AddBasketButton()\n    {\n        InitializeComponent();\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Controls/CustomTabbedPage.cs",
    "content": "﻿namespace eShop.ClientApp.Controls;\n\npublic class CustomTabbedPage : TabbedPage\n{\n    public static BindableProperty BadgeTextProperty =\n        BindableProperty.CreateAttached(\"BadgeText\", typeof(string), typeof(CustomTabbedPage), default(string));\n\n    public static BindableProperty BadgeColorProperty =\n        BindableProperty.CreateAttached(\"BadgeColor\", typeof(Color), typeof(CustomTabbedPage), Colors.Transparent);\n\n    public static string GetBadgeText(BindableObject view)\n    {\n        return (string)view.GetValue(BadgeTextProperty);\n    }\n\n    public static void SetBadgeText(BindableObject view, string value)\n    {\n        view.SetValue(BadgeTextProperty, value);\n    }\n\n    public static Color GetBadgeColor(BindableObject view)\n    {\n        return (Color)view.GetValue(BadgeColorProperty);\n    }\n\n    public static void SetBadgeColor(BindableObject view, Color value)\n    {\n        view.SetValue(BadgeColorProperty, value);\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Controls/ToggleButton.cs",
    "content": "﻿using System.Windows.Input;\n\nnamespace eShop.ClientApp.Controls;\n\npublic class ToggleButton : ContentView\n{\n    public static readonly BindableProperty CommandProperty =\n        BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(ToggleButton));\n\n    public static readonly BindableProperty CommandParameterProperty =\n        BindableProperty.Create(nameof(CommandParameter), typeof(object), typeof(ToggleButton));\n\n    public static readonly BindableProperty CheckedProperty =\n        BindableProperty.Create(nameof(Checked), typeof(bool), typeof(ToggleButton), false, BindingMode.TwoWay,\n            propertyChanged: OnCheckedChanged);\n\n    public static readonly BindableProperty AnimateProperty =\n        BindableProperty.Create(nameof(Animate), typeof(bool), typeof(ToggleButton), false);\n\n    public static readonly BindableProperty CheckedImageProperty =\n        BindableProperty.Create(nameof(CheckedImage), typeof(ImageSource), typeof(ToggleButton));\n\n    public static readonly BindableProperty UnCheckedImageProperty =\n        BindableProperty.Create(nameof(UnCheckedImage), typeof(ImageSource), typeof(ToggleButton));\n\n    private ICommand _toggleCommand;\n    private Image _toggleImage;\n\n    public ToggleButton()\n    {\n        Initialize();\n    }\n\n    public ICommand Command\n    {\n        get => (ICommand)GetValue(CommandProperty);\n        set => SetValue(CommandProperty, value);\n    }\n\n    public object CommandParameter\n    {\n        get => GetValue(CommandParameterProperty);\n        set => SetValue(CommandParameterProperty, value);\n    }\n\n    public bool Checked\n    {\n        get => (bool)GetValue(CheckedProperty);\n        set => SetValue(CheckedProperty, value);\n    }\n\n    public bool Animate\n    {\n        get => (bool)GetValue(AnimateProperty);\n        set => SetValue(CheckedProperty, value);\n    }\n\n    public ImageSource CheckedImage\n    {\n        get => (ImageSource)GetValue(CheckedImageProperty);\n        set => SetValue(CheckedImageProperty, value);\n    }\n\n    public ImageSource UnCheckedImage\n    {\n        get => (ImageSource)GetValue(UnCheckedImageProperty);\n        set => SetValue(UnCheckedImageProperty, value);\n    }\n\n    public ICommand ToggleCommand =>\n        _toggleCommand ??= new Command(() =>\n        {\n            Checked = !Checked;\n\n            if (Command != null)\n            {\n                Command.Execute(CommandParameter);\n            }\n        });\n\n    private void Initialize()\n    {\n        _toggleImage = new Image();\n\n        Animate = true;\n\n        GestureRecognizers.Add(new TapGestureRecognizer {Command = ToggleCommand});\n\n        _toggleImage.Source = UnCheckedImage;\n        Content = _toggleImage;\n    }\n\n    protected override void OnParentSet()\n    {\n        base.OnParentSet();\n        _toggleImage.Source = UnCheckedImage;\n        Content = _toggleImage;\n    }\n\n    private static async void OnCheckedChanged(BindableObject bindable, object oldValue, object newValue)\n    {\n        var toggleButton = (ToggleButton)bindable;\n\n        if (Equals(newValue, null) && !Equals(oldValue, null))\n        {\n            return;\n        }\n\n        toggleButton._toggleImage.Source = toggleButton.Checked\n            ? toggleButton.CheckedImage\n            : toggleButton.UnCheckedImage;\n\n        toggleButton.Content = toggleButton._toggleImage;\n\n        if (toggleButton.Animate)\n        {\n            await toggleButton.ScaleTo(0.9, 50, Easing.Linear);\n            await Task.Delay(100);\n            await toggleButton.ScaleTo(1, 50, Easing.Linear);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Converters/DoesNotHaveCountConverter.cs",
    "content": "﻿using System.Globalization;\nusing CommunityToolkit.Maui.Converters;\n\nnamespace eShop.ClientApp.Converters;\n\npublic class DoesNotHaveCountConverter : BaseConverterOneWay<int, bool>\n{\n    public override bool DefaultConvertReturnValue { get; set; } = false;\n\n    public override bool ConvertFrom(int value, CultureInfo culture)\n    {\n        return value <= 0;\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Converters/DoubleConverter.cs",
    "content": "﻿using System.Globalization;\nusing CommunityToolkit.Maui.Converters;\n\nnamespace eShop.ClientApp.Converters;\n\npublic class DoubleConverter : BaseConverter<double, string>\n{\n    public override string DefaultConvertReturnValue { get; set; } = string.Empty;\n    public override double DefaultConvertBackReturnValue { get; set; } = 0d;\n\n    public override double ConvertBackTo(string value, CultureInfo culture)\n    {\n        return double.TryParse(value, out var parsed) ? parsed : DefaultConvertBackReturnValue;\n    }\n\n    public override string ConvertFrom(double value, CultureInfo culture)\n    {\n        return value.ToString();\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Converters/FirstValidationErrorConverter.cs",
    "content": "﻿using System.Globalization;\nusing CommunityToolkit.Maui.Converters;\n\nnamespace eShop.ClientApp.Converters;\n\npublic class FirstValidationErrorConverter : BaseConverterOneWay<IEnumerable<string>, string>\n{\n    public override string DefaultConvertReturnValue { get; set; } = string.Empty;\n\n    public override string ConvertFrom(IEnumerable<string> value, CultureInfo culture)\n    {\n        return value?.FirstOrDefault() ?? DefaultConvertReturnValue;\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Converters/HasCountConverter.cs",
    "content": "﻿using System.Globalization;\nusing CommunityToolkit.Maui.Converters;\n\nnamespace eShop.ClientApp.Converters;\n\npublic class HasCountConverter : BaseConverterOneWay<int, bool>\n{\n    public override bool DefaultConvertReturnValue { get; set; } = false;\n\n    public override bool ConvertFrom(int value, CultureInfo culture)\n    {\n        return value > 0;\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Converters/ItemsToHeightConverter.cs",
    "content": "﻿using System.Globalization;\nusing CommunityToolkit.Maui.Converters;\n\nnamespace eShop.ClientApp.Converters;\n\npublic class ItemsToHeightConverter : BaseConverterOneWay<int, int>\n{\n    private const int ItemHeight = 156;\n\n    public override int DefaultConvertReturnValue { get; set; } = ItemHeight;\n\n    public override int ConvertFrom(int value, CultureInfo culture)\n    {\n        return value * ItemHeight;\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Converters/WebNavigatedEventArgsConverter.cs",
    "content": "﻿using System.Globalization;\nusing CommunityToolkit.Maui.Converters;\n\nnamespace eShop.ClientApp.Converters;\n\npublic class WebNavigatedEventArgsConverter : BaseConverterOneWay<WebNavigatedEventArgs, string>\n{\n    public override string DefaultConvertReturnValue { get; set; } = string.Empty;\n\n    public override string ConvertFrom(WebNavigatedEventArgs value, CultureInfo culture)\n    {\n        return value?.Url ?? string.Empty;\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Converters/WebNavigatingEventArgsConverter.cs",
    "content": "﻿using System.Globalization;\nusing Microsoft.Maui.Controls;\n\nnamespace eShop.ClientApp.Converters;\n\npublic class WebNavigatingEventArgsConverter : IValueConverter\n{\n    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)\n    {\n        if (value is WebNavigatingEventArgs eventArgs)\n        {\n            return eventArgs.Url ?? string.Empty;\n        }\n\n        return string.Empty;\n    }\n\n    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)\n    {\n        throw new NotImplementedException(\"ConvertBack is not supported for WebNavigatingEventArgsConverter.\");\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Effects/EntryLineColorEffect.cs",
    "content": "﻿namespace eShop.ClientApp.Effects;\n\npublic class EntryLineColorEffect : RoutingEffect\n{\n    public EntryLineColorEffect()\n        : base(\"ClientApp.EntryLineColorEffect\")\n    {\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Effects/ThemeEffects.cs",
    "content": "﻿namespace eShop.ClientApp.Effects;\n\npublic static class ThemeEffects\n{\n    public static readonly BindableProperty CircleProperty =\n        BindableProperty.CreateAttached(\"Circle\", typeof(bool), typeof(ThemeEffects), false,\n            propertyChanged: OnChanged<CircleEffect, bool>);\n\n    public static bool GetCircle(BindableObject view)\n    {\n        return (bool)view.GetValue(CircleProperty);\n    }\n\n    public static void SetCircle(BindableObject view, bool circle)\n    {\n        view.SetValue(CircleProperty, circle);\n    }\n\n\n    private static void OnChanged<TEffect, TProp>(BindableObject bindable, object oldValue, object newValue)\n        where TEffect : Effect, new()\n    {\n        if (bindable is not View view)\n        {\n            return;\n        }\n\n        if (Equals(newValue, default(TProp)))\n        {\n            var toRemove = view.Effects.FirstOrDefault(e => e is TEffect);\n            if (toRemove != null)\n            {\n                view.Effects.Remove(toRemove);\n            }\n        }\n        else\n        {\n            view.Effects.Add(new TEffect());\n        }\n    }\n\n    private class CircleEffect : RoutingEffect\n    {\n        public CircleEffect()\n            : base(\"ClientApp.CircleEffect\")\n        {\n        }\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Exceptions/ServiceAuthenticationException.cs",
    "content": "﻿namespace eShop.ClientApp.Exceptions;\n\npublic class ServiceAuthenticationException : Exception\n{\n    public ServiceAuthenticationException()\n    {\n    }\n\n    public ServiceAuthenticationException(string content)\n    {\n        Content = content;\n    }\n\n    public string Content { get; }\n}\n"
  },
  {
    "path": "src/ClientApp/Extensions/DictionaryExtensions.cs",
    "content": "﻿namespace eShop.ClientApp;\n\npublic static class DictionaryExtensions\n{\n    public static bool ValueAsBool(this IDictionary<string, object> dictionary, string key, bool defaultValue = false)\n    {\n        return dictionary.ContainsKey(key) && dictionary[key] is bool dictValue\n            ? dictValue\n            : defaultValue;\n    }\n\n    public static int ValueAsInt(this IDictionary<string, object> dictionary, string key, int defaultValue = 0)\n    {\n        return dictionary.ContainsKey(key) && dictionary[key] is int intValue\n            ? intValue\n            : defaultValue;\n    }\n\n    public static T ValueAs<T>(this IDictionary<string, object> dictionary, string key, T defaultValue = default)\n    {\n        return dictionary.ContainsKey(key) && dictionary[key] is T value\n            ? value\n            : defaultValue;\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Extensions/ICommandExtensions.cs",
    "content": "﻿using System.Windows.Input;\n\nnamespace eShop.ClientApp;\n\npublic static class ICommandExtensions\n{\n    public static void AttemptNotifyCanExecuteChanged<TCommand>(this TCommand command)\n        where TCommand : ICommand\n    {\n        if (command is IRelayCommand rc)\n        {\n            rc?.NotifyCanExecuteChanged();\n        }\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Extensions/VisualElementExtensions.cs",
    "content": "﻿using System.Linq.Expressions;\nusing System.Reflection;\n\nnamespace eShop.ClientApp;\n\npublic static class VisualElementExtensions\n{\n    /// <summary>\n    ///     Extends VisualElement with a new ColorTo method which provides a higher level approach for animating an elements\n    ///     color.\n    /// </summary>\n    /// <returns>A task containing the animation result boolean.</returns>\n    /// <param name=\"element\">VisualElement to process.</param>\n    /// <param name=\"start\">Expression.</param>\n    /// <param name=\"end\">end color.</param>\n    /// <param name=\"rate\">The time, in milliseconds, between frames.</param>\n    /// <param name=\"length\">The number of milliseconds over which to interpolate the animation.</param>\n    /// <param name=\"easing\">The easing function to use to transition in, out, or in and out of the animation.</param>\n    /// <typeparam name=\"TElement\">The 1st type parameter.</typeparam>\n    public static Task<bool> ColorTo<TElement>(this TElement element, Expression<Func<TElement, Color>> start,\n        Color end, uint rate = 16, uint length = 250, Easing easing = null)\n        where TElement : IAnimatable\n    {\n        if (element is null)\n        {\n            return Task.FromResult(false);\n        }\n\n        easing ??= Easing.Linear;\n\n        var member = (MemberExpression)start.Body;\n        var property = member.Member as PropertyInfo;\n\n        var animationName = $\"color_to_{property.Name}_{element.GetHashCode()}\";\n\n        var tcs = new TaskCompletionSource<bool>();\n\n        var elementStartingColor = (Color)property.GetValue(element);\n\n        var transitionAnimation =\n            new Animation(d => property.SetValue(element, elementStartingColor.Lerp(end, (float)d)), 0d, 1d, easing);\n\n        try\n        {\n            element.AbortAnimation(animationName);\n\n            transitionAnimation.Commit(element, animationName, rate, length, finished: (f, a) => tcs.SetResult(a));\n        }\n        catch (InvalidOperationException)\n        {\n        }\n\n        return tcs.Task;\n    }\n\n    /// <summary>\n    ///     Extends VisualElement with a new SizeTo method which provides a higher level approach for animating transitions.\n    /// </summary>\n    /// <returns>A task containing the animation result boolean.</returns>\n    /// <param name=\"element\">The VisualElement to perform animation on.</param>\n    /// <param name=\"start\">The animation starting point.</param>\n    /// <param name=\"end\">The animation ending point.</param>\n    /// <param name=\"rate\">The time, in milliseconds, between frames.</param>\n    /// <param name=\"length\">The number of milliseconds over which to interpolate the animation.</param>\n    /// <param name=\"easing\">The easing function to use to transition in, out, or in and out of the animation.</param>\n    /// <typeparam name=\"TElement\">The 1st type parameter.</typeparam>\n    public static Task<bool> TransitionTo<TElement>(this TElement element, Expression<Func<TElement, double>> start,\n        double end, uint rate = 16, uint length = 250, Easing easing = null)\n        where TElement : IAnimatable\n    {\n        if (element is null)\n        {\n            return Task.FromResult(false);\n        }\n\n        easing ??= Easing.Linear;\n\n        var member = (MemberExpression)start.Body;\n        var property = member.Member as PropertyInfo;\n\n        var animationName = $\"transition_to_{property.Name}_{element.GetHashCode()}\";\n\n        var tcs = new TaskCompletionSource<bool>();\n\n        var elementStartingPosition = (double)property.GetValue(element);\n\n        var transitionAnimation =\n            new Animation(d => property.SetValue(element, d), elementStartingPosition, end, easing);\n\n        try\n        {\n            element.AbortAnimation(animationName);\n\n            transitionAnimation.Commit(element, animationName, rate, length, finished: (f, a) => tcs.SetResult(a));\n        }\n        catch (InvalidOperationException)\n        {\n        }\n\n        return tcs.Task;\n    }\n\n    /// <summary>\n    ///     Extends VisualElement with a new SizeTo method which provides a higher level approach for animating transitions.\n    /// </summary>\n    /// <returns>A task containing the animation result boolean.</returns>\n    /// <param name=\"element\">The VisualElement to perform animation on.</param>\n    /// <param name=\"start\">The animation starting point.</param>\n    /// <param name=\"end\">The animation ending point.</param>\n    /// <param name=\"rate\">The time, in milliseconds, between frames.</param>\n    /// <param name=\"length\">The number of milliseconds over which to interpolate the animation.</param>\n    /// <param name=\"easing\">The easing function to use to transition in, out, or in and out of the animation.</param>\n    /// <typeparam name=\"TElement\">The 1st type parameter.</typeparam>\n    public static Task<bool> TransitionTo<TElement>(this TElement element, Expression<Func<TElement, float>> start,\n        float end, uint rate = 16, uint length = 250, Easing easing = null)\n        where TElement : IAnimatable\n    {\n        if (element is null)\n        {\n            return Task.FromResult(false);\n        }\n\n        easing ??= Easing.Linear;\n\n        var member = (MemberExpression)start.Body;\n        var property = member.Member as PropertyInfo;\n\n        var animationName = $\"transition_to_{property.Name}_{element.GetHashCode()}\";\n\n        var tcs = new TaskCompletionSource<bool>();\n\n        var elementStartingPosition = (float)property.GetValue(element);\n\n        var transitionAnimation =\n            new Animation(d => property.SetValue(element, d), elementStartingPosition, end, easing);\n\n        try\n        {\n            element.AbortAnimation(animationName);\n\n            transitionAnimation.Commit(element, animationName, rate, length, finished: (f, a) => tcs.SetResult(a));\n        }\n        catch (InvalidOperationException)\n        {\n        }\n\n        return tcs.Task;\n    }\n\n    /// <summary>\n    ///     Extends VisualElement with a new SizeTo method which provides a higher level approach for animating transitions.\n    /// </summary>\n    /// <returns>A task containing the animation result boolean.</returns>\n    /// <param name=\"element\">The VisualElement to perform animation on.</param>\n    /// <param name=\"start\">The animation starting point.</param>\n    /// <param name=\"end\">The animation ending point.</param>\n    /// <param name=\"rate\">The time, in milliseconds, between frames.</param>\n    /// <param name=\"length\">The number of milliseconds over which to interpolate the animation.</param>\n    /// <param name=\"easing\">The easing function to use to transition in, out, or in and out of the animation.</param>\n    /// <typeparam name=\"TElement\">The 1st type parameter.</typeparam>\n    public static Task<bool> TransitionTo<TElement>(this TElement element, Expression<Func<TElement, int>> start,\n        int end, uint rate = 16, uint length = 250, Easing easing = null)\n        where TElement : IAnimatable\n    {\n        if (element is null)\n        {\n            return Task.FromResult(false);\n        }\n\n        easing ??= Easing.Linear;\n\n        var member = (MemberExpression)start.Body;\n        var property = member.Member as PropertyInfo;\n\n        var animationName = $\"transition_to_{property.Name}_{element.GetHashCode()}\";\n\n        var tcs = new TaskCompletionSource<bool>();\n\n        var elementStartingPosition = (int)property.GetValue(element);\n\n        var transitionAnimation =\n            new Animation(d => property.SetValue(element, (int)d), elementStartingPosition, end, easing);\n\n        try\n        {\n            element.AbortAnimation(animationName);\n\n            transitionAnimation.Commit(element, animationName, rate, length, finished: (f, a) => tcs.SetResult(a));\n        }\n        catch (InvalidOperationException)\n        {\n        }\n\n        return tcs.Task;\n    }\n\n    public static Task<bool> TransitionTo<TElement>(this TElement element, Expression<Func<TElement, uint>> start,\n        uint end, uint rate = 16, uint length = 250, Easing easing = null)\n        where TElement : IAnimatable\n    {\n        if (element is null)\n        {\n            return Task.FromResult(false);\n        }\n\n        easing ??= Easing.Linear;\n\n        var member = (MemberExpression)start.Body;\n        var property = member.Member as PropertyInfo;\n\n        var animationName = $\"transition_to_{property.Name}_{element.GetHashCode()}\";\n\n        var tcs = new TaskCompletionSource<bool>();\n\n        var elementStartingPosition = (uint)property.GetValue(element);\n\n        var transitionAnimation =\n            new Animation(d => property.SetValue(element, (uint)d), elementStartingPosition, end, easing);\n\n        try\n        {\n            element.AbortAnimation(animationName);\n\n            transitionAnimation.Commit(element, animationName, rate, length, finished: (f, a) => tcs.SetResult(a));\n        }\n        catch (InvalidOperationException)\n        {\n        }\n\n        return tcs.Task;\n    }\n\n    public static Task<bool> TransitionTo<TElement>(this TElement element, string animationName,\n        Action<double> callback, Func<double> start, double end, uint rate = 16, uint length = 250,\n        Easing easing = null)\n        where TElement : IAnimatable\n    {\n        if (element is null)\n        {\n            return Task.FromResult(false);\n        }\n\n        easing ??= Easing.Linear;\n\n        var tcs = new TaskCompletionSource<bool>();\n\n        var transitionAnimation = new Animation(callback, start?.Invoke() ?? default(double), end, easing);\n\n        try\n        {\n            element.AbortAnimation(animationName);\n\n            transitionAnimation.Commit(element, animationName, rate, length, finished: (f, a) => tcs.SetResult(a));\n        }\n        catch (InvalidOperationException)\n        {\n        }\n\n        return tcs.Task;\n    }\n\n    public static Task<bool> TransitionTo<TElement>(this TElement element, string animationName, Action<float> callback,\n        Func<float> start, float end, uint rate = 16, uint length = 250, Easing easing = null)\n        where TElement : IAnimatable\n    {\n        if (element is null)\n        {\n            return Task.FromResult(false);\n        }\n\n        easing ??= Easing.Linear;\n\n        var tcs = new TaskCompletionSource<bool>();\n\n        var transitionAnimation =\n            new Animation(x => callback((float)x), start?.Invoke() ?? default(float), end, easing);\n\n        try\n        {\n            element.AbortAnimation(animationName);\n\n            transitionAnimation.Commit(element, animationName, rate, length, finished: (f, a) => tcs.SetResult(a));\n        }\n        catch (InvalidOperationException)\n        {\n        }\n\n        return tcs.Task;\n    }\n\n    public static Task<bool> TransitionTo<TElement>(this TElement element, string animationName,\n        Action<double> callback, double start, double end, uint rate = 16, uint length = 250, Easing easing = null)\n        where TElement : IAnimatable\n    {\n        if (element is null)\n        {\n            return Task.FromResult(false);\n        }\n\n        easing ??= Easing.Linear;\n\n        var tcs = new TaskCompletionSource<bool>();\n\n        var transitionAnimation = new Animation(callback, start, end, easing);\n\n        try\n        {\n            element.AbortAnimation(animationName);\n\n            transitionAnimation.Commit(element, animationName, rate, length, finished: (f, a) => tcs.SetResult(a));\n        }\n        catch (InvalidOperationException)\n        {\n        }\n\n        return tcs.Task;\n    }\n\n    public static Task<bool> TransitionTo<TElement>(this TElement element, string animationName, Action<int> callback,\n        Func<int> start, int end, uint rate = 16, uint length = 250, Easing easing = null)\n        where TElement : IAnimatable\n    {\n        if (element is null)\n        {\n            return Task.FromResult(false);\n        }\n\n        easing ??= Easing.Linear;\n\n        var tcs = new TaskCompletionSource<bool>();\n\n        var transitionAnimation = new Animation(x => callback((int)x), start?.Invoke() ?? default(int), end, easing);\n\n        try\n        {\n            element.AbortAnimation(animationName);\n\n            transitionAnimation.Commit(element, animationName, rate, length, finished: (f, a) => tcs.SetResult(a));\n        }\n        catch (InvalidOperationException)\n        {\n        }\n\n        return tcs.Task;\n    }\n\n    /// <summary>\n    ///     Lerp the specified color, to and amount.\n    /// </summary>\n    /// <returns>The lerp.</returns>\n    /// <param name=\"color\">Color to calculate from.</param>\n    /// <param name=\"to\">Color to calculate to.</param>\n    /// <param name=\"amount\">Amount of transition to apply.</param>\n    public static Color Lerp(this Color color, Color to, float amount)\n    {\n        return\n            Color.FromRgba(\n                color.Red.Lerp(to.Red, amount),\n                color.Green.Lerp(to.Green, amount),\n                color.Blue.Lerp(to.Blue, amount),\n                color.Alpha.Lerp(to.Alpha, amount));\n    }\n\n    /// <summary>\n    ///     Lerp the specified start, end and amount.\n    /// </summary>\n    /// <returns>The lerp.</returns>\n    /// <param name=\"start\">Start.</param>\n    /// <param name=\"end\">End.</param>\n    /// <param name=\"amount\">Amount.</param>\n    public static double Lerp(this double start, double end, double amount)\n    {\n        var difference = end - start;\n        var adjusted = difference * amount;\n        return start + adjusted;\n    }\n\n    public static float Lerp(this float start, float end, float amount)\n    {\n        var difference = end - start;\n        var adjusted = difference * amount;\n        return start + adjusted;\n    }\n\n    public static int Lerp(this int start, int end, double amount)\n    {\n        var difference = end - start;\n        var adjusted = (int)(difference * amount);\n        return start + adjusted;\n    }\n\n    public static byte Lerp(this byte start, byte end, double amount)\n    {\n        var difference = end - start;\n        var adjusted = (int)(difference * amount);\n\n        return (byte)(start + adjusted);\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/GlobalSuppressions.cs",
    "content": "﻿// This file is used by Code Analysis to maintain SuppressMessage\n// attributes that are applied to this project.\n// Project-level suppressions either have no target or are given\n// a specific target and scoped to a namespace, type, member, etc.\n\nusing System.Diagnostics.CodeAnalysis;\n\n[assembly: SuppressMessage(\n    \"Interoperability\",\n    \"CA1416:Validate platform compatibility\",\n    Justification = \"Using latest Microsoft MAUI components\",\n    Scope = \"module\")]\n"
  },
  {
    "path": "src/ClientApp/GlobalUsings.cs",
    "content": "﻿global using eShop.ClientApp.ViewModels;\nglobal using CommunityToolkit.Mvvm.ComponentModel;\nglobal using CommunityToolkit.Mvvm.Input;\n"
  },
  {
    "path": "src/ClientApp/Helpers/EasingHelper.cs",
    "content": "﻿using eShop.ClientApp.Animations.Base;\n\nnamespace eShop.ClientApp.Helpers;\n\npublic static class EasingHelper\n{\n    public static Easing GetEasing(EasingType type)\n    {\n        return type switch\n        {\n            EasingType.BounceIn => Easing.BounceIn,\n            EasingType.BounceOut => Easing.BounceOut,\n            EasingType.CubicIn => Easing.CubicIn,\n            EasingType.CubicInOut => Easing.CubicInOut,\n            EasingType.CubicOut => Easing.CubicOut,\n            EasingType.Linear => Easing.Linear,\n            EasingType.SinIn => Easing.SinIn,\n            EasingType.SinInOut => Easing.SinInOut,\n            EasingType.SinOut => Easing.SinOut,\n            EasingType.SpringIn => Easing.SpringIn,\n            EasingType.SpringOut => Easing.SpringOut,\n            _ => null\n        };\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Helpers/UriHelper.cs",
    "content": "﻿namespace eShop.ClientApp.Helpers;\n\npublic static class UriHelper\n{\n    private static readonly char[] _trims = {'\\\\', '/'};\n    public static string CombineUri(params string[] uriParts)\n    {\n        var uri = string.Empty;\n        \n        if (uriParts != null && uriParts.Length > 0)\n        {\n            uri = (uriParts[0] ?? string.Empty).TrimEnd(_trims);\n            for (var i = 1; i < uriParts.Length; i++)\n            {\n                uri = $\"{uri.TrimEnd(_trims)}/{(uriParts[i] ?? string.Empty).TrimStart(_trims)}\";\n            }\n        }\n\n        return uri;\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/MauiProgram.cs",
    "content": "﻿using CommunityToolkit.Maui;\nusing eShop.ClientApp.Services;\nusing eShop.ClientApp.Services.AppEnvironment;\nusing eShop.ClientApp.Services.Basket;\nusing eShop.ClientApp.Services.Catalog;\nusing eShop.ClientApp.Services.FixUri;\nusing eShop.ClientApp.Services.Identity;\nusing eShop.ClientApp.Services.Location;\nusing eShop.ClientApp.Services.OpenUrl;\nusing eShop.ClientApp.Services.Order;\nusing eShop.ClientApp.Services.RequestProvider;\nusing eShop.ClientApp.Services.Settings;\nusing eShop.ClientApp.Services.Theme;\nusing eShop.ClientApp.Views;\nusing IdentityModel.OidcClient;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Microsoft.Extensions.Logging;\nusing IBrowser = IdentityModel.OidcClient.Browser.IBrowser;\n\nnamespace eShop.ClientApp;\n\npublic static class MauiProgram\n{\n    public static MauiApp CreateMauiApp()\n    {\n        return MauiApp\n            .CreateBuilder()\n            .UseMauiApp<App>()\n            .ConfigureEffects(\n                effects =>\n                {\n                })\n            .UseMauiCommunityToolkit()\n            .ConfigureFonts(\n                fonts =>\n                {\n                    fonts.AddFont(\"FontAwesomeRegular.otf\", \"FontAwesome-Regular\");\n                    fonts.AddFont(\"FontAwesomeSolid.otf\", \"FontAwesome-Solid\");\n                    fonts.AddFont(\"Montserrat-Bold.ttf\", \"Montserrat-Bold\");\n                    fonts.AddFont(\"Montserrat-Regular.ttf\", \"Montserrat-Regular\");\n                })\n            .ConfigureEssentials(\n                essentials =>\n                {\n                    essentials\n                        .AddAppAction(AppActions.ViewProfileAction)\n                        .OnAppAction(App.HandleAppActions);\n                })\n#if !WINDOWS\n            .UseMauiMaps()\n#endif\n            .ConfigureHandlers()\n            .RegisterAppServices()\n            .RegisterViewModels()\n            .RegisterViews()\n            .Build();\n    }\n\n    public static MauiAppBuilder ConfigureHandlers(this MauiAppBuilder mauiAppBuilder)\n    {\n#if IOS || MACCATALYST\n        mauiAppBuilder.ConfigureMauiHandlers(handlers =>\n        {\n            handlers.AddHandler<Microsoft.Maui.Controls.CollectionView, Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2>();\n            handlers.AddHandler<Microsoft.Maui.Controls.CarouselView, Microsoft.Maui.Controls.Handlers.Items2.CarouselViewHandler2>();\n        });\n#endif\n\n        return mauiAppBuilder;\n    }\n\n    public static MauiAppBuilder RegisterAppServices(this MauiAppBuilder mauiAppBuilder)\n    {\n        mauiAppBuilder.Services.AddSingleton<ISettingsService, SettingsService>();\n        mauiAppBuilder.Services.AddSingleton<INavigationService, MauiNavigationService>();\n        mauiAppBuilder.Services.AddSingleton<IDialogService, DialogService>();\n        mauiAppBuilder.Services.AddSingleton<IOpenUrlService, OpenUrlService>();\n        mauiAppBuilder.Services.AddSingleton<IRequestProvider>(\n            sp =>\n            {\n                var debugHttpHandler = sp.GetKeyedService<HttpMessageHandler>(\"DebugHttpMessageHandler\");\n                return new RequestProvider(debugHttpHandler);\n            });\n        mauiAppBuilder.Services.AddSingleton<IIdentityService, IdentityService>(\n            sp =>\n            {\n                var browser = sp.GetRequiredService<IBrowser>();\n                var settingsService = sp.GetRequiredService<ISettingsService>();\n                var debugHttpHandler = sp.GetKeyedService<HttpMessageHandler>(\"DebugHttpMessageHandler\");\n                return new IdentityService(browser, settingsService, debugHttpHandler);\n            });\n        mauiAppBuilder.Services.AddSingleton<IFixUriService, FixUriService>();\n        mauiAppBuilder.Services.AddSingleton<ILocationService, LocationService>();\n\n        mauiAppBuilder.Services.AddSingleton<ITheme, Theme>();\n\n        mauiAppBuilder.Services.AddSingleton<IAppEnvironmentService, AppEnvironmentService>(\n            serviceProvider =>\n            {\n                var requestProvider = serviceProvider.GetRequiredService<IRequestProvider>();\n                var fixUriService = serviceProvider.GetRequiredService<IFixUriService>();\n                var settingsService = serviceProvider.GetRequiredService<ISettingsService>();\n                var identityService = serviceProvider.GetRequiredService<IIdentityService>();\n\n                var aes =\n                    new AppEnvironmentService(\n                        new BasketMockService(), new BasketService(identityService, settingsService, fixUriService),\n                        new CatalogMockService(), new CatalogService(settingsService, requestProvider, fixUriService),\n                        new OrderMockService(), new OrderService(identityService, settingsService, requestProvider),\n                        new IdentityMockService(), identityService);\n\n                aes.UpdateDependencies(settingsService.UseMocks);\n                return aes;\n            });\n\n        mauiAppBuilder.Services.AddTransient<IBrowser, MauiAuthenticationBrowser>();\n\n#if DEBUG\n        mauiAppBuilder.Services.AddKeyedSingleton<HttpMessageHandler>(\n            \"DebugHttpMessageHandler\",\n            (sp, key) =>\n            {\n#if ANDROID\n                var handler = new Xamarin.Android.Net.AndroidMessageHandler();\n                handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>\n                {\n                    if (cert != null && cert.Issuer.Equals(\"CN=localhost\", StringComparison.OrdinalIgnoreCase))\n                    {\n                        return true;\n                    }\n                    \n                    return errors == System.Net.Security.SslPolicyErrors.None;\n                };\n                return handler;\n#elif IOS || MACCATALYST\n                var handler = new NSUrlSessionHandler\n                {\n                    TrustOverrideForUrl = (sender, url, trust) => url.StartsWith(\"https://localhost\", StringComparison.OrdinalIgnoreCase),\n                };\n                return handler;\n#else\n                return null;\n#endif\n            });\n        \n        mauiAppBuilder.Logging.AddDebug();\n#endif\n\n        return mauiAppBuilder;\n    }\n\n    public static MauiAppBuilder RegisterViewModels(this MauiAppBuilder mauiAppBuilder)\n    {\n        mauiAppBuilder.Services.AddSingleton<MainViewModel>();\n        mauiAppBuilder.Services.AddSingleton<LoginViewModel>();\n        mauiAppBuilder.Services.AddSingleton<BasketViewModel>();\n        mauiAppBuilder.Services.AddSingleton<CatalogViewModel>();\n        mauiAppBuilder.Services.AddSingleton<CatalogItemViewModel>();\n        mauiAppBuilder.Services.AddSingleton<MapViewModel>();\n        mauiAppBuilder.Services.AddSingleton<ProfileViewModel>();\n\n        mauiAppBuilder.Services.AddTransient<CheckoutViewModel>();\n        mauiAppBuilder.Services.AddTransient<OrderDetailViewModel>();\n        mauiAppBuilder.Services.AddTransient<SettingsViewModel>();\n\n        return mauiAppBuilder;\n    }\n\n    public static MauiAppBuilder RegisterViews(this MauiAppBuilder mauiAppBuilder)\n    {\n        mauiAppBuilder.Services.AddSingleton<CatalogItemView>();\n\n        mauiAppBuilder.Services.AddTransient<BasketView>();\n        mauiAppBuilder.Services.AddTransient<CatalogView>();\n        mauiAppBuilder.Services.AddTransient<CheckoutView>();\n        mauiAppBuilder.Services.AddTransient<FiltersView>();\n        mauiAppBuilder.Services.AddTransient<LoginView>();\n        mauiAppBuilder.Services.AddTransient<OrderDetailView>();\n        mauiAppBuilder.Services.AddTransient<MapView>();\n        mauiAppBuilder.Services.AddTransient<ProfileView>();\n        mauiAppBuilder.Services.AddTransient<SettingsView>();\n\n        return mauiAppBuilder;\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Messages/ProductCountChangedMessage.cs",
    "content": "﻿using CommunityToolkit.Mvvm.Messaging.Messages;\n\nnamespace eShop.ClientApp.Messages;\n\npublic class ProductCountChangedMessage(int count) : ValueChangedMessage<int>(count);\n"
  },
  {
    "path": "src/ClientApp/Models/Basket/BasketItem.cs",
    "content": "﻿namespace eShop.ClientApp.Models.Basket;\n\npublic class BasketItem : BindableObject\n{\n    private int _quantity;\n\n    public string Id { get; set; }\n\n    public int ProductId { get; set; }\n\n    public string ProductName { get; set; }\n\n    public decimal UnitPrice { get; set; }\n\n    public decimal OldUnitPrice { get; set; }\n\n    public bool HasNewPrice => OldUnitPrice != 0.0m;\n\n    public int Quantity\n    {\n        get => _quantity;\n        set\n        {\n            _quantity = value;\n            OnPropertyChanged();\n        }\n    }\n\n    public string PictureUrl { get; set; }\n\n    public decimal Total => Quantity * UnitPrice;\n\n    public override string ToString()\n    {\n        return $\"Product Id: {ProductId}, Quantity: {Quantity}\";\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/Basket/CustomerBasket.cs",
    "content": "﻿namespace eShop.ClientApp.Models.Basket;\n\npublic class CustomerBasket\n{\n    private readonly List<BasketItem> _items = new();\n    public string BuyerId { get; set; }\n    public IReadOnlyList<BasketItem> Items => _items;\n\n    public int ItemCount => _items.Sum(x => x.Quantity);\n\n    public void AddItemToBasket(BasketItem basketItem)\n    {\n        foreach (var item in _items)\n        {\n            if (item.ProductId == basketItem.ProductId)\n            {\n                item.Quantity++;\n                return;\n            }\n        }\n\n        _items.Add(basketItem);\n    }\n\n    public void RemoveItemFromBasket(BasketItem basketItem)\n    {\n        for (var i = _items.Count - 1; i >= 0; i--)\n        {\n            if (_items[i].ProductId == basketItem.ProductId)\n            {\n                _items.RemoveAt(i);\n                return;\n            }\n        }\n    }\n\n    public void ClearBasket()\n    {\n        _items.Clear();\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/Catalog/CatalogBrand.cs",
    "content": "﻿namespace eShop.ClientApp.Models.Catalog;\n\npublic class CatalogBrand\n{\n    public int Id { get; set; }\n    public string Brand { get; set; }\n\n    public override string ToString()\n    {\n        return Brand;\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/Catalog/CatalogItem.cs",
    "content": "namespace eShop.ClientApp.Models.Catalog;\n\npublic class CatalogItem\n{\n    public int Id { get; set; }\n    public string Name { get; set; }\n    \n    public string Description { get; set; }\n    public decimal Price { get; set; }\n    public string PictureUri { get; set; }\n    public int CatalogBrandId { get; set; }\n    public CatalogBrand CatalogBrand { get; set; }\n    public int CatalogTypeId { get; set; }\n    public CatalogType CatalogType { get; set; }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/Catalog/CatalogRoot.cs",
    "content": "﻿namespace eShop.ClientApp.Models.Catalog;\n\npublic class CatalogRoot\n{\n    public int PageIndex { get; set; }\n    public int PageSize { get; set; }\n    public int Count { get; set; }\n    public List<CatalogItem> Data { get; set; }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/Catalog/CatalogType.cs",
    "content": "﻿namespace eShop.ClientApp.Models.Catalog;\n\npublic class CatalogType\n{\n    public int Id { get; set; }\n    public string Type { get; set; }\n\n    public override string ToString()\n    {\n        return Type;\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/Location/GeolocationError.cs",
    "content": "﻿namespace eShop.ClientApp.Models.Location;\n\npublic enum GeolocationError\n{\n    PositionUnavailable,\n    Unauthorized\n}\n"
  },
  {
    "path": "src/ClientApp/Models/Location/GeolocationException.cs",
    "content": "﻿namespace eShop.ClientApp.Models.Location;\n\npublic class GeolocationException : Exception\n{\n    public GeolocationException(GeolocationError error)\n        : base(\"A geolocation error occurred: \" + error)\n    {\n        if (!Enum.IsDefined(typeof(GeolocationError), error))\n        {\n            throw new ArgumentException(\"error is not a valid GeolocationError member\", nameof(error));\n        }\n\n        Error = error;\n    }\n\n    public GeolocationException(GeolocationError error, Exception innerException)\n        : base(\"A geolocation error occurred: \" + error, innerException)\n    {\n        if (!Enum.IsDefined(typeof(GeolocationError), error))\n        {\n            throw new ArgumentException(\"error is not a valid GeolocationError member\", nameof(error));\n        }\n\n        Error = error;\n    }\n\n    public GeolocationError Error { get; private set; }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/Location/Location.cs",
    "content": "﻿namespace eShop.ClientApp.Models.Location;\n\npublic class Location\n{\n    public double Longitude { get; set; }\n    public double Latitude { get; set; }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/Location/Position.cs",
    "content": "﻿namespace eShop.ClientApp.Models.Location;\n\npublic class Position\n{\n    public Position()\n    {\n    }\n\n    public Position(double latitude, double longitude)\n    {\n        Timestamp = DateTimeOffset.UtcNow;\n        Latitude = latitude;\n        Longitude = longitude;\n    }\n\n    public Position(Position position)\n    {\n        if (position == null)\n        {\n            throw new ArgumentNullException(nameof(position));\n        }\n\n        Timestamp = position.Timestamp;\n        Latitude = position.Latitude;\n        Longitude = position.Longitude;\n        Altitude = position.Altitude;\n        AltitudeAccuracy = position.AltitudeAccuracy;\n        Accuracy = position.Accuracy;\n        Heading = position.Heading;\n        Speed = position.Speed;\n    }\n\n    public DateTimeOffset Timestamp { get; set; }\n    public double Latitude { get; set; }\n    public double Longitude { get; set; }\n    public double Altitude { get; set; }\n    public double Accuracy { get; set; }\n    public double AltitudeAccuracy { get; set; }\n    public double Heading { get; set; }\n    public double Speed { get; set; }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/Marketing/Campaign.cs",
    "content": "﻿namespace eShop.ClientApp.Models.Marketing;\n\npublic class Campaign\n{\n    public int Id { get; set; }\n\n    public string Name { get; set; }\n\n    public string Description { get; set; }\n\n    public DateTime From { get; set; }\n\n    public DateTime To { get; set; }\n\n    public string PictureUri { get; set; }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/Marketing/CampaignItem.cs",
    "content": "﻿namespace eShop.ClientApp.Models.Marketing;\n\npublic class CampaignItem\n{\n    public int Id { get; set; }\n\n    public string Name { get; set; }\n\n    public string Description { get; set; }\n\n    public DateTime From { get; set; }\n\n    public DateTime To { get; set; }\n\n    public string PictureUri { get; set; }\n\n    public string DetailsUri { get; set; }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/Marketing/CampaignRoot.cs",
    "content": "﻿namespace eShop.ClientApp.Models.Marketing;\n\npublic class CampaignRoot\n{\n    public int PageIndex { get; set; }\n    public int PageSize { get; set; }\n    public int Count { get; set; }\n    public List<CampaignItem> Data { get; set; }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/Navigation/TabParameter.cs",
    "content": "﻿namespace eShop.ClientApp.Models.Navigation;\n\npublic class TabParameter\n{\n    public int TabIndex { get; set; }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/Orders/CancelOrderCommand.cs",
    "content": "﻿namespace eShop.ClientApp.Models.Orders;\n\npublic class CancelOrderCommand\n{\n    public CancelOrderCommand(int orderNumber)\n    {\n        OrderNumber = orderNumber;\n    }\n\n    public int OrderNumber { get; }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/Orders/CardType.cs",
    "content": "﻿namespace eShop.ClientApp.Models.Orders;\n\npublic class CardType\n{\n    public int Id { get; set; }\n    public string Name { get; set; }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/Orders/Order.cs",
    "content": "using System.Text.Json.Serialization;\n\nnamespace eShop.ClientApp.Models.Orders;\n\npublic class Order\n{\n    public Order()\n    {\n        SequenceNumber = 1;\n        OrderItems = new List<OrderItem>();\n    }\n\n    [JsonPropertyName(\"userId\")]\n    public string UserId { get; set; }\n    \n    [JsonPropertyName(\"userName\")]\n    public string UserName { get; set; }\n    \n    public int SequenceNumber { get; set; }\n\n    [JsonPropertyName(\"date\")] public DateTime OrderDate { get; set; }\n\n    [JsonPropertyName(\"status\")] public string OrderStatus { get; set; }\n\n    [JsonPropertyName(\"city\")] public string ShippingCity { get; set; }\n\n    [JsonPropertyName(\"street\")] public string ShippingStreet { get; set; }\n\n    [JsonPropertyName(\"state\")] public string ShippingState { get; set; }\n\n    [JsonPropertyName(\"country\")] public string ShippingCountry { get; set; }\n\n    [JsonPropertyName(\"zipCode\")] public string ShippingZipCode { get; set; }\n\n    public int CardTypeId { get; set; }\n\n    public string CardNumber { get; set; }\n\n    public string CardHolderName { get; set; }\n\n    public DateTime CardExpiration { get; set; }\n\n    public string CardSecurityNumber { get; set; }\n\n    [JsonPropertyName(\"items\")]\n    public List<OrderItem> OrderItems { get; set; }\n\n    [JsonPropertyName(\"total\")] public decimal Total { get; set; }\n\n    [JsonPropertyName(\"ordernumber\")] public int OrderNumber { get; set; }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/Orders/OrderCheckout.cs",
    "content": "using System.ComponentModel.DataAnnotations;\nusing eShop.ClientApp.Models.Orders;\n\nnamespace eShop.ClientApp.Models.Basket;\n\npublic class OrderCheckout\n{\n    [Required]\n    public string City { get; set; }\n    [Required]\n    public string Street { get; set; }\n    [Required]\n    public string State { get; set; }\n    [Required]\n    public string Country { get; set; }\n\n    public string ZipCode { get; set; }\n    [Required]\n    public string CardNumber { get; set; }\n    [Required]\n    public string CardHolderName { get; set; }\n\n    [Required]\n    public DateTime CardExpiration { get; set; }\n\n    [Required]\n    public string CardSecurityNumber { get; set; }\n\n    public int CardTypeId { get; set; }\n\n    public string Buyer { get; set; }\n\n    public IList<OrderItem> Items { get; set; }\n    \n    [Required]\n    public Guid RequestId { get; set; }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/Orders/OrderItem.cs",
    "content": "using System.Text.Json.Serialization;\n\nnamespace eShop.ClientApp.Models.Orders;\n\npublic class OrderItem\n{\n    public long ProductId { get; set; }\n    \n    public Guid? OrderId { get; set; }\n\n    [JsonPropertyName(\"unitprice\")] \n    public decimal UnitPrice { get; set; }\n\n    [JsonPropertyName(\"productname\")] \n    public string ProductName { get; set; }\n\n    [JsonPropertyName(\"pictureurl\")] \n    public string PictureUrl { get; set; }\n\n    [JsonPropertyName(\"quantity\")] \n    public int Quantity { get; set; }\n\n    public decimal Discount { get; set; }\n    public decimal Total => Quantity * UnitPrice;\n\n    public override string ToString()\n    {\n        return String.Format(\"Product Id: {0}, Quantity: {1}\", ProductId, Quantity);\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/Permissions/Permission.cs",
    "content": "﻿namespace eShop.ClientApp.Models.Permissions;\n\npublic enum Permission\n{\n    Unknown,\n    Location,\n    LocationAlways,\n    LocationWhenInUse\n}\n"
  },
  {
    "path": "src/ClientApp/Models/Permissions/PermissionStatus.cs",
    "content": "﻿namespace eShop.ClientApp.Models.Permissions;\n\npublic enum PermissionStatus\n{\n    Denied,\n    Disabled,\n    Granted,\n    Restricted,\n    Unknown\n}\n"
  },
  {
    "path": "src/ClientApp/Models/Token/UserToken.cs",
    "content": "using System.Text.Json.Serialization;\n\nnamespace eShop.ClientApp.Models.Token;\n\npublic class UserToken\n{\n    [JsonPropertyName(\"id_token\")] public string IdToken { get; set; }\n\n    [JsonPropertyName(\"access_token\")] public string AccessToken { get; set; }\n\n    [JsonPropertyName(\"refresh_token\")]\n    public string RefreshToken { get; set; }\n    \n    [JsonPropertyName(\"expires_at\")]\n    public DateTimeOffset ExpiresAt { get; set; }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/User/Address.cs",
    "content": "﻿namespace eShop.ClientApp.Models.User;\n\npublic class Address\n{\n    public Guid Id { get; set; }\n    public string Street { get; set; }\n    public string City { get; set; }\n    public string State { get; set; }\n    public string StateCode { get; set; }\n    public string Country { get; set; }\n    public string CountryCode { get; set; }\n    public string ZipCode { get; set; }\n    public double Latitude { get; set; }\n    public double Longitude { get; set; }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/User/LogoutParameter.cs",
    "content": "﻿namespace eShop.ClientApp.Models.User;\n\npublic class LogoutParameter\n{\n    public bool Logout { get; set; }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/User/PaymentInfo.cs",
    "content": "﻿using eShop.ClientApp.Models.Orders;\n\nnamespace eShop.ClientApp.Models.User;\n\npublic class PaymentInfo\n{\n    public Guid Id { get; set; }\n    public string CardNumber { get; set; }\n    public string SecurityNumber { get; set; }\n    public int ExpirationMonth { get; set; }\n    public int ExpirationYear { get; set; }\n    public string CardHolderName { get; set; }\n    public CardType CardType { get; set; }\n    public string Expiration { get; set; }\n}\n"
  },
  {
    "path": "src/ClientApp/Models/User/UserInfo.cs",
    "content": "﻿using System.Text.Json.Serialization;\n\nnamespace eShop.ClientApp.Models.User;\n\npublic class UserInfo\n{\n    [JsonPropertyName(\"sub\")] public string UserId { get; set; }\n\n    [JsonPropertyName(\"preferred_username\")]\n    public string PreferredUsername { get; set; }\n\n    [JsonPropertyName(\"name\")] public string Name { get; set; }\n\n    [JsonPropertyName(\"last_name\")] public string LastName { get; set; }\n\n    [JsonPropertyName(\"card_number\")] public string CardNumber { get; set; }\n\n    [JsonPropertyName(\"card_holder\")] public string CardHolder { get; set; }\n\n    [JsonPropertyName(\"card_security_number\")]\n    public string CardSecurityNumber { get; set; }\n\n    [JsonPropertyName(\"address_city\")] public string Address { get; set; }\n\n    [JsonPropertyName(\"address_country\")] public string Country { get; set; }\n\n    [JsonPropertyName(\"address_state\")] public string State { get; set; }\n\n    [JsonPropertyName(\"address_street\")] public string Street { get; set; }\n\n    [JsonPropertyName(\"address_zip_code\")] public string ZipCode { get; set; }\n\n    [JsonPropertyName(\"email\")] public string Email { get; set; }\n\n    [JsonPropertyName(\"email_verified\")] public bool EmailVerified { get; set; }\n\n    [JsonPropertyName(\"phone_number\")] public string PhoneNumber { get; set; }\n\n    [JsonPropertyName(\"phone_number_verified\")]\n    public bool PhoneNumberVerified { get; set; }\n\n    public static UserInfo Default { get; } = new();\n}\n"
  },
  {
    "path": "src/ClientApp/Platforms/Android/AndroidManifest.xml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <application android:allowBackup=\"true\" android:supportsRtl=\"true\" android:usesCleartextTraffic=\"true\" android:networkSecurityConfig=\"@xml/network_security_config\">\n        <meta-data android:name=\"com.google.android.geo.API_KEY\" android:value=\"YOUR_KEY_GOES_HERE\"/>\n        <meta-data android:name=\"com.google.android.gms.version\" android:value=\"@integer/google_play_services_version\"/>\n    </application>\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>\n    <uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>\n    <uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>\n    <queries>\n        <intent>\n            <action android:name=\"android.support.customtabs.action.CustomTabsService\" />\n        </intent>\n    </queries>\n</manifest>"
  },
  {
    "path": "src/ClientApp/Platforms/Android/MainActivity.cs",
    "content": "﻿using Android.App;\nusing Android.Content.PM;\n\nnamespace eShop.ClientApp;\n\n[Activity(Theme = \"@style/Maui.SplashTheme\", MainLauncher = true,\n    ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode |\n                           ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]\n[IntentFilter(new[] {Platform.Intent.ActionAppAction},\n    Categories = new[] {global::Android.Content.Intent.CategoryDefault})]\npublic class MainActivity : MauiAppCompatActivity\n{\n    protected override void OnResume()\n    {\n        base.OnResume();\n\n        Platform.OnResume(this);\n    }\n\n    protected override void OnNewIntent(Android.Content.Intent intent)\n    {\n        base.OnNewIntent(intent);\n\n        Platform.OnNewIntent(intent);\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Platforms/Android/MainApplication.cs",
    "content": "﻿using Android.App;\nusing Android.Runtime;\n\nnamespace eShop.ClientApp;\n\n[Application]\npublic class MainApplication : MauiApplication\n{\n    public MainApplication(IntPtr handle, JniHandleOwnership ownership)\n        : base(handle, ownership)\n    {\n    }\n\n    protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();\n}\n"
  },
  {
    "path": "src/ClientApp/Platforms/Android/Resources/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#512BD4</color>\n    <color name=\"colorPrimaryDark\">#2B0B98</color>\n    <color name=\"colorAccent\">#2B0B98</color>\n</resources>"
  },
  {
    "path": "src/ClientApp/Platforms/Android/Resources/xml/network_security_config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<network-security-config>\n    <domain-config cleartextTrafficPermitted=\"true\">\n        <domain includeSubdomains=\"true\">10.0.2.2</domain>\n    </domain-config>\n</network-security-config>"
  },
  {
    "path": "src/ClientApp/Platforms/Android/WebAuthenticationCallbackActivity.cs",
    "content": "﻿using Android.App;\nusing Android.Content.PM;\n\nnamespace eShop.ClientApp;\n\n[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop, Exported = true)]\n[IntentFilter(new[] { Android.Content.Intent.ActionView },\n    Categories = new[] { Android.Content.Intent.CategoryDefault, Android.Content.Intent.CategoryBrowsable },\n    DataScheme = \"maui\",\n    DataHost = \"authcallback\")]\npublic class WebAuthenticationCallbackActivity : Microsoft.Maui.Authentication.WebAuthenticatorCallbackActivity\n{\n\n}\n"
  },
  {
    "path": "src/ClientApp/Platforms/MacCatalyst/AppDelegate.cs",
    "content": "﻿using Foundation;\n\nnamespace eShop.ClientApp;\n\n[Register(\"AppDelegate\")]\npublic class AppDelegate : MauiUIApplicationDelegate\n{\n    protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();\n}\n"
  },
  {
    "path": "src/ClientApp/Platforms/MacCatalyst/Entitlements.Debug.plist",
    "content": "﻿<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <!-- Enable this value to use browser developer tools while debugging.-->\n        <!-- See https://aka.ms/blazor-hybrid-developer-tools -->\n        <key>com.apple.security.get-task-allow</key>\n        <true/>\n    </dict>\n</plist>\n\n"
  },
  {
    "path": "src/ClientApp/Platforms/MacCatalyst/Entitlements.Release.plist",
    "content": "﻿<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <!-- See https://aka.ms/maui-publish-app-store#add-entitlements for more information about adding entitlements.-->\n    <dict>\n        <key>com.apple.security.app-sandbox</key>\n        <true/>\n        <key>com.apple.security.network.client</key>\n        <true/>\n    </dict>\n</plist>\n\n"
  },
  {
    "path": "src/ClientApp/Platforms/MacCatalyst/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>UIDeviceFamily</key>\n        <array>\n            <integer>1</integer>\n            <integer>2</integer>\n        </array>\n        <key>UIRequiredDeviceCapabilities</key>\n        <array>\n            <string>arm64</string>\n        </array>\n        <key>UISupportedInterfaceOrientations</key>\n        <array>\n            <string>UIInterfaceOrientationPortrait</string>\n            <string>UIInterfaceOrientationLandscapeLeft</string>\n            <string>UIInterfaceOrientationLandscapeRight</string>\n        </array>\n        <key>UISupportedInterfaceOrientations~ipad</key>\n        <array>\n            <string>UIInterfaceOrientationPortrait</string>\n            <string>UIInterfaceOrientationPortraitUpsideDown</string>\n            <string>UIInterfaceOrientationLandscapeLeft</string>\n            <string>UIInterfaceOrientationLandscapeRight</string>\n        </array>\n        <key>NSAppTransportSecurity</key>\n        <dict>\n            <key>NSAllowsArbitraryLoads</key>\n            <true/>\n        </dict>\n        <key>XSAppIconAssets</key>\n        <string>Assets.xcassets/appicon.appiconset</string>\n        <key>NSLocationWhenInUseUsageDescription</key>\n        <string>Can we use your location when your app is being used?</string>\n    </dict>\n</plist>\n"
  },
  {
    "path": "src/ClientApp/Platforms/MacCatalyst/Program.cs",
    "content": "﻿using UIKit;\n\nnamespace eShop.ClientApp;\n\npublic class Program\n{\n    // This is the main entry point of the application.\n    static void Main(string[] args)\n    {\n        // if you want to use a different Application Delegate class from \"AppDelegate\"\n        // you can specify it here.\n        UIApplication.Main(args, null, typeof(AppDelegate));\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Platforms/Windows/App.xaml",
    "content": "﻿<maui:MauiWinUIApplication\n    x:Class=\"eShop.ClientApp.WinUI.App\"\n    xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n    xmlns:maui=\"using:Microsoft.Maui\"\n    xmlns:local=\"using:eShopOnContainers.WinUI\">\n\n</maui:MauiWinUIApplication>\n"
  },
  {
    "path": "src/ClientApp/Platforms/Windows/App.xaml.cs",
    "content": "﻿// To learn more about WinUI, the WinUI project structure,\n// and more about our project templates, see: http://aka.ms/winui-project-info.\n\nnamespace eShop.ClientApp.WinUI;\n\n/// <summary>\n/// Provides application-specific behavior to supplement the default Application class.\n/// </summary>\npublic partial class App : MauiWinUIApplication\n{\n    /// <summary>\n    /// Initializes the singleton application object.  This is the first line of authored code\n    /// executed, and as such is the logical equivalent of main() or WinMain().\n    /// </summary>\n    public App() => this.InitializeComponent();\n\n    protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();\n}\n\n"
  },
  {
    "path": "src/ClientApp/Platforms/Windows/Package.appxmanifest",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Package\n        xmlns:uap=\"http://schemas.microsoft.com/appx/manifest/uap/windows10\"\n        xmlns:rescap=\"http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities\"\n        xmlns=\"http://schemas.microsoft.com/appx/manifest/foundation/windows10\"\n        IgnorableNamespaces=\"uap rescap\">\n\n    <Identity Publisher=\"CN=User Name\"/>\n\n    <Properties>\n        <PublisherDisplayName>User Name</PublisherDisplayName>\n    </Properties>\n\n    <Dependencies>\n        <TargetDeviceFamily Name=\"Windows.Universal\" MinVersion=\"10.0.17763.0\" MaxVersionTested=\"10.0.19041.0\"/>\n        <TargetDeviceFamily Name=\"Windows.Desktop\" MinVersion=\"10.0.17763.0\" MaxVersionTested=\"10.0.19041.0\"/>\n    </Dependencies>\n\n    <Resources>\n        <Resource Language=\"x-generate\"/>\n    </Resources>\n\n    <Applications>\n        <Application Id=\"App\" Executable=\"$targetnametoken$.exe\" EntryPoint=\"$targetentrypoint$\">\n            <uap:VisualElements/>\n        </Application>\n    </Applications>\n\n    <Capabilities>\n        <rescap:Capability Name=\"runFullTrust\"/>\n    </Capabilities>\n\n</Package>\n"
  },
  {
    "path": "src/ClientApp/Platforms/Windows/app.manifest",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<assembly manifestVersion=\"1.0\" xmlns=\"urn:schemas-microsoft-com:asm.v1\">\n    <assemblyIdentity version=\"1.0.0.0\" name=\"ClientApp.WinUI.app\"/>\n\n    <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n        <windowsSettings>\n            <!-- The combination of below two tags have the following effect:\n                 1) Per-Monitor for >= Windows 10 Anniversary Update\n                 2) System < Windows 10 Anniversary Update\n            -->\n            <dpiAware xmlns=\"http://schemas.microsoft.com/SMI/2005/WindowsSettings\">true/PM</dpiAware>\n            <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2, PerMonitor\n            </dpiAwareness>\n        </windowsSettings>\n    </application>\n</assembly>\n"
  },
  {
    "path": "src/ClientApp/Platforms/iOS/AppDelegate.cs",
    "content": "﻿using Foundation;\n\nnamespace eShop.ClientApp;\n\n[Register(\"AppDelegate\")]\npublic class AppDelegate : MauiUIApplicationDelegate\n{\n    protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();\n}\n"
  },
  {
    "path": "src/ClientApp/Platforms/iOS/Entitlements.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>keychain-access-groups</key>\n\t<array>\n\t\t<string>$(AppIdentifierPrefix)com.companyname.eshop</string>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "src/ClientApp/Platforms/iOS/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"[]>\n<plist version=\"1.0\">\n    <dict>\n        <key>LSRequiresIPhoneOS</key>\n        <true/>\n        <key>UIDeviceFamily</key>\n        <array>\n            <integer>1</integer>\n            <integer>2</integer>\n        </array>\n        <key>UIRequiredDeviceCapabilities</key>\n        <array>\n            <string>arm64</string>\n        </array>\n        <key>UISupportedInterfaceOrientations</key>\n        <array>\n            <string>UIInterfaceOrientationPortrait</string>\n            <string>UIInterfaceOrientationLandscapeLeft</string>\n            <string>UIInterfaceOrientationLandscapeRight</string>\n        </array>\n        <key>UISupportedInterfaceOrientations~ipad</key>\n        <array>\n            <string>UIInterfaceOrientationPortrait</string>\n            <string>UIInterfaceOrientationPortraitUpsideDown</string>\n            <string>UIInterfaceOrientationLandscapeLeft</string>\n            <string>UIInterfaceOrientationLandscapeRight</string>\n        </array>\n        <key>XSAppIconAssets</key>\n        <string>Assets.xcassets/appicon.appiconset</string>\n        <key>NSLocationWhenInUseUsageDescription</key>\n        <string>Can we use your location when your app is being used?</string>\n        <key>NSAppTransportSecurity</key>    \n        <dict>\n            <key>NSAllowsLocalNetworking</key>\n            <true/>\n        </dict>\n    </dict>\n</plist>"
  },
  {
    "path": "src/ClientApp/Platforms/iOS/Program.cs",
    "content": "﻿using UIKit;\n\nnamespace eShop.ClientApp;\n\npublic class Program\n{\n    // This is the main entry point of the application.\n    static void Main(string[] args)\n    {\n        // if you want to use a different Application Delegate class from \"AppDelegate\"\n        // you can specify it here.\n        UIApplication.Main(args, null, typeof(AppDelegate));\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"Windows Machine\": {\n      \"commandName\": \"MsixPackage\",\n      \"nativeDebugging\": false\n    }\n  }\n}"
  },
  {
    "path": "src/ClientApp/Resources/Raw/AboutAssets.txt",
    "content": "﻿Any raw assets you want to be deployed with your application can be placed in\nthis directory (and child directories) and given a Build Action of \"MauiAsset\":\n\n\t<MauiAsset Include=\"AboutAssets.txt\" />\n\nThese files will be deployed with you package and will be accessible using Essentials:\n\n\tasync Task LoadMauiAsset()\n\t{\n\t\tusing var stream = await FileSystem.OpenAppPackageFileAsync(\"AboutAssets.txt\");\n\t\tusing var reader = new StreamReader(stream);\n\n\t\tvar contents = reader.ReadToEnd();\n\t}\n"
  },
  {
    "path": "src/ClientApp/Resources/Styles/Colors.xaml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<?xaml-comp compile=\"true\" ?>\n\n<ResourceDictionary xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\" xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\">\n\n\n    <!--  COLORS  -->\n    <Color x:Key=\"WhiteColor\">#ffffff</Color>\n    <Color x:Key=\"BlackColor\">#000000</Color>\n    <Color x:Key=\"AccentColor\">#00857D</Color>\n    <Color x:Key=\"LightGreenColor\">#83D01B</Color>\n    <Color x:Key=\"GreenColor\">#00A69C</Color>\n    <Color x:Key=\"GrayColor\">#e2e2e2</Color>\n    <Color x:Key=\"DisabledColor\">Gray</Color>\n    <Color x:Key=\"ErrorColor\">#ff5252</Color>\n    <Color x:Key=\"TextColor\">#000</Color>\n    <Color x:Key=\"TextLightColor\">#444</Color>\n    <Color x:Key=\"InverseTextColor\">#FFFFFF</Color>\n    <Color x:Key=\"LightTextColor\">#979797</Color>\n    <Color x:Key=\"iOSDefaultTintColor\">#007aff</Color>\n    <Color x:Key=\"SeparatorLineColor\">#CCCCCC</Color>\n    <Color x:Key=\"DefaultButtonClassBackgroundColor\">#C9C9C9</Color>\n    <Color x:Key=\"DefaultButtonClassBorderColor\">Transparent</Color>\n    <Color x:Key=\"DefaultButtonClassTextColor\">#FFFFFF</Color>\n    <Color x:Key=\"EntryBackgroundColor\">Transparent</Color>\n    <Color x:Key=\"DefaultAccentColorColor\">#1FAECE</Color>\n    <Color x:Key=\"ListViewBackgroundColor\">Transparent</Color>\n    <Color x:Key=\"ThemeListViewBackgroundColor\">Transparent</Color>\n    <Color x:Key=\"ActivityIndicatorColor\">#00857D</Color>\n    <Color x:Key=\"HighlightColor\">#ed361a</Color>\n    <Color x:Key=\"ImportantColor\">#2556cb</Color>\n\n    <!--  Theme Colors  -->\n    <Color x:Key=\"LightBackgroundColor\">#FFFFFF</Color>\n    <Color x:Key=\"DarkBackgroundColor\">#222222</Color>\n    <Color x:Key=\"LightForegroundColor\">#222222</Color>\n    <Color x:Key=\"DarkForegroundColor\">#FFFFFF</Color>\n    <Color x:Key=\"LightFontColor\">#222222</Color>\n    <Color x:Key=\"DarkFontColor\">#E7E7E7</Color>\n    <Color x:Key=\"LightSeparatorColor\">#222222</Color>\n    <Color x:Key=\"DarkSeparatorColor\">#E7E7E7</Color>\n    <Color x:Key=\"LightGrayColor\">#eeeeee</Color>\n\n\n</ResourceDictionary>"
  },
  {
    "path": "src/ClientApp/Resources/Styles/Styles.xaml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<?xaml-comp compile=\"true\" ?>\n<ResourceDictionary\n    xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n    xmlns:converters=\"clr-namespace:eShop.ClientApp.Converters\"\n    xmlns:ios=\"clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls\"\n    xmlns:mct=\"http://schemas.microsoft.com/dotnet/2022/maui/toolkit\">\n\n    <x:Double x:Key=\"BaseButtonBorderRadius\">6</x:Double>\n    <x:Double x:Key=\"BaseButtonBorderWidth\">0</x:Double>\n\n    <!--  FONTS  -->\n    <x:String x:Key=\"SettingsIcon\">&#xf013;</x:String>\n    <x:String x:Key=\"CatalogIcon\">&#xf03a;</x:String>\n    <x:String x:Key=\"MapIcon\">&#xf279;</x:String>\n    <x:String x:Key=\"ProfileIcon\">&#xf2bd;</x:String>\n    <x:String x:Key=\"BasketIcon\">&#xf07a;</x:String>\n    <x:String x:Key=\"CampaignIcon\">&#xf155;</x:String>\n    <x:String x:Key=\"FilterIcon\">&#xf0b0;</x:String>\n    <x:String x:Key=\"AddIcon\">&#xf067;</x:String>\n    <x:String x:Key=\"CircleIcon\">&#xf111;</x:String>\n    <x:String x:Key=\"ToggleOffIcon\">&#xf204;</x:String>\n    <x:String x:Key=\"ToggleOnIcon\">&#xf205;</x:String>\n\n    <FontImageSource\n        x:Key=\"SettingsIconImageSource\"\n        FontFamily=\"FontAwesome-Solid\"\n        Glyph=\"{StaticResource SettingsIcon}\" />\n    <FontImageSource\n        x:Key=\"SettingsIconLightImageSource\"\n        FontFamily=\"FontAwesome-Solid\"\n        Glyph=\"{StaticResource SettingsIcon}\"\n        Color=\"{StaticResource WhiteColor}\" />\n\n    <FontImageSource\n        x:Key=\"SettingsIconDarkImageSource\"\n        FontFamily=\"FontAwesome-Solid\"\n        Glyph=\"{StaticResource SettingsIcon}\"\n        Color=\"{StaticResource BlackColor}\" />\n\n    <FontImageSource\n        x:Key=\"CatalogIconImageSource\"\n        FontAutoScalingEnabled=\"True\"\n        FontFamily=\"FontAwesome-Solid\"\n        Glyph=\"{StaticResource CatalogIcon}\"\n        Size=\"22\"\n        Color=\"{AppThemeBinding Dark={StaticResource DarkFontColor},\n                                Light={StaticResource LightFontColor}}\" />\n    <FontImageSource\n        x:Key=\"MapIconImageSource\"\n        FontAutoScalingEnabled=\"True\"\n        FontFamily=\"FontAwesome-Solid\"\n        Glyph=\"{StaticResource MapIcon}\"\n        Size=\"22\"\n        Color=\"{AppThemeBinding Dark={StaticResource DarkFontColor},\n                                Light={StaticResource LightFontColor}}\" />\n    <FontImageSource\n        x:Key=\"ProfileIconImageSource\"\n        FontAutoScalingEnabled=\"True\"\n        FontFamily=\"FontAwesome-Solid\"\n        Glyph=\"{StaticResource ProfileIcon}\"\n        Size=\"22\"\n        Color=\"{AppThemeBinding Dark={StaticResource DarkFontColor},\n                                Light={StaticResource LightFontColor}}\" />\n    <FontImageSource\n        x:Key=\"BasketIconImageSource\"\n        FontFamily=\"FontAwesome-Solid\"\n        Glyph=\"{StaticResource BasketIcon}\"\n        Size=\"22\"\n        Color=\"{AppThemeBinding Dark={StaticResource DarkFontColor},\n                                Light={StaticResource LightFontColor}}\" />\n\n    <FontImageSource\n        x:Key=\"BasketIconForTitleImageSource\"\n        FontFamily=\"FontAwesome-Solid\"\n        Glyph=\"{StaticResource BasketIcon}\"\n        Size=\"24\"\n        Color=\"{AppThemeBinding Dark={StaticResource DarkFontColor},\n                                Light={StaticResource LightFontColor}}\" />\n    <FontImageSource\n        x:Key=\"CampaignIconImageSource\"\n        FontFamily=\"FontAwesome-Solid\"\n        Glyph=\"{StaticResource CampaignIcon}\" />\n    <FontImageSource\n        x:Key=\"FilterIconImageSource\"\n        FontFamily=\"FontAwesome-Solid\"\n        Glyph=\"{StaticResource FilterIcon}\" />\n    <FontImageSource\n        x:Key=\"FilterHighlightIconImageSource\"\n        FontFamily=\"FontAwesome-Solid\"\n        Glyph=\"{StaticResource FilterIcon}\"\n        Color=\"{StaticResource AccentColor}\" />\n    <FontImageSource\n        x:Key=\"FilterIconForTitleImageSource\"\n        FontFamily=\"FontAwesome-Solid\"\n        Glyph=\"{StaticResource FilterIcon}\"\n        Size=\"22\"\n        Color=\"{AppThemeBinding Dark={StaticResource DarkFontColor},\n                                Light={StaticResource LightFontColor}}\" />\n    <FontImageSource\n        x:Key=\"SettingsIconForTitleImageSource\"\n        FontFamily=\"FontAwesome-Solid\"\n        Glyph=\"{StaticResource SettingsIcon}\"\n        Size=\"22\"\n        Color=\"{AppThemeBinding Dark={StaticResource DarkFontColor},\n                                Light={StaticResource LightFontColor}}\" />\n    <FontImageSource\n        x:Key=\"AddIconImageSource\"\n        FontFamily=\"FontAwesome-Solid\"\n        Glyph=\"{StaticResource AddIcon}\" />\n    <FontImageSource\n        x:Key=\"AddIconLightImageSource\"\n        FontFamily=\"FontAwesome-Solid\"\n        Glyph=\"{StaticResource AddIcon}\"\n        Color=\"{StaticResource WhiteColor}\" />\n    <FontImageSource\n        x:Key=\"CircleIconImageSource\"\n        FontFamily=\"FontAwesome-Solid\"\n        Glyph=\"{StaticResource CircleIcon}\" />\n    <FontImageSource\n        x:Key=\"ToggleOffImageSource\"\n        FontFamily=\"FontAwesome-Solid\"\n        Glyph=\"{StaticResource ToggleOffIcon}\"\n        Color=\"{StaticResource DisabledColor}\" />\n    <FontImageSource\n        x:Key=\"ToggleOnImageSource\"\n        FontFamily=\"FontAwesome-Solid\"\n        Glyph=\"{StaticResource ToggleOnIcon}\"\n        Color=\"{StaticResource AccentColor}\" />\n\n    <OnPlatform x:Key=\"BaseButtonFontSize\" x:TypeArguments=\"x:Double\">\n        <On Platform=\"iOS\" Value=\"18\" />\n        <On Platform=\"Default\" Value=\"16\" />\n    </OnPlatform>\n\n    <OnPlatform x:Key=\"BaseFontSize\" x:TypeArguments=\"x:Double\">\n        <On Platform=\"iOS\" Value=\"16\" />\n        <On Platform=\"Default\" Value=\"15\" />\n    </OnPlatform>\n\n    <OnPlatform x:Key=\"LittleSize\" x:TypeArguments=\"x:Double\">\n        <On Platform=\"iOS\" Value=\"11\" />\n        <On Platform=\"Default\" Value=\"12\" />\n    </OnPlatform>\n\n    <OnPlatform x:Key=\"MidMediumSize\" x:TypeArguments=\"x:Double\">\n        <On Platform=\"iOS\" Value=\"12\" />\n        <On Platform=\"Default\" Value=\"14\" />\n    </OnPlatform>\n\n    <OnPlatform x:Key=\"MediumSize\" x:TypeArguments=\"x:Double\">\n        <On Platform=\"iOS\" Value=\"14\" />\n        <On Platform=\"Default\" Value=\"16\" />\n    </OnPlatform>\n\n    <OnPlatform x:Key=\"LargeSize\" x:TypeArguments=\"x:Double\">\n        <On Platform=\"iOS\" Value=\"16\" />\n        <On Platform=\"Default\" Value=\"18\" />\n    </OnPlatform>\n\n    <OnPlatform x:Key=\"LargerSize\" x:TypeArguments=\"x:Double\">\n        <On Platform=\"iOS\" Value=\"18\" />\n        <On Platform=\"Default\" Value=\"20\" />\n    </OnPlatform>\n\n    <OnPlatform x:Key=\"BigSize\" x:TypeArguments=\"x:Double\">\n        <On Platform=\"iOS\" Value=\"20\" />\n        <On Platform=\"Default\" Value=\"24\" />\n    </OnPlatform>\n\n    <OnPlatform x:Key=\"ExtraBigSize\" x:TypeArguments=\"x:Double\">\n        <On Platform=\"iOS\" Value=\"24\" />\n        <On Platform=\"Default\" Value=\"32\" />\n    </OnPlatform>\n\n    <OnPlatform x:Key=\"HugeSize\" x:TypeArguments=\"x:Double\">\n        <On Platform=\"iOS\" Value=\"32\" />\n        <On Platform=\"Default\" Value=\"48\" />\n    </OnPlatform>\n\n    <OnPlatform x:Key=\"BaseButtonFontAttributes\" x:TypeArguments=\"FontAttributes\">\n        <On Platform=\"iOS\" Value=\"Bold\" />\n        <On Platform=\"Default\" Value=\"None\" />\n    </OnPlatform>\n\n    <!--  CONVERTERS  -->\n    <mct:IntToBoolConverter x:Key=\"CountToBoolConverter\" />\n    <mct:InvertedBoolConverter x:Key=\"InverseBoolConverter\" />\n    <mct:TextCaseConverter x:Key=\"ToUpperConverter\" Type=\"Upper\" />\n    <mct:IsStringNullOrEmptyConverter x:Key=\"StringNullOrEmptyBoolConverter\" />\n    <mct:ItemTappedEventArgsConverter x:Key=\"ItemTappedEventArgsConverter\" />\n    <mct:IsListNullOrEmptyConverter x:Key=\"ListIsNullOrEmptyConverter\" />\n    <mct:IsListNotNullOrEmptyConverter x:Key=\"ListIsNotNullOrEmptyConverter\" />\n    <converters:DoesNotHaveCountConverter x:Key=\"DoesNotHaveCountConverter\" />\n    <converters:HasCountConverter x:Key=\"HasCountConverter\" />\n    <converters:ItemsToHeightConverter x:Key=\"ItemsToHeightConverter\" />\n    <x:String x:Key=\"DateTimeFormat\">{0:MMMM dd yyyy}</x:String>\n\n    <!--  EXPLICIT STYLES  -->\n    <Style x:Key=\"ValidationErrorLabelStyle\" TargetType=\"{x:Type Label}\">\n        <Setter Property=\"TextColor\" Value=\"{StaticResource ErrorColor}\" />\n        <Setter Property=\"FontSize\" Value=\"{StaticResource LittleSize}\" />\n    </Style>\n\n    <Style x:Key=\"EntryStyle\" TargetType=\"{x:Type Entry}\">\n        <Setter Property=\"FontFamily\" Value=\"PlusJakartaSans-Regular\" />\n        <Setter Property=\"FontSize\" Value=\"{StaticResource LargeSize}\" />\n        <Setter Property=\"HorizontalOptions\" Value=\"FillAndExpand\" />\n        <Setter Property=\"FontAttributes\" Value=\"Bold\" />\n        <Setter Property=\"Opacity\" Value=\"0.6\" />\n        <Style.Triggers>\n            <Trigger TargetType=\"Entry\" Property=\"IsFocused\" Value=\"True\">\n                <Setter Property=\"Opacity\" Value=\"1\" />\n            </Trigger>\n        </Style.Triggers>\n    </Style>\n\n    <Style x:Key=\"LargeSizeFontStyle\" TargetType=\"{x:Type Label}\">\n        <Setter Property=\"FontFamily\" Value=\"PlusJakartaSans-Regular\" />\n        <Setter Property=\"FontSize\" Value=\"{StaticResource LargerSize}\" />\n    </Style>\n\n    <Style x:Key=\"MediumSizeFontStyle\" TargetType=\"{x:Type Label}\">\n        <Setter Property=\"FontFamily\" Value=\"PlusJakartaSans-Regular\" />\n        <Setter Property=\"FontSize\" Value=\"{StaticResource MediumSize}\" />\n    </Style>\n\n    <Style x:Key=\"LittleSizeFontStyle\" TargetType=\"{x:Type Label}\">\n        <Setter Property=\"FontFamily\" Value=\"PlusJakartaSans-Regular\" />\n        <Setter Property=\"FontSize\" Value=\"{StaticResource LittleSize}\" />\n    </Style>\n\n    <Style x:Key=\"WinUIEntryStyle\" TargetType=\"{x:Type Entry}\">\n        <Setter Property=\"FontFamily\" Value=\"PlusJakartaSans-Regular\" />\n        <Setter Property=\"TextColor\" Value=\"{AppThemeBinding Light={StaticResource BlackColor}, Dark=White}\" />\n        <Setter Property=\"PlaceholderColor\" Value=\"{AppThemeBinding Light={StaticResource BlackColor}, Dark=White}\" />\n        <Setter Property=\"FontSize\" Value=\"{StaticResource LargeSize}\" />\n        <Setter Property=\"HorizontalOptions\" Value=\"FillAndExpand\" />\n        <Setter Property=\"FontAttributes\" Value=\"Bold\" />\n        <Setter Property=\"BackgroundColor\" Value=\"Transparent\" />\n        <Setter Property=\"Opacity\" Value=\"0.6\" />\n        <Style.Triggers>\n            <Trigger TargetType=\"Entry\" Property=\"IsFocused\" Value=\"True\">\n                <Setter Property=\"Opacity\" Value=\"1\" />\n            </Trigger>\n        </Style.Triggers>\n    </Style>\n\n    <!--  IMPLICIT STYLES  -->\n    <Style\n        ApplyToDerivedTypes=\"True\"\n        CanCascade=\"True\"\n        TargetType=\"Button\">\n        <Setter Property=\"FontSize\" Value=\"{StaticResource BaseButtonFontSize}\" />\n        <Setter Property=\"FontAttributes\" Value=\"{StaticResource BaseButtonFontAttributes}\" />\n        <Setter Property=\"CornerRadius\" Value=\"{StaticResource BaseButtonBorderRadius}\" />\n        <Setter Property=\"BorderWidth\" Value=\"{StaticResource BaseButtonBorderWidth}\" />\n        <Setter Property=\"BackgroundColor\" Value=\"{StaticResource DefaultButtonClassBackgroundColor}\" />\n        <Setter Property=\"BorderColor\" Value=\"{StaticResource DefaultButtonClassBorderColor}\" />\n        <Setter Property=\"TextColor\" Value=\"{StaticResource DefaultButtonClassTextColor}\" />\n    </Style>\n\n    <Style\n        x:Key=\"TransparentButtonStyle\"\n        CanCascade=\"True\"\n        TargetType=\"Button\">\n        <Setter Property=\"BackgroundColor\" Value=\"Transparent\" />\n        <Setter Property=\"TextColor\" Value=\"{AppThemeBinding Dark={StaticResource DarkFontColor}, Light={StaticResource LightFontColor}}\" />\n    </Style>\n\n    <Style\n        ApplyToDerivedTypes=\"True\"\n        CanCascade=\"True\"\n        TargetType=\"Label\">\n        <Setter Property=\"FontSize\" Value=\"{StaticResource BaseFontSize}\" />\n        <Setter Property=\"TextColor\" Value=\"{AppThemeBinding Dark={StaticResource DarkFontColor}, Light={StaticResource LightFontColor}}\" />\n    </Style>\n\n    <Style\n        ApplyToDerivedTypes=\"True\"\n        CanCascade=\"True\"\n        TargetType=\"Entry\">\n        <Setter Property=\"TextColor\" Value=\"{AppThemeBinding Dark={StaticResource DarkFontColor}, Light={StaticResource LightFontColor}}\" />\n        <Setter Property=\"PlaceholderColor\" Value=\"{AppThemeBinding Dark={StaticResource DarkFontColor}, Light={StaticResource LightFontColor}}\" />\n        <Setter Property=\"BackgroundColor\" Value=\"{AppThemeBinding Dark={StaticResource DarkBackgroundColor}, Light={StaticResource LightBackgroundColor}}\" />\n    </Style>\n\n    <Style\n        ApplyToDerivedTypes=\"True\"\n        CanCascade=\"True\"\n        TargetType=\"ImageCell\">\n        <Setter Property=\"TextColor\" Value=\"{StaticResource InverseTextColor}\" />\n        <Setter Property=\"DetailColor\" Value=\"{StaticResource AccentColor}\" />\n    </Style>\n\n    <Style\n        ApplyToDerivedTypes=\"True\"\n        CanCascade=\"True\"\n        TargetType=\"TextCell\">\n        <Setter Property=\"TextColor\" Value=\"{StaticResource InverseTextColor}\" />\n        <Setter Property=\"DetailColor\" Value=\"{StaticResource AccentColor}\" />\n    </Style>\n\n    <Style\n        ApplyToDerivedTypes=\"True\"\n        CanCascade=\"True\"\n        TargetType=\"CollectionView\">\n        <Setter Property=\"BackgroundColor\" Value=\"{StaticResource ThemeListViewBackgroundColor}\" />\n    </Style>\n\n    <Style\n        ApplyToDerivedTypes=\"True\"\n        CanCascade=\"True\"\n        TargetType=\"ActivityIndicator\">\n        <Setter Property=\"Color\" Value=\"{StaticResource ActivityIndicatorColor}\" />\n    </Style>\n\n    <Style ApplyToDerivedTypes=\"True\" TargetType=\"TabBar\">\n        <Setter Property=\"Shell.TabBarTitleColor\" Value=\"{StaticResource HighlightColor}\" />\n        <Setter Property=\"Shell.TabBarForegroundColor\">\n            <OnPlatform x:TypeArguments=\"Color\">\n                <On Platform=\"WinUI\" Value=\"{StaticResource HighlightColor}\" />\n            </OnPlatform>\n        </Setter>\n        <Setter Property=\"Shell.TabBarUnselectedColor\">\n            <OnPlatform x:TypeArguments=\"Color\">\n                <On Platform=\"WinUI\" Value=\"Gray\" />\n            </OnPlatform>\n        </Setter>\n    </Style>\n\n\n    <Style ApplyToDerivedTypes=\"True\" TargetType=\"ContentPage\">\n        <Setter Property=\"NavigationPage.BackButtonTitle\" Value=\"\" />\n        <Setter Property=\"ios:Page.UseSafeArea\" Value=\"True\" />\n        <Setter Property=\"BackgroundColor\" Value=\"{AppThemeBinding Light={StaticResource LightBackgroundColor}, Dark={StaticResource DarkBackgroundColor}}\" />\n        <Setter Property=\"Shell.ForegroundColor\" Value=\"{AppThemeBinding Light={StaticResource LightForegroundColor}, Dark={StaticResource DarkForegroundColor}}\" />\n    </Style>\n\n\n</ResourceDictionary>"
  },
  {
    "path": "src/ClientApp/Services/AppEnvironment/AppEnvironmentService.cs",
    "content": "using eShop.ClientApp.Services.Basket;\nusing eShop.ClientApp.Services.Catalog;\nusing eShop.ClientApp.Services.Identity;\nusing eShop.ClientApp.Services.Order;\n\nnamespace eShop.ClientApp.Services.AppEnvironment;\n\npublic class AppEnvironmentService : IAppEnvironmentService\n{\n    private readonly IBasketService _basketService;\n    private readonly ICatalogService _catalogService;\n    private readonly IIdentityService _identityService;\n    private readonly IBasketService _mockBasketService;\n\n    private readonly ICatalogService _mockCatalogService;\n\n    private readonly IIdentityService _mockIdentityService;\n\n    private readonly IOrderService _mockOrderService;\n    private readonly IOrderService _orderService;\n\n    public AppEnvironmentService(\n        IBasketService mockBasketService, IBasketService basketService,\n        ICatalogService mockCatalogService, ICatalogService catalogService,\n        IOrderService mockOrderService, IOrderService orderService,\n        IIdentityService mockIdentityService, IIdentityService identityService)\n    {\n        _mockBasketService = mockBasketService;\n        _basketService = basketService;\n\n        _mockCatalogService = mockCatalogService;\n        _catalogService = catalogService;\n\n        _mockOrderService = mockOrderService;\n        _orderService = orderService;\n\n        _mockIdentityService = mockIdentityService;\n        _identityService = identityService;\n    }\n\n    public IBasketService BasketService { get; private set; }\n\n    public ICatalogService CatalogService { get; private set; }\n\n    public IOrderService OrderService { get; private set; }\n\n    public IIdentityService IdentityService { get; private set; }\n\n    public void UpdateDependencies(bool useMockServices)\n    {\n        if (useMockServices)\n        {\n            BasketService = _mockBasketService;\n            CatalogService = _mockCatalogService;\n            OrderService = _mockOrderService;\n            IdentityService = _mockIdentityService;\n        }\n        else\n        {\n            BasketService = _basketService;\n            CatalogService = _catalogService;\n            OrderService = _orderService;\n            IdentityService = _identityService;\n        }\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Services/AppEnvironment/IAppEnvironmentService.cs",
    "content": "using eShop.ClientApp.Services.Basket;\nusing eShop.ClientApp.Services.Catalog;\nusing eShop.ClientApp.Services.Identity;\nusing eShop.ClientApp.Services.Order;\n\nnamespace eShop.ClientApp.Services.AppEnvironment;\n\npublic interface IAppEnvironmentService\n{\n    IBasketService BasketService { get; }\n\n    ICatalogService CatalogService { get; }\n\n    IOrderService OrderService { get; }\n\n    IIdentityService IdentityService { get; }\n\n    void UpdateDependencies(bool useMockServices);\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Basket/BasketMockService.cs",
    "content": "using eShop.ClientApp.Models.Basket;\n\nnamespace eShop.ClientApp.Services.Basket;\n\npublic class BasketMockService : IBasketService\n{\n    private CustomerBasket _mockCustomBasket;\n\n    public BasketMockService()\n    {\n        _mockCustomBasket = new CustomerBasket {BuyerId = \"9245fe4a-d402-451c-b9ed-9c1a04247482\"};\n        _mockCustomBasket.AddItemToBasket(new BasketItem\n        {\n            Id = \"1\",\n            PictureUrl = \"fake_product_01.png\",\n            ProductId = Common.Common.MockCatalogItemId01,\n            ProductName = \".NET Bot Blue Sweatshirt (M)\",\n            Quantity = 1,\n            UnitPrice = 19.50M\n        });\n\n        _mockCustomBasket.AddItemToBasket(new BasketItem\n        {\n            Id = \"2\",\n            PictureUrl = \"fake_product_04.png\",\n            ProductId = Common.Common.MockCatalogItemId04,\n            ProductName = \".NET Black Cup\",\n            Quantity = 1,\n            UnitPrice = 17.00M\n        });\n    }\n\n    public IEnumerable<BasketItem> LocalBasketItems { get; set; }\n\n    public async Task<CustomerBasket> GetBasketAsync()\n    {\n        await Task.Delay(10);\n\n        return _mockCustomBasket;\n    }\n\n    public async Task<CustomerBasket> UpdateBasketAsync(CustomerBasket customerBasket)\n    {\n        await Task.Delay(10);\n\n        _mockCustomBasket = customerBasket;\n\n        return _mockCustomBasket;\n    }\n\n    public async Task ClearBasketAsync()\n    {\n        await Task.Delay(10);\n\n        _mockCustomBasket.ClearBasket();\n\n        LocalBasketItems = null;\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Basket/BasketService.cs",
    "content": "using eShop.ClientApp.BasketGrpcClient;\nusing eShop.ClientApp.Models.Basket;\nusing eShop.ClientApp.Services.FixUri;\nusing eShop.ClientApp.Services.Identity;\nusing eShop.ClientApp.Services.Settings;\nusing Google.Protobuf;\nusing Grpc.Core;\nusing Grpc.Net.Client;\nusing BasketItem = eShop.ClientApp.Models.Basket.BasketItem;\n\nnamespace eShop.ClientApp.Services.Basket;\n\npublic class BasketService : IBasketService, IDisposable\n{\n    private readonly IFixUriService _fixUriService;\n    private readonly IIdentityService _identityService;\n    private readonly ISettingsService _settingsService;\n    private BasketGrpcClient.Basket.BasketClient _basketClient;\n\n    private GrpcChannel _channel;\n\n    public BasketService(IIdentityService identityService, ISettingsService settingsService,\n        IFixUriService fixUriService)\n    {\n        _identityService = identityService;\n        _settingsService = settingsService;\n        _fixUriService = fixUriService;\n    }\n\n    public IEnumerable<BasketItem> LocalBasketItems { get; set; }\n\n    public async Task<CustomerBasket> GetBasketAsync()\n    {\n        CustomerBasket basket = new();\n\n        var authToken = await _identityService.GetAuthTokenAsync().ConfigureAwait(false);\n\n        if (string.IsNullOrEmpty(authToken))\n        {\n            return basket;\n        }\n\n        try\n        {\n            var basketResponse = await GetBasketClient()\n                .GetBasketAsync(new GetBasketRequest(), CreateAuthenticationHeaders(authToken));\n            \n            if (basketResponse.IsInitialized() && basketResponse.Items.Any())\n            {\n                foreach (var item in basketResponse.Items)\n                {\n                    basket.AddItemToBasket(new BasketItem {ProductId = item.ProductId, Quantity = item.Quantity});\n                }\n            }\n        }\n        catch (Exception exception)\n        {\n            Console.WriteLine(exception);\n            basket = null;\n        }\n\n        _fixUriService.FixBasketItemPictureUri(basket?.Items);\n        return basket;\n    }\n\n    public async Task<CustomerBasket> UpdateBasketAsync(CustomerBasket customerBasket)\n    {\n        var authToken = await _identityService.GetAuthTokenAsync().ConfigureAwait(false);\n\n        if (string.IsNullOrEmpty(authToken))\n        {\n            return customerBasket;\n        }\n\n        var updateBasketRequest = new UpdateBasketRequest();\n\n        updateBasketRequest.Items.Add(\n            customerBasket.Items\n                .Select(\n                    x =>\n                        new BasketGrpcClient.BasketItem {ProductId = x.ProductId, Quantity = x.Quantity}));\n\n        var result = await GetBasketClient()\n            .UpdateBasketAsync(updateBasketRequest, CreateAuthenticationHeaders(authToken)).ConfigureAwait(false);\n\n        if (result.Items.Count > 0)\n        {\n            customerBasket.ClearBasket();\n        }\n\n        foreach (var item in result.Items)\n        {\n            customerBasket.AddItemToBasket(new BasketItem {ProductId = item.ProductId, Quantity = item.Quantity});\n        }\n\n        return customerBasket;\n    }\n\n    public async Task ClearBasketAsync()\n    {\n        var authToken = await _identityService.GetAuthTokenAsync().ConfigureAwait(false);\n\n        if (string.IsNullOrEmpty(authToken))\n        {\n            return;\n        }\n\n        await GetBasketClient().DeleteBasketAsync(new DeleteBasketRequest(), CreateAuthenticationHeaders(authToken))\n            .ConfigureAwait(false);\n    }\n\n    public void Dispose()\n    {\n        Dispose(true);\n        GC.SuppressFinalize(this);\n    }\n\n    private BasketGrpcClient.Basket.BasketClient GetBasketClient()\n    {\n        if (_basketClient is not null)\n        {\n            return _basketClient;\n        }\n\n        _channel = GrpcChannel.ForAddress(_settingsService.GatewayBasketEndpointBase);\n\n        _basketClient = new BasketGrpcClient.Basket.BasketClient(_channel);\n\n        return _basketClient;\n    }\n\n    private Metadata CreateAuthenticationHeaders(string token)\n    {\n        var headers = new Metadata();\n        headers.Add(\"authorization\", $\"Bearer {token}\");\n        return headers;\n    }\n\n    protected virtual void Dispose(bool disposing)\n    {\n        if (disposing)\n        {\n            _channel?.Dispose();\n        }\n    }\n\n    ~BasketService()\n    {\n        Dispose(false);\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Basket/IBasketService.cs",
    "content": "﻿using eShop.ClientApp.Models.Basket;\n\nnamespace eShop.ClientApp.Services.Basket;\n\npublic interface IBasketService\n{\n    IEnumerable<BasketItem> LocalBasketItems { get; set; }\n    Task<CustomerBasket> GetBasketAsync();\n    Task<CustomerBasket> UpdateBasketAsync(CustomerBasket customerBasket);\n    Task ClearBasketAsync();\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Basket/Protos/Basket.cs",
    "content": "// <auto-generated>\n//     Generated by the protocol buffer compiler.  DO NOT EDIT!\n//     source: Services/Basket/Protos/basket.proto\n// </auto-generated>\n#pragma warning disable 1591, 0612, 3021, 8981\n#region Designer generated code\n\nusing pb = global::Google.Protobuf;\nusing pbc = global::Google.Protobuf.Collections;\nusing pbr = global::Google.Protobuf.Reflection;\nusing scg = global::System.Collections.Generic;\nnamespace eShop.ClientApp.BasketGrpcClient {\n\n  /// <summary>Holder for reflection information generated from Services/Basket/Protos/basket.proto</summary>\n  public static partial class BasketReflection {\n\n    #region Descriptor\n    /// <summary>File descriptor for Services/Basket/Protos/basket.proto</summary>\n    public static pbr::FileDescriptor Descriptor {\n      get { return descriptor; }\n    }\n    private static pbr::FileDescriptor descriptor;\n\n    static BasketReflection() {\n      byte[] descriptorData = global::System.Convert.FromBase64String(\n          string.Concat(\n            \"CiNTZXJ2aWNlcy9CYXNrZXQvUHJvdG9zL2Jhc2tldC5wcm90bxIJQmFza2V0\",\n            \"QXBpIhIKEEdldEJhc2tldFJlcXVlc3QiPgoWQ3VzdG9tZXJCYXNrZXRSZXNw\",\n            \"b25zZRIkCgVpdGVtcxgBIAMoCzIVLkJhc2tldEFwaS5CYXNrZXRJdGVtIjIK\",\n            \"CkJhc2tldEl0ZW0SEgoKcHJvZHVjdF9pZBgCIAEoBRIQCghxdWFudGl0eRgG\",\n            \"IAEoBSI7ChNVcGRhdGVCYXNrZXRSZXF1ZXN0EiQKBWl0ZW1zGAIgAygLMhUu\",\n            \"QmFza2V0QXBpLkJhc2tldEl0ZW0iFQoTRGVsZXRlQmFza2V0UmVxdWVzdCIW\",\n            \"ChREZWxldGVCYXNrZXRSZXNwb25zZTL/AQoGQmFza2V0Ek0KCUdldEJhc2tl\",\n            \"dBIbLkJhc2tldEFwaS5HZXRCYXNrZXRSZXF1ZXN0GiEuQmFza2V0QXBpLkN1\",\n            \"c3RvbWVyQmFza2V0UmVzcG9uc2UiABJTCgxVcGRhdGVCYXNrZXQSHi5CYXNr\",\n            \"ZXRBcGkuVXBkYXRlQmFza2V0UmVxdWVzdBohLkJhc2tldEFwaS5DdXN0b21l\",\n            \"ckJhc2tldFJlc3BvbnNlIgASUQoMRGVsZXRlQmFza2V0Eh4uQmFza2V0QXBp\",\n            \"LkRlbGV0ZUJhc2tldFJlcXVlc3QaHy5CYXNrZXRBcGkuRGVsZXRlQmFza2V0\",\n            \"UmVzcG9uc2UiAEIjqgIgZVNob3AuQ2xpZW50QXBwLkJhc2tldEdycGNDbGll\",\n            \"bnRiBnByb3RvMw==\"));\n      descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,\n          new pbr::FileDescriptor[] { },\n          new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] {\n            new pbr::GeneratedClrTypeInfo(typeof(global::eShop.ClientApp.BasketGrpcClient.GetBasketRequest), global::eShop.ClientApp.BasketGrpcClient.GetBasketRequest.Parser, null, null, null, null, null),\n            new pbr::GeneratedClrTypeInfo(typeof(global::eShop.ClientApp.BasketGrpcClient.CustomerBasketResponse), global::eShop.ClientApp.BasketGrpcClient.CustomerBasketResponse.Parser, new[]{ \"Items\" }, null, null, null, null),\n            new pbr::GeneratedClrTypeInfo(typeof(global::eShop.ClientApp.BasketGrpcClient.BasketItem), global::eShop.ClientApp.BasketGrpcClient.BasketItem.Parser, new[]{ \"ProductId\", \"Quantity\" }, null, null, null, null),\n            new pbr::GeneratedClrTypeInfo(typeof(global::eShop.ClientApp.BasketGrpcClient.UpdateBasketRequest), global::eShop.ClientApp.BasketGrpcClient.UpdateBasketRequest.Parser, new[]{ \"Items\" }, null, null, null, null),\n            new pbr::GeneratedClrTypeInfo(typeof(global::eShop.ClientApp.BasketGrpcClient.DeleteBasketRequest), global::eShop.ClientApp.BasketGrpcClient.DeleteBasketRequest.Parser, null, null, null, null, null),\n            new pbr::GeneratedClrTypeInfo(typeof(global::eShop.ClientApp.BasketGrpcClient.DeleteBasketResponse), global::eShop.ClientApp.BasketGrpcClient.DeleteBasketResponse.Parser, null, null, null, null, null)\n          }));\n    }\n    #endregion\n\n  }\n  #region Messages\n  [global::System.Diagnostics.DebuggerDisplayAttribute(\"{ToString(),nq}\")]\n  public sealed partial class GetBasketRequest : pb::IMessage<GetBasketRequest>\n  #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n      , pb::IBufferMessage\n  #endif\n  {\n    private static readonly pb::MessageParser<GetBasketRequest> _parser = new pb::MessageParser<GetBasketRequest>(() => new GetBasketRequest());\n    private pb::UnknownFieldSet _unknownFields;\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public static pb::MessageParser<GetBasketRequest> Parser { get { return _parser; } }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public static pbr::MessageDescriptor Descriptor {\n      get { return global::eShop.ClientApp.BasketGrpcClient.BasketReflection.Descriptor.MessageTypes[0]; }\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    pbr::MessageDescriptor pb::IMessage.Descriptor {\n      get { return Descriptor; }\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public GetBasketRequest() {\n      OnConstruction();\n    }\n\n    partial void OnConstruction();\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public GetBasketRequest(GetBasketRequest other) : this() {\n      _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public GetBasketRequest Clone() {\n      return new GetBasketRequest(this);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public override bool Equals(object other) {\n      return Equals(other as GetBasketRequest);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public bool Equals(GetBasketRequest other) {\n      if (ReferenceEquals(other, null)) {\n        return false;\n      }\n      if (ReferenceEquals(other, this)) {\n        return true;\n      }\n      return Equals(_unknownFields, other._unknownFields);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public override int GetHashCode() {\n      int hash = 1;\n      if (_unknownFields != null) {\n        hash ^= _unknownFields.GetHashCode();\n      }\n      return hash;\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public override string ToString() {\n      return pb::JsonFormatter.ToDiagnosticString(this);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public void WriteTo(pb::CodedOutputStream output) {\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n      output.WriteRawMessage(this);\n    #else\n      if (_unknownFields != null) {\n        _unknownFields.WriteTo(output);\n      }\n    #endif\n    }\n\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {\n      if (_unknownFields != null) {\n        _unknownFields.WriteTo(ref output);\n      }\n    }\n    #endif\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public int CalculateSize() {\n      int size = 0;\n      if (_unknownFields != null) {\n        size += _unknownFields.CalculateSize();\n      }\n      return size;\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public void MergeFrom(GetBasketRequest other) {\n      if (other == null) {\n        return;\n      }\n      _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public void MergeFrom(pb::CodedInputStream input) {\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n      input.ReadRawMessage(this);\n    #else\n      uint tag;\n      while ((tag = input.ReadTag()) != 0) {\n      if ((tag & 7) == 4) {\n        // Abort on any end group tag.\n        return;\n      }\n      switch(tag) {\n          default:\n            _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);\n            break;\n        }\n      }\n    #endif\n    }\n\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) {\n      uint tag;\n      while ((tag = input.ReadTag()) != 0) {\n      if ((tag & 7) == 4) {\n        // Abort on any end group tag.\n        return;\n      }\n      switch(tag) {\n          default:\n            _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);\n            break;\n        }\n      }\n    }\n    #endif\n\n  }\n\n  [global::System.Diagnostics.DebuggerDisplayAttribute(\"{ToString(),nq}\")]\n  public sealed partial class CustomerBasketResponse : pb::IMessage<CustomerBasketResponse>\n  #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n      , pb::IBufferMessage\n  #endif\n  {\n    private static readonly pb::MessageParser<CustomerBasketResponse> _parser = new pb::MessageParser<CustomerBasketResponse>(() => new CustomerBasketResponse());\n    private pb::UnknownFieldSet _unknownFields;\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public static pb::MessageParser<CustomerBasketResponse> Parser { get { return _parser; } }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public static pbr::MessageDescriptor Descriptor {\n      get { return global::eShop.ClientApp.BasketGrpcClient.BasketReflection.Descriptor.MessageTypes[1]; }\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    pbr::MessageDescriptor pb::IMessage.Descriptor {\n      get { return Descriptor; }\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public CustomerBasketResponse() {\n      OnConstruction();\n    }\n\n    partial void OnConstruction();\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public CustomerBasketResponse(CustomerBasketResponse other) : this() {\n      items_ = other.items_.Clone();\n      _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public CustomerBasketResponse Clone() {\n      return new CustomerBasketResponse(this);\n    }\n\n    /// <summary>Field number for the \"items\" field.</summary>\n    public const int ItemsFieldNumber = 1;\n    private static readonly pb::FieldCodec<global::eShop.ClientApp.BasketGrpcClient.BasketItem> _repeated_items_codec\n        = pb::FieldCodec.ForMessage(10, global::eShop.ClientApp.BasketGrpcClient.BasketItem.Parser);\n    private readonly pbc::RepeatedField<global::eShop.ClientApp.BasketGrpcClient.BasketItem> items_ = new pbc::RepeatedField<global::eShop.ClientApp.BasketGrpcClient.BasketItem>();\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public pbc::RepeatedField<global::eShop.ClientApp.BasketGrpcClient.BasketItem> Items {\n      get { return items_; }\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public override bool Equals(object other) {\n      return Equals(other as CustomerBasketResponse);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public bool Equals(CustomerBasketResponse other) {\n      if (ReferenceEquals(other, null)) {\n        return false;\n      }\n      if (ReferenceEquals(other, this)) {\n        return true;\n      }\n      if(!items_.Equals(other.items_)) return false;\n      return Equals(_unknownFields, other._unknownFields);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public override int GetHashCode() {\n      int hash = 1;\n      hash ^= items_.GetHashCode();\n      if (_unknownFields != null) {\n        hash ^= _unknownFields.GetHashCode();\n      }\n      return hash;\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public override string ToString() {\n      return pb::JsonFormatter.ToDiagnosticString(this);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public void WriteTo(pb::CodedOutputStream output) {\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n      output.WriteRawMessage(this);\n    #else\n      items_.WriteTo(output, _repeated_items_codec);\n      if (_unknownFields != null) {\n        _unknownFields.WriteTo(output);\n      }\n    #endif\n    }\n\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {\n      items_.WriteTo(ref output, _repeated_items_codec);\n      if (_unknownFields != null) {\n        _unknownFields.WriteTo(ref output);\n      }\n    }\n    #endif\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public int CalculateSize() {\n      int size = 0;\n      size += items_.CalculateSize(_repeated_items_codec);\n      if (_unknownFields != null) {\n        size += _unknownFields.CalculateSize();\n      }\n      return size;\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public void MergeFrom(CustomerBasketResponse other) {\n      if (other == null) {\n        return;\n      }\n      items_.Add(other.items_);\n      _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public void MergeFrom(pb::CodedInputStream input) {\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n      input.ReadRawMessage(this);\n    #else\n      uint tag;\n      while ((tag = input.ReadTag()) != 0) {\n      if ((tag & 7) == 4) {\n        // Abort on any end group tag.\n        return;\n      }\n      switch(tag) {\n          default:\n            _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);\n            break;\n          case 10: {\n            items_.AddEntriesFrom(input, _repeated_items_codec);\n            break;\n          }\n        }\n      }\n    #endif\n    }\n\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) {\n      uint tag;\n      while ((tag = input.ReadTag()) != 0) {\n      if ((tag & 7) == 4) {\n        // Abort on any end group tag.\n        return;\n      }\n      switch(tag) {\n          default:\n            _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);\n            break;\n          case 10: {\n            items_.AddEntriesFrom(ref input, _repeated_items_codec);\n            break;\n          }\n        }\n      }\n    }\n    #endif\n\n  }\n\n  [global::System.Diagnostics.DebuggerDisplayAttribute(\"{ToString(),nq}\")]\n  public sealed partial class BasketItem : pb::IMessage<BasketItem>\n  #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n      , pb::IBufferMessage\n  #endif\n  {\n    private static readonly pb::MessageParser<BasketItem> _parser = new pb::MessageParser<BasketItem>(() => new BasketItem());\n    private pb::UnknownFieldSet _unknownFields;\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public static pb::MessageParser<BasketItem> Parser { get { return _parser; } }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public static pbr::MessageDescriptor Descriptor {\n      get { return global::eShop.ClientApp.BasketGrpcClient.BasketReflection.Descriptor.MessageTypes[2]; }\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    pbr::MessageDescriptor pb::IMessage.Descriptor {\n      get { return Descriptor; }\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public BasketItem() {\n      OnConstruction();\n    }\n\n    partial void OnConstruction();\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public BasketItem(BasketItem other) : this() {\n      productId_ = other.productId_;\n      quantity_ = other.quantity_;\n      _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public BasketItem Clone() {\n      return new BasketItem(this);\n    }\n\n    /// <summary>Field number for the \"product_id\" field.</summary>\n    public const int ProductIdFieldNumber = 2;\n    private int productId_;\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public int ProductId {\n      get { return productId_; }\n      set {\n        productId_ = value;\n      }\n    }\n\n    /// <summary>Field number for the \"quantity\" field.</summary>\n    public const int QuantityFieldNumber = 6;\n    private int quantity_;\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public int Quantity {\n      get { return quantity_; }\n      set {\n        quantity_ = value;\n      }\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public override bool Equals(object other) {\n      return Equals(other as BasketItem);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public bool Equals(BasketItem other) {\n      if (ReferenceEquals(other, null)) {\n        return false;\n      }\n      if (ReferenceEquals(other, this)) {\n        return true;\n      }\n      if (ProductId != other.ProductId) return false;\n      if (Quantity != other.Quantity) return false;\n      return Equals(_unknownFields, other._unknownFields);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public override int GetHashCode() {\n      int hash = 1;\n      if (ProductId != 0) hash ^= ProductId.GetHashCode();\n      if (Quantity != 0) hash ^= Quantity.GetHashCode();\n      if (_unknownFields != null) {\n        hash ^= _unknownFields.GetHashCode();\n      }\n      return hash;\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public override string ToString() {\n      return pb::JsonFormatter.ToDiagnosticString(this);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public void WriteTo(pb::CodedOutputStream output) {\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n      output.WriteRawMessage(this);\n    #else\n      if (ProductId != 0) {\n        output.WriteRawTag(16);\n        output.WriteInt32(ProductId);\n      }\n      if (Quantity != 0) {\n        output.WriteRawTag(48);\n        output.WriteInt32(Quantity);\n      }\n      if (_unknownFields != null) {\n        _unknownFields.WriteTo(output);\n      }\n    #endif\n    }\n\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {\n      if (ProductId != 0) {\n        output.WriteRawTag(16);\n        output.WriteInt32(ProductId);\n      }\n      if (Quantity != 0) {\n        output.WriteRawTag(48);\n        output.WriteInt32(Quantity);\n      }\n      if (_unknownFields != null) {\n        _unknownFields.WriteTo(ref output);\n      }\n    }\n    #endif\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public int CalculateSize() {\n      int size = 0;\n      if (ProductId != 0) {\n        size += 1 + pb::CodedOutputStream.ComputeInt32Size(ProductId);\n      }\n      if (Quantity != 0) {\n        size += 1 + pb::CodedOutputStream.ComputeInt32Size(Quantity);\n      }\n      if (_unknownFields != null) {\n        size += _unknownFields.CalculateSize();\n      }\n      return size;\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public void MergeFrom(BasketItem other) {\n      if (other == null) {\n        return;\n      }\n      if (other.ProductId != 0) {\n        ProductId = other.ProductId;\n      }\n      if (other.Quantity != 0) {\n        Quantity = other.Quantity;\n      }\n      _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public void MergeFrom(pb::CodedInputStream input) {\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n      input.ReadRawMessage(this);\n    #else\n      uint tag;\n      while ((tag = input.ReadTag()) != 0) {\n      if ((tag & 7) == 4) {\n        // Abort on any end group tag.\n        return;\n      }\n      switch(tag) {\n          default:\n            _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);\n            break;\n          case 16: {\n            ProductId = input.ReadInt32();\n            break;\n          }\n          case 48: {\n            Quantity = input.ReadInt32();\n            break;\n          }\n        }\n      }\n    #endif\n    }\n\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) {\n      uint tag;\n      while ((tag = input.ReadTag()) != 0) {\n      if ((tag & 7) == 4) {\n        // Abort on any end group tag.\n        return;\n      }\n      switch(tag) {\n          default:\n            _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);\n            break;\n          case 16: {\n            ProductId = input.ReadInt32();\n            break;\n          }\n          case 48: {\n            Quantity = input.ReadInt32();\n            break;\n          }\n        }\n      }\n    }\n    #endif\n\n  }\n\n  [global::System.Diagnostics.DebuggerDisplayAttribute(\"{ToString(),nq}\")]\n  public sealed partial class UpdateBasketRequest : pb::IMessage<UpdateBasketRequest>\n  #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n      , pb::IBufferMessage\n  #endif\n  {\n    private static readonly pb::MessageParser<UpdateBasketRequest> _parser = new pb::MessageParser<UpdateBasketRequest>(() => new UpdateBasketRequest());\n    private pb::UnknownFieldSet _unknownFields;\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public static pb::MessageParser<UpdateBasketRequest> Parser { get { return _parser; } }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public static pbr::MessageDescriptor Descriptor {\n      get { return global::eShop.ClientApp.BasketGrpcClient.BasketReflection.Descriptor.MessageTypes[3]; }\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    pbr::MessageDescriptor pb::IMessage.Descriptor {\n      get { return Descriptor; }\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public UpdateBasketRequest() {\n      OnConstruction();\n    }\n\n    partial void OnConstruction();\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public UpdateBasketRequest(UpdateBasketRequest other) : this() {\n      items_ = other.items_.Clone();\n      _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public UpdateBasketRequest Clone() {\n      return new UpdateBasketRequest(this);\n    }\n\n    /// <summary>Field number for the \"items\" field.</summary>\n    public const int ItemsFieldNumber = 2;\n    private static readonly pb::FieldCodec<global::eShop.ClientApp.BasketGrpcClient.BasketItem> _repeated_items_codec\n        = pb::FieldCodec.ForMessage(18, global::eShop.ClientApp.BasketGrpcClient.BasketItem.Parser);\n    private readonly pbc::RepeatedField<global::eShop.ClientApp.BasketGrpcClient.BasketItem> items_ = new pbc::RepeatedField<global::eShop.ClientApp.BasketGrpcClient.BasketItem>();\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public pbc::RepeatedField<global::eShop.ClientApp.BasketGrpcClient.BasketItem> Items {\n      get { return items_; }\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public override bool Equals(object other) {\n      return Equals(other as UpdateBasketRequest);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public bool Equals(UpdateBasketRequest other) {\n      if (ReferenceEquals(other, null)) {\n        return false;\n      }\n      if (ReferenceEquals(other, this)) {\n        return true;\n      }\n      if(!items_.Equals(other.items_)) return false;\n      return Equals(_unknownFields, other._unknownFields);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public override int GetHashCode() {\n      int hash = 1;\n      hash ^= items_.GetHashCode();\n      if (_unknownFields != null) {\n        hash ^= _unknownFields.GetHashCode();\n      }\n      return hash;\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public override string ToString() {\n      return pb::JsonFormatter.ToDiagnosticString(this);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public void WriteTo(pb::CodedOutputStream output) {\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n      output.WriteRawMessage(this);\n    #else\n      items_.WriteTo(output, _repeated_items_codec);\n      if (_unknownFields != null) {\n        _unknownFields.WriteTo(output);\n      }\n    #endif\n    }\n\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {\n      items_.WriteTo(ref output, _repeated_items_codec);\n      if (_unknownFields != null) {\n        _unknownFields.WriteTo(ref output);\n      }\n    }\n    #endif\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public int CalculateSize() {\n      int size = 0;\n      size += items_.CalculateSize(_repeated_items_codec);\n      if (_unknownFields != null) {\n        size += _unknownFields.CalculateSize();\n      }\n      return size;\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public void MergeFrom(UpdateBasketRequest other) {\n      if (other == null) {\n        return;\n      }\n      items_.Add(other.items_);\n      _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public void MergeFrom(pb::CodedInputStream input) {\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n      input.ReadRawMessage(this);\n    #else\n      uint tag;\n      while ((tag = input.ReadTag()) != 0) {\n      if ((tag & 7) == 4) {\n        // Abort on any end group tag.\n        return;\n      }\n      switch(tag) {\n          default:\n            _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);\n            break;\n          case 18: {\n            items_.AddEntriesFrom(input, _repeated_items_codec);\n            break;\n          }\n        }\n      }\n    #endif\n    }\n\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) {\n      uint tag;\n      while ((tag = input.ReadTag()) != 0) {\n      if ((tag & 7) == 4) {\n        // Abort on any end group tag.\n        return;\n      }\n      switch(tag) {\n          default:\n            _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);\n            break;\n          case 18: {\n            items_.AddEntriesFrom(ref input, _repeated_items_codec);\n            break;\n          }\n        }\n      }\n    }\n    #endif\n\n  }\n\n  [global::System.Diagnostics.DebuggerDisplayAttribute(\"{ToString(),nq}\")]\n  public sealed partial class DeleteBasketRequest : pb::IMessage<DeleteBasketRequest>\n  #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n      , pb::IBufferMessage\n  #endif\n  {\n    private static readonly pb::MessageParser<DeleteBasketRequest> _parser = new pb::MessageParser<DeleteBasketRequest>(() => new DeleteBasketRequest());\n    private pb::UnknownFieldSet _unknownFields;\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public static pb::MessageParser<DeleteBasketRequest> Parser { get { return _parser; } }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public static pbr::MessageDescriptor Descriptor {\n      get { return global::eShop.ClientApp.BasketGrpcClient.BasketReflection.Descriptor.MessageTypes[4]; }\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    pbr::MessageDescriptor pb::IMessage.Descriptor {\n      get { return Descriptor; }\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public DeleteBasketRequest() {\n      OnConstruction();\n    }\n\n    partial void OnConstruction();\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public DeleteBasketRequest(DeleteBasketRequest other) : this() {\n      _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public DeleteBasketRequest Clone() {\n      return new DeleteBasketRequest(this);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public override bool Equals(object other) {\n      return Equals(other as DeleteBasketRequest);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public bool Equals(DeleteBasketRequest other) {\n      if (ReferenceEquals(other, null)) {\n        return false;\n      }\n      if (ReferenceEquals(other, this)) {\n        return true;\n      }\n      return Equals(_unknownFields, other._unknownFields);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public override int GetHashCode() {\n      int hash = 1;\n      if (_unknownFields != null) {\n        hash ^= _unknownFields.GetHashCode();\n      }\n      return hash;\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public override string ToString() {\n      return pb::JsonFormatter.ToDiagnosticString(this);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public void WriteTo(pb::CodedOutputStream output) {\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n      output.WriteRawMessage(this);\n    #else\n      if (_unknownFields != null) {\n        _unknownFields.WriteTo(output);\n      }\n    #endif\n    }\n\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {\n      if (_unknownFields != null) {\n        _unknownFields.WriteTo(ref output);\n      }\n    }\n    #endif\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public int CalculateSize() {\n      int size = 0;\n      if (_unknownFields != null) {\n        size += _unknownFields.CalculateSize();\n      }\n      return size;\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public void MergeFrom(DeleteBasketRequest other) {\n      if (other == null) {\n        return;\n      }\n      _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public void MergeFrom(pb::CodedInputStream input) {\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n      input.ReadRawMessage(this);\n    #else\n      uint tag;\n      while ((tag = input.ReadTag()) != 0) {\n      if ((tag & 7) == 4) {\n        // Abort on any end group tag.\n        return;\n      }\n      switch(tag) {\n          default:\n            _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);\n            break;\n        }\n      }\n    #endif\n    }\n\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) {\n      uint tag;\n      while ((tag = input.ReadTag()) != 0) {\n      if ((tag & 7) == 4) {\n        // Abort on any end group tag.\n        return;\n      }\n      switch(tag) {\n          default:\n            _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);\n            break;\n        }\n      }\n    }\n    #endif\n\n  }\n\n  [global::System.Diagnostics.DebuggerDisplayAttribute(\"{ToString(),nq}\")]\n  public sealed partial class DeleteBasketResponse : pb::IMessage<DeleteBasketResponse>\n  #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n      , pb::IBufferMessage\n  #endif\n  {\n    private static readonly pb::MessageParser<DeleteBasketResponse> _parser = new pb::MessageParser<DeleteBasketResponse>(() => new DeleteBasketResponse());\n    private pb::UnknownFieldSet _unknownFields;\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public static pb::MessageParser<DeleteBasketResponse> Parser { get { return _parser; } }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public static pbr::MessageDescriptor Descriptor {\n      get { return global::eShop.ClientApp.BasketGrpcClient.BasketReflection.Descriptor.MessageTypes[5]; }\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    pbr::MessageDescriptor pb::IMessage.Descriptor {\n      get { return Descriptor; }\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public DeleteBasketResponse() {\n      OnConstruction();\n    }\n\n    partial void OnConstruction();\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public DeleteBasketResponse(DeleteBasketResponse other) : this() {\n      _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public DeleteBasketResponse Clone() {\n      return new DeleteBasketResponse(this);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public override bool Equals(object other) {\n      return Equals(other as DeleteBasketResponse);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public bool Equals(DeleteBasketResponse other) {\n      if (ReferenceEquals(other, null)) {\n        return false;\n      }\n      if (ReferenceEquals(other, this)) {\n        return true;\n      }\n      return Equals(_unknownFields, other._unknownFields);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public override int GetHashCode() {\n      int hash = 1;\n      if (_unknownFields != null) {\n        hash ^= _unknownFields.GetHashCode();\n      }\n      return hash;\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public override string ToString() {\n      return pb::JsonFormatter.ToDiagnosticString(this);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public void WriteTo(pb::CodedOutputStream output) {\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n      output.WriteRawMessage(this);\n    #else\n      if (_unknownFields != null) {\n        _unknownFields.WriteTo(output);\n      }\n    #endif\n    }\n\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {\n      if (_unknownFields != null) {\n        _unknownFields.WriteTo(ref output);\n      }\n    }\n    #endif\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public int CalculateSize() {\n      int size = 0;\n      if (_unknownFields != null) {\n        size += _unknownFields.CalculateSize();\n      }\n      return size;\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public void MergeFrom(DeleteBasketResponse other) {\n      if (other == null) {\n        return;\n      }\n      _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);\n    }\n\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    public void MergeFrom(pb::CodedInputStream input) {\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n      input.ReadRawMessage(this);\n    #else\n      uint tag;\n      while ((tag = input.ReadTag()) != 0) {\n      if ((tag & 7) == 4) {\n        // Abort on any end group tag.\n        return;\n      }\n      switch(tag) {\n          default:\n            _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);\n            break;\n        }\n      }\n    #endif\n    }\n\n    #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE\n    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]\n    [global::System.CodeDom.Compiler.GeneratedCode(\"protoc\", null)]\n    void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) {\n      uint tag;\n      while ((tag = input.ReadTag()) != 0) {\n      if ((tag & 7) == 4) {\n        // Abort on any end group tag.\n        return;\n      }\n      switch(tag) {\n          default:\n            _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);\n            break;\n        }\n      }\n    }\n    #endif\n\n  }\n\n  #endregion\n\n}\n\n#endregion Designer generated code\n"
  },
  {
    "path": "src/ClientApp/Services/Basket/Protos/BasketGrpc.cs",
    "content": "// <auto-generated>\n//     Generated by the protocol buffer compiler.  DO NOT EDIT!\n//     source: Services/Basket/Protos/basket.proto\n// </auto-generated>\n#pragma warning disable 0414, 1591, 8981, 0612\n#region Designer generated code\n\nusing grpc = global::Grpc.Core;\n\nnamespace eShop.ClientApp.BasketGrpcClient {\n  public static partial class Basket\n  {\n    static readonly string __ServiceName = \"BasketApi.Basket\";\n\n    [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n    static void __Helper_SerializeMessage(global::Google.Protobuf.IMessage message, grpc::SerializationContext context)\n    {\n      #if !GRPC_DISABLE_PROTOBUF_BUFFER_SERIALIZATION\n      if (message is global::Google.Protobuf.IBufferMessage)\n      {\n        context.SetPayloadLength(message.CalculateSize());\n        global::Google.Protobuf.MessageExtensions.WriteTo(message, context.GetBufferWriter());\n        context.Complete();\n        return;\n      }\n      #endif\n      context.Complete(global::Google.Protobuf.MessageExtensions.ToByteArray(message));\n    }\n\n    [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n    static class __Helper_MessageCache<T>\n    {\n      public static readonly bool IsBufferMessage = global::System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(global::Google.Protobuf.IBufferMessage)).IsAssignableFrom(typeof(T));\n    }\n\n    [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n    static T __Helper_DeserializeMessage<T>(grpc::DeserializationContext context, global::Google.Protobuf.MessageParser<T> parser) where T : global::Google.Protobuf.IMessage<T>\n    {\n      #if !GRPC_DISABLE_PROTOBUF_BUFFER_SERIALIZATION\n      if (__Helper_MessageCache<T>.IsBufferMessage)\n      {\n        return parser.ParseFrom(context.PayloadAsReadOnlySequence());\n      }\n      #endif\n      return parser.ParseFrom(context.PayloadAsNewBuffer());\n    }\n\n    [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n    static readonly grpc::Marshaller<global::eShop.ClientApp.BasketGrpcClient.GetBasketRequest> __Marshaller_BasketApi_GetBasketRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::eShop.ClientApp.BasketGrpcClient.GetBasketRequest.Parser));\n    [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n    static readonly grpc::Marshaller<global::eShop.ClientApp.BasketGrpcClient.CustomerBasketResponse> __Marshaller_BasketApi_CustomerBasketResponse = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::eShop.ClientApp.BasketGrpcClient.CustomerBasketResponse.Parser));\n    [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n    static readonly grpc::Marshaller<global::eShop.ClientApp.BasketGrpcClient.UpdateBasketRequest> __Marshaller_BasketApi_UpdateBasketRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::eShop.ClientApp.BasketGrpcClient.UpdateBasketRequest.Parser));\n    [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n    static readonly grpc::Marshaller<global::eShop.ClientApp.BasketGrpcClient.DeleteBasketRequest> __Marshaller_BasketApi_DeleteBasketRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::eShop.ClientApp.BasketGrpcClient.DeleteBasketRequest.Parser));\n    [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n    static readonly grpc::Marshaller<global::eShop.ClientApp.BasketGrpcClient.DeleteBasketResponse> __Marshaller_BasketApi_DeleteBasketResponse = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::eShop.ClientApp.BasketGrpcClient.DeleteBasketResponse.Parser));\n\n    [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n    static readonly grpc::Method<global::eShop.ClientApp.BasketGrpcClient.GetBasketRequest, global::eShop.ClientApp.BasketGrpcClient.CustomerBasketResponse> __Method_GetBasket = new grpc::Method<global::eShop.ClientApp.BasketGrpcClient.GetBasketRequest, global::eShop.ClientApp.BasketGrpcClient.CustomerBasketResponse>(\n        grpc::MethodType.Unary,\n        __ServiceName,\n        \"GetBasket\",\n        __Marshaller_BasketApi_GetBasketRequest,\n        __Marshaller_BasketApi_CustomerBasketResponse);\n\n    [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n    static readonly grpc::Method<global::eShop.ClientApp.BasketGrpcClient.UpdateBasketRequest, global::eShop.ClientApp.BasketGrpcClient.CustomerBasketResponse> __Method_UpdateBasket = new grpc::Method<global::eShop.ClientApp.BasketGrpcClient.UpdateBasketRequest, global::eShop.ClientApp.BasketGrpcClient.CustomerBasketResponse>(\n        grpc::MethodType.Unary,\n        __ServiceName,\n        \"UpdateBasket\",\n        __Marshaller_BasketApi_UpdateBasketRequest,\n        __Marshaller_BasketApi_CustomerBasketResponse);\n\n    [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n    static readonly grpc::Method<global::eShop.ClientApp.BasketGrpcClient.DeleteBasketRequest, global::eShop.ClientApp.BasketGrpcClient.DeleteBasketResponse> __Method_DeleteBasket = new grpc::Method<global::eShop.ClientApp.BasketGrpcClient.DeleteBasketRequest, global::eShop.ClientApp.BasketGrpcClient.DeleteBasketResponse>(\n        grpc::MethodType.Unary,\n        __ServiceName,\n        \"DeleteBasket\",\n        __Marshaller_BasketApi_DeleteBasketRequest,\n        __Marshaller_BasketApi_DeleteBasketResponse);\n\n    /// <summary>Service descriptor</summary>\n    public static global::Google.Protobuf.Reflection.ServiceDescriptor Descriptor\n    {\n      get { return global::eShop.ClientApp.BasketGrpcClient.BasketReflection.Descriptor.Services[0]; }\n    }\n\n    /// <summary>Client for Basket</summary>\n    public partial class BasketClient : grpc::ClientBase<BasketClient>\n    {\n      /// <summary>Creates a new client for Basket</summary>\n      /// <param name=\"channel\">The channel to use to make remote calls.</param>\n      [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n      public BasketClient(grpc::ChannelBase channel) : base(channel)\n      {\n      }\n      /// <summary>Creates a new client for Basket that uses a custom <c>CallInvoker</c>.</summary>\n      /// <param name=\"callInvoker\">The callInvoker to use to make remote calls.</param>\n      [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n      public BasketClient(grpc::CallInvoker callInvoker) : base(callInvoker)\n      {\n      }\n      /// <summary>Protected parameterless constructor to allow creation of test doubles.</summary>\n      [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n      protected BasketClient() : base()\n      {\n      }\n      /// <summary>Protected constructor to allow creation of configured clients.</summary>\n      /// <param name=\"configuration\">The client configuration.</param>\n      [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n      protected BasketClient(ClientBaseConfiguration configuration) : base(configuration)\n      {\n      }\n\n      [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n      public virtual global::eShop.ClientApp.BasketGrpcClient.CustomerBasketResponse GetBasket(global::eShop.ClientApp.BasketGrpcClient.GetBasketRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))\n      {\n        return GetBasket(request, new grpc::CallOptions(headers, deadline, cancellationToken));\n      }\n      [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n      public virtual global::eShop.ClientApp.BasketGrpcClient.CustomerBasketResponse GetBasket(global::eShop.ClientApp.BasketGrpcClient.GetBasketRequest request, grpc::CallOptions options)\n      {\n        return CallInvoker.BlockingUnaryCall(__Method_GetBasket, null, options, request);\n      }\n      [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n      public virtual grpc::AsyncUnaryCall<global::eShop.ClientApp.BasketGrpcClient.CustomerBasketResponse> GetBasketAsync(global::eShop.ClientApp.BasketGrpcClient.GetBasketRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))\n      {\n        return GetBasketAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));\n      }\n      [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n      public virtual grpc::AsyncUnaryCall<global::eShop.ClientApp.BasketGrpcClient.CustomerBasketResponse> GetBasketAsync(global::eShop.ClientApp.BasketGrpcClient.GetBasketRequest request, grpc::CallOptions options)\n      {\n        return CallInvoker.AsyncUnaryCall(__Method_GetBasket, null, options, request);\n      }\n      [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n      public virtual global::eShop.ClientApp.BasketGrpcClient.CustomerBasketResponse UpdateBasket(global::eShop.ClientApp.BasketGrpcClient.UpdateBasketRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))\n      {\n        return UpdateBasket(request, new grpc::CallOptions(headers, deadline, cancellationToken));\n      }\n      [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n      public virtual global::eShop.ClientApp.BasketGrpcClient.CustomerBasketResponse UpdateBasket(global::eShop.ClientApp.BasketGrpcClient.UpdateBasketRequest request, grpc::CallOptions options)\n      {\n        return CallInvoker.BlockingUnaryCall(__Method_UpdateBasket, null, options, request);\n      }\n      [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n      public virtual grpc::AsyncUnaryCall<global::eShop.ClientApp.BasketGrpcClient.CustomerBasketResponse> UpdateBasketAsync(global::eShop.ClientApp.BasketGrpcClient.UpdateBasketRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))\n      {\n        return UpdateBasketAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));\n      }\n      [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n      public virtual grpc::AsyncUnaryCall<global::eShop.ClientApp.BasketGrpcClient.CustomerBasketResponse> UpdateBasketAsync(global::eShop.ClientApp.BasketGrpcClient.UpdateBasketRequest request, grpc::CallOptions options)\n      {\n        return CallInvoker.AsyncUnaryCall(__Method_UpdateBasket, null, options, request);\n      }\n      [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n      public virtual global::eShop.ClientApp.BasketGrpcClient.DeleteBasketResponse DeleteBasket(global::eShop.ClientApp.BasketGrpcClient.DeleteBasketRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))\n      {\n        return DeleteBasket(request, new grpc::CallOptions(headers, deadline, cancellationToken));\n      }\n      [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n      public virtual global::eShop.ClientApp.BasketGrpcClient.DeleteBasketResponse DeleteBasket(global::eShop.ClientApp.BasketGrpcClient.DeleteBasketRequest request, grpc::CallOptions options)\n      {\n        return CallInvoker.BlockingUnaryCall(__Method_DeleteBasket, null, options, request);\n      }\n      [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n      public virtual grpc::AsyncUnaryCall<global::eShop.ClientApp.BasketGrpcClient.DeleteBasketResponse> DeleteBasketAsync(global::eShop.ClientApp.BasketGrpcClient.DeleteBasketRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken))\n      {\n        return DeleteBasketAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken));\n      }\n      [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n      public virtual grpc::AsyncUnaryCall<global::eShop.ClientApp.BasketGrpcClient.DeleteBasketResponse> DeleteBasketAsync(global::eShop.ClientApp.BasketGrpcClient.DeleteBasketRequest request, grpc::CallOptions options)\n      {\n        return CallInvoker.AsyncUnaryCall(__Method_DeleteBasket, null, options, request);\n      }\n      /// <summary>Creates a new instance of client from given <c>ClientBaseConfiguration</c>.</summary>\n      [global::System.CodeDom.Compiler.GeneratedCode(\"grpc_csharp_plugin\", null)]\n      protected override BasketClient NewInstance(ClientBaseConfiguration configuration)\n      {\n        return new BasketClient(configuration);\n      }\n    }\n\n  }\n}\n#endregion\n"
  },
  {
    "path": "src/ClientApp/Services/Basket/Protos/basket.proto",
    "content": "﻿syntax = \"proto3\";\n\noption csharp_namespace = \"eShop.ClientApp.BasketGrpcClient\";\n\npackage BasketApi;\n\nservice Basket {\n    rpc GetBasket(GetBasketRequest) returns (CustomerBasketResponse) {}\n    rpc UpdateBasket(UpdateBasketRequest) returns (CustomerBasketResponse) {}\n    rpc DeleteBasket(DeleteBasketRequest) returns (DeleteBasketResponse) {}\n}\n\nmessage GetBasketRequest {\n}\n\nmessage CustomerBasketResponse {\n    repeated BasketItem items = 1;\n}\n\nmessage BasketItem {\n    int32 product_id = 2;\n    int32 quantity = 6;\n}\n\nmessage UpdateBasketRequest {\n    repeated BasketItem items = 2;\n}\n\nmessage DeleteBasketRequest {\n}\n\nmessage DeleteBasketResponse {\n}"
  },
  {
    "path": "src/ClientApp/Services/Catalog/CatalogMockService.cs",
    "content": "﻿using eShop.ClientApp.Models.Catalog;\n\nnamespace eShop.ClientApp.Services.Catalog;\n\npublic class CatalogMockService : ICatalogService\n{\n    private static readonly List<CatalogBrand> MockCatalogBrands =\n        new() {new CatalogBrand {Id = 1, Brand = \"Azure\"}, new CatalogBrand {Id = 2, Brand = \"Visual Studio\"}};\n\n    private static readonly List<CatalogType> MockCatalogTypes =\n        new() {new CatalogType {Id = 1, Type = \"Mug\"}, new CatalogType {Id = 2, Type = \"T-Shirt\"}};\n\n    private static readonly List<CatalogItem> MockCatalog =\n        new()\n        {\n            new CatalogItem\n            {\n                Id = Common.Common.MockCatalogItemId01,\n                PictureUri = \"fake_product_01.png\",\n                Name = \"Adventurer GPS Watch\",\n                Price = 199.99M,\n                CatalogBrandId = 2,\n                CatalogBrand = MockCatalogBrands[1],\n                CatalogTypeId = 2,\n                CatalogType = MockCatalogTypes[1],\n                Description = \"Navigate with confidence using the Adventurer GPS Watch by Adventurer. This rugged and durable watch features a built-in GPS, altimeter, and compass, allowing you to track your progress and find your way in any terrain. With its sleek black design and easy-to-read display, this watch is both stylish and practical. The Adventurer GPS Watch is a must-have for every adventurer.\"\n            },\n            new CatalogItem\n            {\n                Id = Common.Common.MockCatalogItemId02,\n                PictureUri = \"fake_product_02.png\",\n                Name = \"AeroLite Cycling Helmet\",\n                Price = 129.99M,\n                CatalogBrandId = 2,\n                CatalogBrand = MockCatalogBrands[1],\n                CatalogTypeId = 2,\n                CatalogType = MockCatalogTypes[1],\n                Description = \"Stay safe on your cycling adventures with the Trailblazer Bike Helmet by Green Equipment. This lightweight and durable helmet features an adjustable fit system and ventilation for added comfort. With its vibrant green color and sleek design, you'll stand out on the road. The Trailblazer Bike Helmet is perfect for all types of cycling, from mountain biking to road cycling.\"\n            },\n            new CatalogItem\n            {\n                Id = Common.Common.MockCatalogItemId03,\n                PictureUri = \"fake_product_03.png\",\n                Name = \"Alpine AlpinePack Backpack\",\n                Price = 129.00M,\n                CatalogBrandId = 2,\n                CatalogBrand = MockCatalogBrands[1],\n                CatalogTypeId = 2,\n                CatalogType = MockCatalogTypes[1],\n                Description = \"The AlpinePack backpack by Green Equipment is your ultimate companion for outdoor adventures. This versatile and durable backpack features a sleek navy design with reinforced straps. With a capacity of 45 liters, multiple compartments, and a hydration pack sleeve, it offers ample storage and organization. The ergonomic back panel ensures maximum comfort, even on the most challenging treks.\"\n            },\n            new CatalogItem\n            {\n                Id = Common.Common.MockCatalogItemId04,\n                PictureUri = \"fake_product_04.png\",\n                Name = \"Alpine Fusion Goggles\",\n                Price = 79.99M,\n                CatalogBrandId = 2,\n                CatalogBrand = MockCatalogBrands[1],\n                CatalogTypeId = 1,\n                CatalogType = MockCatalogTypes[0],\n                Description = \"Enhance your skiing experience with the Alpine Fusion Goggles from WildRunner. These goggles offer full UV protection and anti-fog lenses to keep your vision clear on the slopes. With their stylish silver frame and orange lenses, you'll stand out from the crowd. Adjustable straps ensure a secure fit, while the soft foam padding provides comfort all day long.\"\n            },\n            new CatalogItem\n            {\n                Id = Common.Common.MockCatalogItemId05,\n                PictureUri = \"fake_product_05.png\",\n                Name = \"Alpine PeakDown Jacket\",\n                Price = 249.99M,\n                CatalogBrandId = 1,\n                CatalogBrand = MockCatalogBrands[0],\n                CatalogTypeId = 2,\n                CatalogType = MockCatalogTypes[1],\n                Description = \"The Solstix Alpine Peak Down Jacket is crafted for extreme cold conditions. With its bold red color and sleek design, this jacket combines style with functionality. Made with high-quality goose down insulation, the Alpine Peak Jacket provides exceptional warmth and comfort. The jacket features a removable hood, adjustable cuffs, and multiple zippered pockets for storage. Conquer the harshest weather with the Solstix Alpine Peak Down Jacket.\"\n            }\n        };\n\n    public async Task<IEnumerable<CatalogItem>> GetCatalogAsync()\n    {\n        await Task.Delay(10);\n\n        return MockCatalog;\n    }\n\n    public async Task<CatalogItem> GetCatalogItemAsync(int catalogItemId)\n    {\n        await Task.Delay(10);\n\n        return MockCatalog.FirstOrDefault(x => x.Id == catalogItemId);\n    }\n\n    public async Task<IEnumerable<CatalogItem>> FilterAsync(int catalogBrandId, int catalogTypeId)\n    {\n        await Task.Delay(10);\n\n        return MockCatalog\n            .Where(\n                c => c.CatalogBrandId == catalogBrandId &&\n                     c.CatalogTypeId == catalogTypeId)\n            .ToArray();\n    }\n\n    public async Task<IEnumerable<CatalogBrand>> GetCatalogBrandAsync()\n    {\n        await Task.Delay(10);\n\n        return MockCatalogBrands;\n    }\n\n    public async Task<IEnumerable<CatalogType>> GetCatalogTypeAsync()\n    {\n        await Task.Delay(10);\n\n        return MockCatalogTypes;\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Catalog/CatalogService.cs",
    "content": "using eShop.ClientApp.Helpers;\nusing eShop.ClientApp.Models.Catalog;\nusing eShop.ClientApp.Services.FixUri;\nusing eShop.ClientApp.Services.RequestProvider;\nusing eShop.ClientApp.Services.Settings;\n\nnamespace eShop.ClientApp.Services.Catalog;\n\npublic class CatalogService : ICatalogService\n{\n    private const string ApiUrlBase = \"api/catalog\";\n    private const string ApiVersion = \"api-version=2.0\";\n\n    private readonly IFixUriService _fixUriService;\n    private readonly IRequestProvider _requestProvider;\n    private readonly ISettingsService _settingsService;\n\n    public CatalogService(ISettingsService settingsService, IRequestProvider requestProvider,\n        IFixUriService fixUriService)\n    {\n        _settingsService = settingsService;\n        _requestProvider = requestProvider;\n        _fixUriService = fixUriService;\n    }\n\n    public async Task<IEnumerable<CatalogItem>> FilterAsync(int catalogBrandId, int catalogTypeId)\n    {\n        var uri = UriHelper.CombineUri(_settingsService.GatewayCatalogEndpointBase,\n            $\"{ApiUrlBase}//items?type={catalogTypeId}&brand={catalogBrandId}&PageSize=100&PageIndex=0&{ApiVersion}\");\n\n        var catalog = await _requestProvider.GetAsync<CatalogRoot>(uri).ConfigureAwait(false);\n\n        return catalog?.Data ?? Enumerable.Empty<CatalogItem>();\n    }\n\n    public async Task<IEnumerable<CatalogItem>> GetCatalogAsync()\n    {\n        var uri = UriHelper.CombineUri(_settingsService.GatewayCatalogEndpointBase, $\"{ApiUrlBase}/items?PageSize=100&{ApiVersion}\");\n\n        var catalog = await _requestProvider.GetAsync<CatalogRoot>(uri).ConfigureAwait(false);\n\n        if (catalog?.Data != null)\n        {\n            _fixUriService.FixCatalogItemPictureUri(catalog.Data);\n            return catalog.Data;\n        }\n\n        return Enumerable.Empty<CatalogItem>();\n    }\n\n    public async Task<CatalogItem> GetCatalogItemAsync(int catalogItemId)\n    {\n        var uri = UriHelper.CombineUri(_settingsService.GatewayCatalogEndpointBase,\n            $\"{ApiUrlBase}/items/{catalogItemId}?{ApiVersion}\");\n\n        var catalogItem = await _requestProvider.GetAsync<CatalogItem>(uri).ConfigureAwait(false);\n\n        if (catalogItem != null)\n        {\n            _fixUriService.FixCatalogItemPictureUri(new[] {catalogItem});\n            return catalogItem;\n        }\n\n        return default;\n    }\n\n    public async Task<IEnumerable<CatalogBrand>> GetCatalogBrandAsync()\n    {\n        var uri = UriHelper.CombineUri(_settingsService.GatewayCatalogEndpointBase, $\"{ApiUrlBase}/catalogbrands?{ApiVersion}\");\n\n        var brands = await _requestProvider.GetAsync<IEnumerable<CatalogBrand>>(uri).ConfigureAwait(false);\n\n        return brands?.ToArray() ?? Enumerable.Empty<CatalogBrand>();\n    }\n\n    public async Task<IEnumerable<CatalogType>> GetCatalogTypeAsync()\n    {\n        var uri = UriHelper.CombineUri(_settingsService.GatewayCatalogEndpointBase, $\"{ApiUrlBase}/catalogtypes?{ApiVersion}\");\n\n        var types = await _requestProvider.GetAsync<IEnumerable<CatalogType>>(uri).ConfigureAwait(false);\n\n        return types?.ToArray() ?? Enumerable.Empty<CatalogType>();\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Catalog/ICatalogService.cs",
    "content": "using eShop.ClientApp.Models.Catalog;\n\nnamespace eShop.ClientApp.Services.Catalog;\n\npublic interface ICatalogService\n{\n    Task<IEnumerable<CatalogBrand>> GetCatalogBrandAsync();\n    Task<IEnumerable<CatalogItem>> FilterAsync(int catalogBrandId, int catalogTypeId);\n    Task<IEnumerable<CatalogType>> GetCatalogTypeAsync();\n    Task<IEnumerable<CatalogItem>> GetCatalogAsync();\n\n    Task<CatalogItem> GetCatalogItemAsync(int catalogItemId);\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Common/Common.cs",
    "content": "﻿namespace eShop.ClientApp.Services.Common;\n\npublic static class Common\n{\n    public static int MockCatalogItemId01 = 1;\n    public static int MockCatalogItemId02 = 2;\n    public static int MockCatalogItemId03 = 3;\n    public static int MockCatalogItemId04 = 4;\n    public static int MockCatalogItemId05 = 5;\n\n    public static int MockCampaignId01 = 1;\n    public static int MockCampaignId02 = 2;\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Dialog/DialogService.cs",
    "content": "﻿namespace eShop.ClientApp.Services;\n\npublic class DialogService : IDialogService\n{\n    public Task ShowAlertAsync(string message, string title, string buttonLabel)\n    {\n        return AppShell.Current.DisplayAlert(title, message, buttonLabel);\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Dialog/IDialogService.cs",
    "content": "﻿namespace eShop.ClientApp.Services;\n\npublic interface IDialogService\n{\n    Task ShowAlertAsync(string message, string title, string buttonLabel);\n}\n"
  },
  {
    "path": "src/ClientApp/Services/EShopJsonSerializerContext.cs",
    "content": "using System.Text.Json.Serialization;\nusing eShop.ClientApp.Models.Catalog;\nusing eShop.ClientApp.Models.Orders;\nusing eShop.ClientApp.Models.Token;\n\nnamespace eShop.ClientApp.Services;\n\n[JsonSourceGenerationOptions(\n    PropertyNameCaseInsensitive = true,\n    NumberHandling = JsonNumberHandling.AllowReadingFromString)]\n[JsonSerializable(typeof(CancelOrderCommand))]\n[JsonSerializable(typeof(CatalogBrand))]\n[JsonSerializable(typeof(CatalogItem))]\n[JsonSerializable(typeof(CatalogRoot))]\n[JsonSerializable(typeof(CatalogType))]\n[JsonSerializable(typeof(Models.Orders.Order))]\n[JsonSerializable(typeof(Models.Location.Location))]\n[JsonSerializable(typeof(UserToken))]\ninternal partial class EShopJsonSerializerContext : JsonSerializerContext\n{\n}\n"
  },
  {
    "path": "src/ClientApp/Services/FixUri/FixUriService.cs",
    "content": "using System.Diagnostics;\nusing System.Text.RegularExpressions;\nusing eShop.ClientApp.Models.Basket;\nusing eShop.ClientApp.Models.Catalog;\nusing eShop.ClientApp.Models.Marketing;\nusing eShop.ClientApp.Services.Settings;\n\nnamespace eShop.ClientApp.Services.FixUri;\n\npublic class FixUriService : IFixUriService\n{\n    private const string ApiVersion = \"api-version=2.0\";\n\n    private readonly ISettingsService _settingsService;\n\n    private readonly Regex IpRegex = new(@\"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b\");\n\n    public FixUriService(ISettingsService settingsService)\n    {\n        _settingsService = settingsService;\n    }\n\n    public void FixCatalogItemPictureUri(IEnumerable<CatalogItem> catalogItems)\n    {\n        if (catalogItems is null)\n        {\n            return;\n        }\n\n        try\n        {\n            if (!_settingsService.UseMocks && _settingsService.GatewayCatalogEndpointBase != _settingsService.DefaultEndpoint)\n            {\n                foreach (var catalogItem in catalogItems)\n                {\n                    catalogItem.PictureUri = Path.Combine(_settingsService.GatewayCatalogEndpointBase, $\"api/catalog/items/{catalogItem.Id}/pic?{ApiVersion}\");\n                }\n            }\n        }\n        catch (Exception ex)\n        {\n            Debug.WriteLine(ex.Message);\n        }\n    }\n\n    public void FixBasketItemPictureUri(IEnumerable<BasketItem> basketItems)\n    {\n        if (basketItems is null)\n        {\n            return;\n        }\n\n        try\n        {\n            if (!_settingsService.UseMocks && _settingsService.IdentityEndpointBase != _settingsService.DefaultEndpoint)\n            {\n                foreach (var basketItem in basketItems)\n                {\n                    var serverResult = IpRegex.Matches(basketItem.PictureUrl);\n                    var localResult = IpRegex.Matches(_settingsService.IdentityEndpointBase);\n\n                    if (serverResult.Count != -1 && localResult.Count != -1)\n                    {\n                        var serviceIp = serverResult[0].Value;\n                        var localIp = localResult[0].Value;\n                        basketItem.PictureUrl = basketItem.PictureUrl.Replace(serviceIp, localIp);\n                    }\n                }\n            }\n        }\n        catch (Exception ex)\n        {\n            Debug.WriteLine(ex.Message);\n        }\n    }\n\n    public void FixCampaignItemPictureUri(IEnumerable<CampaignItem> campaignItems)\n    {\n        if (campaignItems is null)\n        {\n            return;\n        }\n\n        try\n        {\n            if (!_settingsService.UseMocks && _settingsService.IdentityEndpointBase != _settingsService.DefaultEndpoint)\n            {\n                foreach (var campaignItem in campaignItems)\n                {\n                    var serverResult = IpRegex.Matches(campaignItem.PictureUri);\n                    var localResult = IpRegex.Matches(_settingsService.IdentityEndpointBase);\n\n                    if (serverResult.Count != -1 && localResult.Count != -1)\n                    {\n                        var serviceIp = serverResult[0].Value;\n                        var localIp = localResult[0].Value;\n\n                        campaignItem.PictureUri = campaignItem.PictureUri.Replace(serviceIp, localIp);\n                    }\n                }\n            }\n        }\n        catch (Exception ex)\n        {\n            Debug.WriteLine(ex.Message);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Services/FixUri/IFixUriService.cs",
    "content": "﻿using eShop.ClientApp.Models.Basket;\nusing eShop.ClientApp.Models.Catalog;\nusing eShop.ClientApp.Models.Marketing;\n\nnamespace eShop.ClientApp.Services.FixUri;\n\npublic interface IFixUriService\n{\n    void FixCatalogItemPictureUri(IEnumerable<CatalogItem> catalogItems);\n    void FixBasketItemPictureUri(IEnumerable<BasketItem> basketItems);\n    void FixCampaignItemPictureUri(IEnumerable<CampaignItem> campaignItems);\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Identity/AuthorizeRequest.cs",
    "content": "﻿using System.Net;\n\nnamespace eShop.ClientApp.Services.Identity;\n\npublic class AuthorizeRequest\n{\n    private readonly Uri _authorizeEndpoint;\n\n    public AuthorizeRequest(string authorizeEndpoint)\n    {\n        _authorizeEndpoint = new Uri(authorizeEndpoint);\n    }\n\n    public string Create(IDictionary<string, string> values)\n    {\n        var queryString = string.Join(\"&\",\n            values.Select(kvp =>\n                string.Format(\"{0}={1}\", WebUtility.UrlEncode(kvp.Key), WebUtility.UrlEncode(kvp.Value))).ToArray());\n        return string.Format(\"{0}?{1}\", _authorizeEndpoint.AbsoluteUri, queryString);\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Identity/IIdentityService.cs",
    "content": "using eShop.ClientApp.Models.User;\n\nnamespace eShop.ClientApp.Services.Identity;\n\npublic interface IIdentityService\n{\n    Task<bool> SignInAsync();\n\n    Task<bool> SignOutAsync();\n\n    Task<UserInfo> GetUserInfoAsync();\n\n    Task<string> GetAuthTokenAsync();\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Identity/IdentityMockService.cs",
    "content": "using eShop.ClientApp.Models.User;\n\nnamespace eShop.ClientApp.Services.Identity;\n\npublic class IdentityMockService : IIdentityService\n{\n    private bool _signedIn;\n\n    public Task<bool> SignInAsync()\n    {\n        _signedIn = true;\n        return Task.FromResult(_signedIn);\n    }\n\n    public Task<bool> SignOutAsync()\n    {\n        _signedIn = false;\n        return Task.FromResult(_signedIn);\n    }\n\n    public Task<UserInfo> GetUserInfoAsync()\n    {\n        if (!_signedIn)\n        {\n            return Task.FromResult(UserInfo.Default);\n        }\n\n        return Task.FromResult(new UserInfo\n        {\n            UserId = Guid.NewGuid().ToString(),\n            PreferredUsername = \"sampleUser\",\n            Name = \"Sample\",\n            LastName = \"User\",\n            CardNumber = \"XXXXXXXXXXXX3456\",\n            CardHolder = \"Sample User\",\n            CardSecurityNumber = \"123\",\n            Address = \"123 Sample Street\",\n            Country = \"USA\",\n            State = \"Washington\",\n            Street = \"123 Sample Street\",\n            ZipCode = \"12345\",\n            Email = \"sample.user@example.com\",\n            EmailVerified = true,\n            PhoneNumber = \"1234567890\",\n            PhoneNumberVerified = true\n        });\n    }\n\n    public Task<string> GetAuthTokenAsync()\n    {\n        if (!_signedIn)\n        {\n            return Task.FromResult(string.Empty);\n        }\n\n        return Task.FromResult(Guid.NewGuid().ToString());\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Identity/IdentityService.cs",
    "content": "using eShop.ClientApp.Models.Token;\nusing eShop.ClientApp.Models.User;\nusing eShop.ClientApp.Services.Settings;\nusing IdentityModel.OidcClient;\nusing IBrowser = IdentityModel.OidcClient.Browser.IBrowser;\n\nnamespace eShop.ClientApp.Services.Identity;\n\npublic class IdentityService : IIdentityService\n{\n    private readonly IBrowser _browser;\n    private readonly ISettingsService _settingsService;\n    private readonly HttpMessageHandler _httpMessageHandler;\n\n    public IdentityService(IBrowser browser, ISettingsService settingsService, HttpMessageHandler httpMessageHandler)\n    {\n        _browser = browser;\n        _settingsService = settingsService;\n        _httpMessageHandler = httpMessageHandler;\n    }\n\n    public async Task<bool> SignInAsync()\n    {\n        var response = await GetClient().LoginAsync(new LoginRequest()).ConfigureAwait(false);\n\n        if (response.IsError)\n        {\n            return false;\n        }\n\n        await _settingsService\n            .SetUserTokenAsync(\n                new UserToken\n                {\n                    AccessToken = response.AccessToken,\n                    IdToken = response.IdentityToken,\n                    RefreshToken = response.RefreshToken,\n                    ExpiresAt = response.AccessTokenExpiration\n                })\n            .ConfigureAwait(false);\n\n        return !response.IsError;\n    }\n\n    public async Task<bool> SignOutAsync()\n    {\n        var response = await GetClient().LogoutAsync(new LogoutRequest()).ConfigureAwait(false);\n\n        if (response.IsError)\n        {\n            return false;\n        }\n\n        await _settingsService.SetUserTokenAsync(default);\n\n        return !response.IsError;\n    }\n\n    public async Task<UserInfo> GetUserInfoAsync()\n    {\n        var authToken = await GetAuthTokenAsync().ConfigureAwait(false);\n\n        if (string.IsNullOrEmpty(authToken))\n        {\n            return UserInfo.Default;\n        }\n\n        var userInfoWithClaims = await GetClient().GetUserInfoAsync(authToken).ConfigureAwait(false);\n\n        return\n            new UserInfo\n            {\n                UserId = userInfoWithClaims.Claims.FirstOrDefault(c => c.Type == \"sub\")?.Value,\n                Email = userInfoWithClaims.Claims.FirstOrDefault(c => c.Type == \"email\")?.Value,\n                PhoneNumber = userInfoWithClaims.Claims.FirstOrDefault(c => c.Type == \"phone_number\")?.Value,\n                Street = userInfoWithClaims.Claims.FirstOrDefault(c => c.Type == \"address_street\")?.Value,\n                Address = userInfoWithClaims.Claims.FirstOrDefault(c => c.Type == \"address_city\")?.Value,\n                State = userInfoWithClaims.Claims.FirstOrDefault(c => c.Type == \"address_state\")?.Value,\n                ZipCode = userInfoWithClaims.Claims.FirstOrDefault(c => c.Type == \"address_zip_code\")?.Value,\n                Country = userInfoWithClaims.Claims.FirstOrDefault(c => c.Type == \"address_country\")?.Value,\n                PreferredUsername =\n                    userInfoWithClaims.Claims.FirstOrDefault(c => c.Type == \"preferred_username\")?.Value,\n                Name = userInfoWithClaims.Claims.FirstOrDefault(c => c.Type == \"name\")?.Value,\n                LastName = userInfoWithClaims.Claims.FirstOrDefault(c => c.Type == \"last_name\")?.Value,\n                CardNumber = userInfoWithClaims.Claims.FirstOrDefault(c => c.Type == \"card_number\")?.Value,\n                CardHolder = userInfoWithClaims.Claims.FirstOrDefault(c => c.Type == \"card_holder\")?.Value,\n                CardSecurityNumber =\n                    userInfoWithClaims.Claims.FirstOrDefault(c => c.Type == \"card_security_number\")?.Value,\n                PhoneNumberVerified =\n                    bool.Parse(userInfoWithClaims.Claims.FirstOrDefault(c => c.Type == \"phone_number_verified\")\n                        ?.Value ?? \"false\"),\n                EmailVerified =\n                    bool.Parse(userInfoWithClaims.Claims.FirstOrDefault(c => c.Type == \"email_verified\")?.Value ??\n                               \"false\")\n            };\n    }\n    \n    public async Task<string> GetAuthTokenAsync()\n    {\n        var userToken = await _settingsService.GetUserTokenAsync().ConfigureAwait(false);\n\n        if (userToken is null)\n        {\n            return string.Empty;\n        }\n\n        if (userToken.ExpiresAt.Subtract(DateTimeOffset.Now).TotalMinutes > 5)\n        {\n            return userToken.AccessToken;\n        }\n\n        var response = await GetClient().RefreshTokenAsync(userToken.RefreshToken).ConfigureAwait(false);\n\n        if (response.IsError)\n        {\n            return string.Empty;\n        }\n\n        await _settingsService\n            .SetUserTokenAsync(\n                new UserToken\n                {\n                    AccessToken = response.AccessToken,\n                    IdToken = response.IdentityToken,\n                    RefreshToken = response.RefreshToken,\n                    ExpiresAt = response.AccessTokenExpiration\n                })\n            .ConfigureAwait(false);\n\n        return response.AccessToken;\n    }\n\n    private OidcClient GetClient()\n    {\n        var options = new OidcClientOptions\n        {\n            Authority = _settingsService.IdentityEndpointBase,\n            ClientId = _settingsService.ClientId,\n            ClientSecret = _settingsService.ClientSecret,\n            Scope = \"openid profile basket orders offline_access\",\n            RedirectUri = _settingsService.CallbackUri,\n            PostLogoutRedirectUri = _settingsService.CallbackUri,\n            Browser = _browser,\n        };\n\n        if (_httpMessageHandler is not null)\n        {\n            options.BackchannelHandler = _httpMessageHandler;\n        }\n        \n        return new OidcClient(options);\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Location/ILocationService.cs",
    "content": "﻿namespace eShop.ClientApp.Services.Location;\n\npublic interface ILocationService\n{\n    Task UpdateUserLocation(Models.Location.Location newLocReq);\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Location/LocationService.cs",
    "content": "using eShop.ClientApp.Services.Identity;\nusing eShop.ClientApp.Services.RequestProvider;\nusing eShop.ClientApp.Services.Settings;\n\nnamespace eShop.ClientApp.Services.Location;\n\npublic class LocationService : ILocationService\n{\n    private const string ApiUrlBase = \"l/api/v1/locations\";\n    private readonly IIdentityService _identityService;\n\n    public LocationService(IIdentityService identityService)\n    {\n        _identityService = identityService;\n    }\n\n    public async Task UpdateUserLocation(Models.Location.Location newLocReq)\n    {\n        var accessToken = await _identityService.GetAuthTokenAsync().ConfigureAwait(false);\n\n        if (string.IsNullOrEmpty(accessToken))\n        {\n            return;\n        }\n\n        //TODO: Determine mapped location\n        await Task.Delay(10).ConfigureAwait(false);\n        //await _requestProvider.PostAsync(uri, newLocReq, token).ConfigureAwait(false);\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Navigation/INavigationService.cs",
    "content": "﻿namespace eShop.ClientApp.Services;\n\npublic interface INavigationService\n{\n    Task InitializeAsync();\n\n    Task NavigateToAsync(string route, IDictionary<string, object> routeParameters = null);\n\n    Task PopAsync();\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Navigation/MauiNavigationService.cs",
    "content": "using eShop.ClientApp.Models.User;\nusing eShop.ClientApp.Services.AppEnvironment;\n\nnamespace eShop.ClientApp.Services;\n\npublic class MauiNavigationService : INavigationService\n{\n    private readonly IAppEnvironmentService _appEnvironmentService;\n\n    public MauiNavigationService(IAppEnvironmentService appEnvironmentService)\n    {\n        _appEnvironmentService = appEnvironmentService;\n    }\n\n    public async Task InitializeAsync()\n    {\n        var user = await _appEnvironmentService.IdentityService.GetUserInfoAsync();\n\n        await NavigateToAsync(user == UserInfo.Default ? \"//Login\" : \"//Main/Catalog\");\n    }\n\n    public Task NavigateToAsync(string route, IDictionary<string, object> routeParameters = null)\n    {\n        var shellNavigation = new ShellNavigationState(route);\n\n        return routeParameters != null\n            ? Shell.Current.GoToAsync(shellNavigation, routeParameters)\n            : Shell.Current.GoToAsync(shellNavigation);\n    }\n\n    public Task PopAsync()\n    {\n        return Shell.Current.GoToAsync(\"..\");\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Services/OpenUrl/IOpenUrlService.cs",
    "content": "﻿namespace eShop.ClientApp.Services.OpenUrl;\n\npublic interface IOpenUrlService\n{\n    Task OpenUrl(string url);\n}\n"
  },
  {
    "path": "src/ClientApp/Services/OpenUrl/OpenUrlService.cs",
    "content": "﻿namespace eShop.ClientApp.Services.OpenUrl;\n\npublic class OpenUrlService : IOpenUrlService\n{\n    public async Task OpenUrl(string url)\n    {\n        if (await Launcher.CanOpenAsync(url))\n        {\n            await Launcher.OpenAsync(url);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Order/IOrderService.cs",
    "content": "using eShop.ClientApp.Models.Basket;\n\nnamespace eShop.ClientApp.Services.Order;\n\npublic interface IOrderService\n{\n    Task CreateOrderAsync(Models.Orders.Order newOrder);\n\n    Task<IEnumerable<Models.Orders.Order>> GetOrdersAsync();\n\n    Task<Models.Orders.Order> GetOrderAsync(int orderId);\n\n    Task<bool> CancelOrderAsync(int orderId);\n\n    OrderCheckout MapOrderToBasket(Models.Orders.Order order);\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Order/OrderMockService.cs",
    "content": "using eShop.ClientApp.Models.Basket;\nusing eShop.ClientApp.Models.Orders;\nusing eShop.ClientApp.Models.User;\n\nnamespace eShop.ClientApp.Services.Order;\n\npublic class OrderMockService : IOrderService\n{\n    private static readonly DateTime MockExpirationDate = DateTime.Now.AddYears(5);\n\n    private static readonly Address MockAdress = new()\n    {\n        Id = Guid.NewGuid(),\n        City = \"Seattle, WA\",\n        Street = \"120 E 87th Street\",\n        CountryCode = \"98122\",\n        Country = \"United States\",\n        Latitude = 40.785091,\n        Longitude = -73.968285,\n        State = \"Seattle\",\n        StateCode = \"WA\",\n        ZipCode = \"98101\"\n    };\n\n    private static readonly PaymentInfo MockPaymentInfo = new()\n    {\n        Id = Guid.NewGuid(),\n        CardHolderName = \"American Express\",\n        CardNumber = \"XXXXXXXXXXXX0005\",\n        CardType = new CardType\n        {\n            Id = 3,\n            Name = \"MasterCard\"\n        },\n        Expiration = MockExpirationDate.ToString(),\n        ExpirationMonth = MockExpirationDate.Month,\n        ExpirationYear = MockExpirationDate.Year,\n        SecurityNumber = \"123\"\n    };\n\n    private static readonly List<OrderItem> MockOrderItems = new()\n    {\n        new OrderItem\n        {\n            OrderId = Guid.NewGuid(),\n            ProductId = Common.Common.MockCatalogItemId01,\n            Discount = 15,\n            ProductName = \".NET Bot Blue Sweatshirt (M)\",\n            Quantity = 1,\n            UnitPrice = 16.50M,\n            PictureUrl = \"fake_product_01.png\"\n        },\n        new OrderItem\n        {\n            OrderId = Guid.NewGuid(),\n            ProductId = Common.Common.MockCatalogItemId03,\n            Discount = 0,\n            ProductName = \".NET Bot Black Sweatshirt (M)\",\n            Quantity = 2,\n            UnitPrice = 19.95M,\n            PictureUrl = \"fake_product_03.png\"\n        }\n    };\n\n    private static readonly OrderCheckout MockOrderCheckout = new()\n    {\n        CardExpiration = DateTime.UtcNow,\n        CardHolderName = \"FakeCardHolderName\",\n        CardNumber = \"XXXXXXXXXXXX3224\",\n        CardSecurityNumber = \"1234\",\n        CardTypeId = 1,\n        City = \"FakeCity\",\n        Country = \"FakeCountry\",\n        ZipCode = \"FakeZipCode\",\n        Street = \"FakeStreet\"\n    };\n\n    private readonly List<Models.Orders.Order> MockOrders = new()\n    {\n        new Models.Orders.Order\n        {\n            OrderNumber = 1,\n            SequenceNumber = 123,\n            OrderDate = DateTime.Now,\n            OrderStatus = \"Submitted\",\n            OrderItems = MockOrderItems,\n            CardTypeId = MockPaymentInfo.CardType.Id,\n            CardHolderName = MockPaymentInfo.CardHolderName,\n            CardNumber = MockPaymentInfo.CardNumber,\n            CardSecurityNumber = MockPaymentInfo.SecurityNumber,\n            CardExpiration = new DateTime(MockPaymentInfo.ExpirationYear, MockPaymentInfo.ExpirationMonth, 1),\n            ShippingCity = MockAdress.City,\n            ShippingState = MockAdress.State,\n            ShippingCountry = MockAdress.Country,\n            ShippingStreet = MockAdress.Street,\n            Total = 36.46M\n        },\n        new Models.Orders.Order\n        {\n            OrderNumber = 2,\n            SequenceNumber = 132,\n            OrderDate = DateTime.Now,\n            OrderStatus = \"Paid\",\n            OrderItems = MockOrderItems,\n            CardTypeId = MockPaymentInfo.CardType.Id,\n            CardHolderName = MockPaymentInfo.CardHolderName,\n            CardNumber = MockPaymentInfo.CardNumber,\n            CardSecurityNumber = MockPaymentInfo.SecurityNumber,\n            CardExpiration = new DateTime(MockPaymentInfo.ExpirationYear, MockPaymentInfo.ExpirationMonth, 1),\n            ShippingCity = MockAdress.City,\n            ShippingState = MockAdress.State,\n            ShippingCountry = MockAdress.Country,\n            ShippingStreet = MockAdress.Street,\n            Total = 36.46M\n        },\n        new Models.Orders.Order\n        {\n            OrderNumber = 3,\n            SequenceNumber = 231,\n            OrderDate = DateTime.Now,\n            OrderStatus = \"Cancelled\",\n            OrderItems = MockOrderItems,\n            CardTypeId = MockPaymentInfo.CardType.Id,\n            CardHolderName = MockPaymentInfo.CardHolderName,\n            CardNumber = MockPaymentInfo.CardNumber,\n            CardSecurityNumber = MockPaymentInfo.SecurityNumber,\n            CardExpiration = new DateTime(MockPaymentInfo.ExpirationYear, MockPaymentInfo.ExpirationMonth, 1),\n            ShippingCity = MockAdress.City,\n            ShippingState = MockAdress.State,\n            ShippingCountry = MockAdress.Country,\n            ShippingStreet = MockAdress.Street,\n            Total = 36.46M\n        },\n        new Models.Orders.Order\n        {\n            OrderNumber = 4,\n            SequenceNumber = 131,\n            OrderDate = DateTime.Now,\n            OrderStatus = \"Shipped\",\n            OrderItems = MockOrderItems,\n            CardTypeId = MockPaymentInfo.CardType.Id,\n            CardHolderName = MockPaymentInfo.CardHolderName,\n            CardNumber = MockPaymentInfo.CardNumber,\n            CardSecurityNumber = MockPaymentInfo.SecurityNumber,\n            CardExpiration = new DateTime(MockPaymentInfo.ExpirationYear, MockPaymentInfo.ExpirationMonth, 1),\n            ShippingCity = MockAdress.City,\n            ShippingState = MockAdress.State,\n            ShippingCountry = MockAdress.Country,\n            ShippingStreet = MockAdress.Street,\n            Total = 36.46M\n        }\n    };\n\n    public async Task<IEnumerable<Models.Orders.Order>> GetOrdersAsync()\n    {\n        await Task.Delay(10);\n\n        return MockOrders\n            .OrderByDescending(o => o.OrderNumber)\n            .ToArray();\n    }\n\n    public async Task<Models.Orders.Order> GetOrderAsync(int orderId)\n    {\n        await Task.Delay(10);\n\n        return MockOrders\n            .FirstOrDefault(o => o.OrderNumber.Equals(orderId));\n    }\n\n    public async Task CreateOrderAsync(Models.Orders.Order newOrder)\n    {\n        await Task.Delay(10);\n\n        MockOrders.Add(newOrder);\n    }\n\n    public OrderCheckout MapOrderToBasket(Models.Orders.Order order)\n    {\n        return MockOrderCheckout;\n    }\n\n    public Task<bool> CancelOrderAsync(int orderId)\n    {\n        return Task.FromResult(true);\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Order/OrderService.cs",
    "content": "using System.Net;\nusing eShop.ClientApp.Helpers;\nusing eShop.ClientApp.Models.Basket;\nusing eShop.ClientApp.Models.Orders;\nusing eShop.ClientApp.Services.Identity;\nusing eShop.ClientApp.Services.RequestProvider;\nusing eShop.ClientApp.Services.Settings;\n\nnamespace eShop.ClientApp.Services.Order;\n\npublic class OrderService : IOrderService\n{\n    private const string ApiUrlBase = \"api/orders\";\n    private const string ApiVersion = \"api-version=1.0\";\n    \n    private readonly IIdentityService _identityService;\n    private readonly IRequestProvider _requestProvider;\n    private readonly ISettingsService _settingsService;\n\n    public OrderService(IIdentityService identityService, ISettingsService settingsService,\n        IRequestProvider requestProvider)\n    {\n        _identityService = identityService;\n        _settingsService = settingsService;\n        _requestProvider = requestProvider;\n    }\n\n    public async Task CreateOrderAsync(Models.Orders.Order newOrder)\n    {\n        var authToken = await _identityService.GetAuthTokenAsync().ConfigureAwait(false);\n\n        if (string.IsNullOrEmpty(authToken))\n        {\n            return;\n        }\n\n        var uri = $\"{UriHelper.CombineUri(_settingsService.GatewayOrdersEndpointBase, ApiUrlBase)}?{ApiVersion}\";\n\n        var success = await _requestProvider.PostAsync(uri, newOrder, authToken, \"x-requestid\").ConfigureAwait(false);\n    }\n\n    public async Task<IEnumerable<Models.Orders.Order>> GetOrdersAsync()\n    {\n        var authToken = await _identityService.GetAuthTokenAsync().ConfigureAwait(false);\n\n        if (string.IsNullOrEmpty(authToken))\n        {\n            return Enumerable.Empty<Models.Orders.Order>();\n        }\n\n        var uri = $\"{UriHelper.CombineUri(_settingsService.GatewayOrdersEndpointBase, ApiUrlBase)}?{ApiVersion}\";\n\n        var orders =\n            await _requestProvider.GetAsync<IEnumerable<Models.Orders.Order>>(uri, authToken).ConfigureAwait(false);\n\n        return orders ?? Enumerable.Empty<Models.Orders.Order>();\n    }\n\n    public async Task<Models.Orders.Order> GetOrderAsync(int orderId)\n    {\n        var authToken = await _identityService.GetAuthTokenAsync().ConfigureAwait(false);\n\n        if (string.IsNullOrEmpty(authToken))\n        {\n            return new Models.Orders.Order();\n        }\n\n        try\n        {\n            var uri = $\"{UriHelper.CombineUri(_settingsService.GatewayOrdersEndpointBase, $\"{ApiUrlBase}/{orderId}\")}?{ApiVersion}\";\n\n            var order =\n                await _requestProvider.GetAsync<Models.Orders.Order>(uri, authToken).ConfigureAwait(false);\n\n            return order;\n        }\n        catch\n        {\n            return new Models.Orders.Order();\n        }\n    }\n\n    public async Task<bool> CancelOrderAsync(int orderId)\n    {\n        var authToken = await _identityService.GetAuthTokenAsync().ConfigureAwait(false);\n\n        if (string.IsNullOrEmpty(authToken))\n        {\n            return false;\n        }\n\n        var uri = $\"{UriHelper.CombineUri(_settingsService.GatewayOrdersEndpointBase, $\"{ApiUrlBase}/cancel\")}?{ApiVersion}\";\n\n        var cancelOrderCommand = new CancelOrderCommand(orderId);\n\n        var header = \"x-requestid\";\n\n        try\n        {\n            await _requestProvider.PutAsync(uri, cancelOrderCommand, authToken, header).ConfigureAwait(false);\n        }\n        //If the status of the order has changed before to click cancel button, we will get\n        //a BadRequest HttpStatus\n        catch (HttpRequestExceptionEx ex) when (ex.HttpCode == HttpStatusCode.BadRequest)\n        {\n            return false;\n        }\n\n        return true;\n    }\n\n    public OrderCheckout MapOrderToBasket(Models.Orders.Order order)\n    {\n        return new OrderCheckout\n        {\n            CardExpiration = order.CardExpiration,\n            CardHolderName = order.CardHolderName,\n            CardNumber = order.CardNumber,\n            CardSecurityNumber = order.CardSecurityNumber,\n            CardTypeId = order.CardTypeId,\n            City = order.ShippingCity,\n            State = order.ShippingState,\n            Country = order.ShippingCountry,\n            ZipCode = order.ShippingZipCode,\n            Street = order.ShippingStreet\n        };\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Services/RequestProvider/HttpRequestExceptionEx.cs",
    "content": "﻿using System.Net;\n\nnamespace eShop.ClientApp.Services.RequestProvider;\n\npublic class HttpRequestExceptionEx : HttpRequestException\n{\n    public HttpRequestExceptionEx(HttpStatusCode code) : this(code, null, null)\n    {\n    }\n\n    public HttpRequestExceptionEx(HttpStatusCode code, string message) : this(code, message, null)\n    {\n    }\n\n    public HttpRequestExceptionEx(HttpStatusCode code, string message, Exception inner) : base(message, inner)\n    {\n        HttpCode = code;\n    }\n\n    public HttpStatusCode HttpCode { get; }\n}\n"
  },
  {
    "path": "src/ClientApp/Services/RequestProvider/IRequestProvider.cs",
    "content": "namespace eShop.ClientApp.Services.RequestProvider;\n\npublic interface IRequestProvider\n{\n    Task<TResult> GetAsync<TResult>(string uri, string token = \"\");\n    \n    Task<TResponse> PostAsync<TRequest, TResponse>(string uri, TRequest data, string token = \"\", string header = \"\");\n    \n    Task<bool> PostAsync<TRequest>(string uri, TRequest data, string token = \"\", string header = \"\");\n\n    Task<TResult> PostAsync<TResult>(string uri, string data, string clientId, string clientSecret);\n\n    Task<TResult> PutAsync<TResult>(string uri, TResult data, string token = \"\", string header = \"\");\n\n    Task DeleteAsync(string uri, string token = \"\");\n}\n"
  },
  {
    "path": "src/ClientApp/Services/RequestProvider/RequestProvider.cs",
    "content": "#nullable enable\nusing System.Net;\nusing System.Net.Http.Headers;\nusing System.Net.Http.Json;\nusing System.Text.Json;\nusing eShop.ClientApp.Exceptions;\n\nnamespace eShop.ClientApp.Services.RequestProvider;\n\npublic class RequestProvider(HttpMessageHandler _messageHandler) : IRequestProvider\n{\n    private readonly Lazy<HttpClient> _httpClient =\n        new(() =>\n            {\n                var httpClient = _messageHandler is not null ? new HttpClient(_messageHandler) : new HttpClient();\n                httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(\"application/json\"));\n                return httpClient;\n            },\n            LazyThreadSafetyMode.ExecutionAndPublication);\n\n    public async Task<TResult?> GetAsync<TResult>(string uri, string token = \"\")\n    {\n        var httpClient = GetOrCreateHttpClient(token);\n        using var response = await httpClient.GetAsync(uri).ConfigureAwait(false);\n\n        await HandleResponse(response).ConfigureAwait(false);\n\n        var result = await ReadFromJsonAsync<TResult>(response.Content).ConfigureAwait(false);\n\n        return result;\n    }\n\n    public async Task<TResult?> PostAsync<TRequest, TResult>(string uri, TRequest data, string token = \"\", string header = \"\")\n    {\n        var httpClient = GetOrCreateHttpClient(token);\n\n        if (!string.IsNullOrEmpty(header))\n        {\n            AddHeaderParameter(httpClient, header);\n        }\n\n        var requestContent = SerializeToJson(data);\n        using HttpResponseMessage response = await httpClient.PostAsync(uri, requestContent).ConfigureAwait(false);\n\n        await HandleResponse(response).ConfigureAwait(false);\n        var result = await ReadFromJsonAsync<TResult>(response.Content).ConfigureAwait(false);\n\n        return result;\n    }\n\n    public async Task<bool> PostAsync<TRequest>(string uri, TRequest data, string token = \"\", string header = \"\")\n    {\n        var httpClient = GetOrCreateHttpClient(token);\n\n        if (!string.IsNullOrEmpty(header))\n        {\n            AddHeaderParameter(httpClient, header);\n        }\n\n        var requestContent = SerializeToJson(data);\n        using var response = await httpClient.PostAsync(uri, requestContent).ConfigureAwait(false);\n\n        await HandleResponse(response).ConfigureAwait(false);\n\n        return response.IsSuccessStatusCode;\n    }\n\n    public async Task<TResult?> PostAsync<TResult>(string uri, string data, string clientId, string clientSecret)\n       \n    {\n        var httpClient = GetOrCreateHttpClient(string.Empty);\n\n        if (!string.IsNullOrWhiteSpace(clientId) && !string.IsNullOrWhiteSpace(clientSecret))\n        {\n            AddBasicAuthenticationHeader(httpClient, clientId, clientSecret);\n        }\n\n        using var content = new StringContent(data);\n        content.Headers.ContentType = new MediaTypeHeaderValue(\"application/x-www-form-urlencoded\");\n        using var response = await httpClient.PostAsync(uri, content).ConfigureAwait(false);\n\n        await HandleResponse(response).ConfigureAwait(false);\n        var result = await ReadFromJsonAsync<TResult>(response.Content).ConfigureAwait(false);\n\n        return result;\n    }\n\n    public async Task<TResult?> PutAsync<TResult>(string uri, TResult data, string token = \"\", string header = \"\")\n    {\n        var httpClient = GetOrCreateHttpClient(token);\n\n        if (!string.IsNullOrEmpty(header))\n        {\n            AddHeaderParameter(httpClient, header);\n        }\n\n        var requestContent = SerializeToJson(data);\n        using HttpResponseMessage response = await httpClient.PutAsync(uri, requestContent).ConfigureAwait(false);\n\n        await HandleResponse(response).ConfigureAwait(false);\n        var result = await ReadFromJsonAsync<TResult>(response.Content).ConfigureAwait(false);\n\n        return result;\n    }\n\n    public async Task DeleteAsync(string uri, string token = \"\")\n    {\n        var httpClient = GetOrCreateHttpClient(token);\n        await httpClient.DeleteAsync(uri).ConfigureAwait(false);\n    }\n\n    private HttpClient GetOrCreateHttpClient(string token = \"\")\n    {\n        var httpClient = _httpClient.Value;\n\n        httpClient.DefaultRequestHeaders.Authorization =\n            !string.IsNullOrEmpty(token)\n                ? new AuthenticationHeaderValue(\"Bearer\", token)\n                : null;\n\n        return httpClient;\n    }\n\n    private static void AddHeaderParameter(HttpClient httpClient, string parameter)\n    {\n        if (httpClient == null)\n        {\n            return;\n        }\n\n        if (string.IsNullOrEmpty(parameter))\n        {\n            return;\n        }\n\n        httpClient.DefaultRequestHeaders.Add(parameter, Guid.NewGuid().ToString());\n    }\n\n    private static void AddBasicAuthenticationHeader(HttpClient httpClient, string clientId, string clientSecret)\n    {\n        if (httpClient == null)\n        {\n            return;\n        }\n\n        if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(clientSecret))\n        {\n            return;\n        }\n\n        httpClient.DefaultRequestHeaders.Authorization = new BasicAuthenticationHeaderValue(clientId, clientSecret);\n    }\n\n    private static async Task HandleResponse(HttpResponseMessage response)\n    {\n        if (!response.IsSuccessStatusCode)\n        {\n            var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);\n\n            if (response.StatusCode == HttpStatusCode.Forbidden || response.StatusCode == HttpStatusCode.Unauthorized)\n            {\n                throw new ServiceAuthenticationException(content);\n            }\n\n            throw new HttpRequestExceptionEx(response.StatusCode, content);\n        }\n    }\n\n    private static async Task<T?> ReadFromJsonAsync<T>(HttpContent content)\n    {\n        using var contentStream = await content.ReadAsStreamAsync().ConfigureAwait(false);\n        var data = await JsonSerializer.DeserializeAsync(contentStream, typeof(T), EShopJsonSerializerContext.Default).ConfigureAwait(false);\n        return (T?)data;\n    }\n\n    private static JsonContent SerializeToJson<T>(T data)\n    {\n        var typeInfo = EShopJsonSerializerContext.Default.GetTypeInfo(typeof(T)) ?? throw new InvalidOperationException($\"Missing type info for {typeof(T)}\");\n        return JsonContent.Create(data, typeInfo);\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Settings/ISettingsService.cs",
    "content": "#nullable enable\nusing eShop.ClientApp.Models.Token;\n\nnamespace eShop.ClientApp.Services.Settings;\n\npublic interface ISettingsService\n{\n    bool UseMocks { get; set; }\n\n    string DefaultEndpoint { get; set; }\n\n    string RegistrationEndpoint { get; set; }\n\n    string ClientId { get; set; }\n\n    string ClientSecret { get; set; }\n\n    string CallbackUri { get; set; }\n\n    string IdentityEndpointBase { get; set; }\n\n    string GatewayCatalogEndpointBase { get; set; }\n\n    string GatewayOrdersEndpointBase { get; set; }\n\n    string GatewayBasketEndpointBase { get; set; }\n\n    bool UseFakeLocation { get; set; }\n\n    string Latitude { get; set; }\n\n    string Longitude { get; set; }\n\n    bool AllowGpsLocation { get; set; }\n    Task<UserToken?> GetUserTokenAsync();\n\n    Task SetUserTokenAsync(UserToken? userToken);\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Settings/SettingsService.cs",
    "content": "using System.Globalization;\nusing System.Text.Json;\nusing eShop.ClientApp.Models.Token;\n\nnamespace eShop.ClientApp.Services.Settings;\n\npublic class SettingsService : ISettingsService\n{\n    #region Setting Constants\n\n    private const string UserAccessToken = \"user_token\";\n    private const string IdUseMocks = \"use_mocks\";\n    private const string IdIdentityBase = \"url_base\";\n    private const string DefaultClientId = \"maui\";\n    private const string DefaultClientSecret = \"secret\";\n    private const string DefaultCallbackUri = \"maui://authcallback\";\n    private const string IdGatewayMarketingBase = \"url_marketing\";\n    private const string IdGatewayShoppingBase = \"url_shopping\";\n    private const string IdGatewayOrdersBase = \"url_orders\";\n    private const string IdGatewayBasketBase = \"url_basket\";\n    private const string IdUseFakeLocation = \"use_fake_location\";\n    private const string IdLatitude = \"latitude\";\n    private const string IdLongitude = \"longitude\";\n    private const string IdAllowGpsLocation = \"allow_gps_location\";\n    private readonly bool UseMocksDefault = true;\n    private readonly bool UseFakeLocationDefault = false;\n    private readonly bool AllowGpsLocationDefault = false;\n    private readonly double FakeLatitudeDefault = 47.604610d;\n    private readonly double FakeLongitudeDefault = -122.315752d;\n\n    #endregion\n\n    #region Settings Properties\n\n    public async Task SetUserTokenAsync(UserToken userToken)\n    {\n        await SecureStorage\n            .SetAsync(UserAccessToken, userToken is not null ? JsonSerializer.Serialize(userToken, EShopJsonSerializerContext.Default.UserToken) : string.Empty)\n            .ConfigureAwait(false);\n    }\n\n    public async Task<UserToken> GetUserTokenAsync()\n    {\n        var userToken = await SecureStorage.GetAsync(UserAccessToken).ConfigureAwait(false);\n\n        return string.IsNullOrEmpty(userToken) ? default : JsonSerializer.Deserialize(userToken, EShopJsonSerializerContext.Default.UserToken);\n    }\n\n    public bool UseMocks\n    {\n        get => Preferences.Get(IdUseMocks, UseMocksDefault);\n        set => Preferences.Set(IdUseMocks, value);\n    }\n\n    public string DefaultEndpoint\n    {\n        get => Preferences.Get(nameof(DefaultEndpoint), string.Empty);\n        set => Preferences.Set(nameof(DefaultEndpoint), value);\n    }\n\n    public string RegistrationEndpoint\n    {\n        get => Preferences.Get(nameof(RegistrationEndpoint), string.Empty);\n        set => Preferences.Set(nameof(RegistrationEndpoint), value);\n    }\n\n    public string AuthorizeEndpoint\n    {\n        get => Preferences.Get(nameof(AuthorizeEndpoint), string.Empty);\n        set => Preferences.Set(nameof(AuthorizeEndpoint), value);\n    }\n\n    public string UserInfoEndpoint\n    {\n        get => Preferences.Get(nameof(UserInfoEndpoint), string.Empty);\n        set => Preferences.Set(nameof(UserInfoEndpoint), value);\n    }\n\n    public string ClientId\n    {\n        get => Preferences.Get(nameof(ClientId), DefaultClientId);\n        set => Preferences.Set(nameof(ClientId), value);\n    }\n\n    public string ClientSecret\n    {\n        get => Preferences.Get(nameof(ClientSecret), DefaultClientSecret);\n        set => Preferences.Set(nameof(ClientSecret), value);\n    }\n\n    public string CallbackUri\n    {\n        get => Preferences.Get(nameof(CallbackUri), DefaultCallbackUri);\n        set => Preferences.Set(nameof(CallbackUri), value);\n    }\n\n    public string IdentityEndpointBase\n    {\n        get => Preferences.Get(IdIdentityBase, string.Empty);\n        set => Preferences.Set(IdIdentityBase, value);\n    }\n\n    public string GatewayCatalogEndpointBase\n    {\n        get => Preferences.Get(IdGatewayShoppingBase, string.Empty);\n        set => Preferences.Set(IdGatewayShoppingBase, value);\n    }\n\n    public string GatewayMarketingEndpointBase\n    {\n        get => Preferences.Get(IdGatewayMarketingBase, string.Empty);\n        set => Preferences.Set(IdGatewayMarketingBase, value);\n    }\n    \n    public string GatewayOrdersEndpointBase\n    {\n        get => Preferences.Get(IdGatewayOrdersBase, string.Empty);\n        set => Preferences.Set(IdGatewayOrdersBase, value);\n    }\n\n    public string GatewayBasketEndpointBase\n    {\n        get => Preferences.Get(IdGatewayBasketBase, string.Empty);\n        set => Preferences.Set(IdGatewayBasketBase, value);\n    }\n\n    public bool UseFakeLocation\n    {\n        get => Preferences.Get(IdUseFakeLocation, UseFakeLocationDefault);\n        set => Preferences.Set(IdUseFakeLocation, value);\n    }\n\n    public string Latitude\n    {\n        get => Preferences.Get(IdLatitude, FakeLatitudeDefault.ToString(CultureInfo.InvariantCulture));\n        set => Preferences.Set(IdLatitude, value);\n    }\n\n    public string Longitude\n    {\n        get => Preferences.Get(IdLongitude, FakeLongitudeDefault.ToString(CultureInfo.InvariantCulture));\n        set => Preferences.Set(IdLongitude, value);\n    }\n\n    public bool AllowGpsLocation\n    {\n        get => Preferences.Get(IdAllowGpsLocation, AllowGpsLocationDefault);\n        set => Preferences.Set(IdAllowGpsLocation, value);\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Theme/ITheme.cs",
    "content": "﻿namespace eShop.ClientApp.Services.Theme;\n\npublic interface ITheme\n{\n    void SetStatusBarColor(Color color, bool darkStatusBarTint);\n}\n"
  },
  {
    "path": "src/ClientApp/Services/Theme/Theme.shared.cs",
    "content": "﻿namespace eShop.ClientApp.Services.Theme;\n\npublic class Theme : ITheme\n{\n    public void SetStatusBarColor(Color color, bool darkStatusBarTint)\n    {\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Triggers/BeginAnimation.cs",
    "content": "﻿using eShop.ClientApp.Animations.Base;\n\nnamespace eShop.ClientApp.Triggers;\n\npublic class BeginAnimation : TriggerAction<VisualElement>\n{\n    public AnimationBase Animation { get; set; }\n\n    protected override async void Invoke(VisualElement sender)\n    {\n        if (Animation != null)\n        {\n            await Animation.Begin();\n        }\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Validations/IValidationRule.cs",
    "content": "﻿namespace eShop.ClientApp.Validations;\n\npublic interface IValidationRule<T>\n{\n    string ValidationMessage { get; set; }\n\n    bool Check(T value);\n}\n"
  },
  {
    "path": "src/ClientApp/Validations/IValidity.cs",
    "content": "﻿namespace eShop.ClientApp.Validations;\n\npublic interface IValidity\n{\n    bool IsValid { get; }\n}\n"
  },
  {
    "path": "src/ClientApp/Validations/IsNotNullOrEmptyRule.cs",
    "content": "﻿namespace eShop.ClientApp.Validations;\n\npublic class IsNotNullOrEmptyRule<T> : IValidationRule<T>\n{\n    public string ValidationMessage { get; set; }\n\n    public bool Check(T value)\n    {\n        return value is string str &&\n               !string.IsNullOrWhiteSpace(str);\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Validations/ValidatableObject.cs",
    "content": "﻿namespace eShop.ClientApp.Validations;\n\npublic class ValidatableObject<T> : ObservableObject, IValidity\n{\n    private IEnumerable<string> _errors;\n    private bool _isValid;\n    private T _value;\n\n    public ValidatableObject()\n    {\n        _isValid = true;\n        _errors = Enumerable.Empty<string>();\n    }\n\n    public List<IValidationRule<T>> Validations { get; } = new();\n\n    public IEnumerable<string> Errors\n    {\n        get => _errors;\n        private set => SetProperty(ref _errors, value);\n    }\n\n    public T Value\n    {\n        get => _value;\n        set => SetProperty(ref _value, value);\n    }\n\n    public bool IsValid\n    {\n        get => _isValid;\n        private set => SetProperty(ref _isValid, value);\n    }\n\n    public bool Validate()\n    {\n        Errors = Validations\n                     ?.Where(v => !v.Check(Value))\n                     ?.Select(v => v.ValidationMessage)\n                     ?.ToArray()\n                 ?? Enumerable.Empty<string>();\n\n        IsValid = !Errors.Any();\n\n        return IsValid;\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/ViewModels/Base/IViewModelBase.cs",
    "content": "﻿using eShop.ClientApp.Services;\n\nnamespace eShop.ClientApp.ViewModels.Base;\n\npublic interface IViewModelBase : IQueryAttributable\n{\n    public INavigationService NavigationService { get; }\n\n    public IAsyncRelayCommand InitializeAsyncCommand { get; }\n\n    public bool IsBusy { get; }\n\n    public bool IsInitialized { get; }\n\n    Task InitializeAsync();\n}\n"
  },
  {
    "path": "src/ClientApp/ViewModels/Base/ViewModelBase.cs",
    "content": "﻿using eShop.ClientApp.Services;\n\nnamespace eShop.ClientApp.ViewModels.Base;\n\npublic abstract partial class ViewModelBase : ObservableObject, IViewModelBase\n{\n    private long _isBusy;\n\n    [ObservableProperty] private bool _isInitialized;\n\n    public ViewModelBase(INavigationService navigationService)\n    {\n        NavigationService = navigationService;\n\n        InitializeAsyncCommand =\n            new AsyncRelayCommand(\n                async () =>\n                {\n                    await IsBusyFor(InitializeAsync);\n                    IsInitialized = true;\n                },\n                AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler);\n    }\n\n    public bool IsBusy => Interlocked.Read(ref _isBusy) > 0;\n\n    public INavigationService NavigationService { get; }\n\n    public IAsyncRelayCommand InitializeAsyncCommand { get; }\n\n    public virtual void ApplyQueryAttributes(IDictionary<string, object> query)\n    {\n    }\n\n    public virtual Task InitializeAsync()\n    {\n        return Task.CompletedTask;\n    }\n\n    protected async Task IsBusyFor(Func<Task> unitOfWork)\n    {\n        Interlocked.Increment(ref _isBusy);\n        OnPropertyChanged(nameof(IsBusy));\n\n        try\n        {\n            await unitOfWork();\n        }\n        finally\n        {\n            Interlocked.Decrement(ref _isBusy);\n            OnPropertyChanged(nameof(IsBusy));\n        }\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/ViewModels/BasketViewModel.cs",
    "content": "using eShop.ClientApp.Models.Basket;\nusing eShop.ClientApp.Services;\nusing eShop.ClientApp.Services.AppEnvironment;\nusing eShop.ClientApp.Services.Settings;\nusing eShop.ClientApp.ViewModels.Base;\n\nnamespace eShop.ClientApp.ViewModels;\n\npublic partial class BasketViewModel : ViewModelBase\n{\n    private readonly IAppEnvironmentService _appEnvironmentService;\n    private readonly ObservableCollectionEx<BasketItem> _basketItems = new();\n    private readonly ISettingsService _settingsService;\n\n    public BasketViewModel(\n        IAppEnvironmentService appEnvironmentService,\n        INavigationService navigationService, ISettingsService settingsService)\n        : base(navigationService)\n    {\n        _appEnvironmentService = appEnvironmentService;\n        _settingsService = settingsService;\n    }\n\n    public int BadgeCount => _basketItems?.Sum(basketItem => basketItem.Quantity) ?? 0;\n\n    public decimal Total => _basketItems?.Sum(basketItem => basketItem.Quantity * basketItem.UnitPrice) ?? 0m;\n\n    public IReadOnlyList<BasketItem> BasketItems => _basketItems;\n\n    public override async Task InitializeAsync()\n    {\n        // Update Basket\n        var basket = await _appEnvironmentService.BasketService.GetBasketAsync();\n\n        if ((basket?.Items?.Count ?? 0) > 0)\n        {\n            await _basketItems.ReloadDataAsync(\n                async innerList =>\n                {\n                    foreach (var basketItem in basket.Items.ToArray())\n                    {\n                        var catalogItem =\n                            await _appEnvironmentService.CatalogService.GetCatalogItemAsync(basketItem.ProductId);\n                        basketItem.PictureUrl = catalogItem.PictureUri;\n                        basketItem.ProductName = catalogItem.Name;\n                        basketItem.UnitPrice = catalogItem.Price;\n                        await AddBasketItemAsync(basketItem, innerList);\n                    }\n                });\n        }\n    }\n\n    [RelayCommand]\n    private Task AddAsync(BasketItem item)\n    {\n        return AddBasketItemAsync(item, _basketItems);\n    }\n\n    private async Task AddBasketItemAsync(BasketItem item, IList<BasketItem> basketItems)\n    {\n        basketItems.Add(item);\n\n        var basket = await _appEnvironmentService.BasketService.GetBasketAsync();\n\n        if (basket != null)\n        {\n            basket.AddItemToBasket(item);\n            await _appEnvironmentService.BasketService.UpdateBasketAsync(basket);\n        }\n\n        ReCalculateTotal();\n    }\n\n    [RelayCommand]\n    private async Task DeleteAsync(BasketItem item)\n    {\n        _basketItems.Remove(item);\n\n        var basket = await _appEnvironmentService.BasketService.GetBasketAsync();\n        if (basket != null)\n        {\n            basket.RemoveItemFromBasket(item);\n            await _appEnvironmentService.BasketService.UpdateBasketAsync(basket);\n        }\n\n        ReCalculateTotal();\n    }\n\n    public async Task ClearBasketItems()\n    {\n        _basketItems.Clear();\n        \n        await _appEnvironmentService.BasketService.ClearBasketAsync();\n\n        ReCalculateTotal();\n    }\n\n    private void ReCalculateTotal()\n    {\n        OnPropertyChanged(nameof(BadgeCount));\n        OnPropertyChanged(nameof(Total));\n    }\n\n    [RelayCommand]\n    private async Task CheckoutAsync()\n    {\n        if (_basketItems?.Any() ?? false)\n        {\n            _appEnvironmentService.BasketService.LocalBasketItems = _basketItems;\n            await NavigationService.NavigateToAsync(\"Checkout\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/ViewModels/CatalogItemViewModel.cs",
    "content": "﻿using CommunityToolkit.Mvvm.Messaging;\nusing eShop.ClientApp.Messages;\nusing eShop.ClientApp.Models.Basket;\nusing eShop.ClientApp.Models.Catalog;\nusing eShop.ClientApp.Services;\nusing eShop.ClientApp.Services.AppEnvironment;\nusing eShop.ClientApp.ViewModels.Base;\n\nnamespace eShop.ClientApp.ViewModels;\n\npublic partial class CatalogItemViewModel : ViewModelBase\n{\n    private readonly IAppEnvironmentService _appEnvironmentService;\n\n    [ObservableProperty] private CatalogItem _catalogItem;\n\n    public CatalogItemViewModel(IAppEnvironmentService appEnvironmentService, INavigationService navigationService) :\n        base(navigationService)\n    {\n        _appEnvironmentService = appEnvironmentService;\n    }\n\n    public override void ApplyQueryAttributes(IDictionary<string, object> query)\n    {\n        base.ApplyQueryAttributes(query);\n\n        CatalogItem = query.ValueAs<CatalogItem>(\"CatalogItem\");\n    }\n\n    [RelayCommand]\n    private async Task AddCatalogItemAsync()\n    {\n        if (CatalogItem is null)\n        {\n            return;\n        }\n\n        var basket = await _appEnvironmentService.BasketService.GetBasketAsync();\n        if (basket is not null)\n        {\n            basket.AddItemToBasket(\n                new BasketItem\n                {\n                    ProductId = CatalogItem.Id,\n                    ProductName = CatalogItem.Name,\n                    PictureUrl = CatalogItem.PictureUri,\n                    UnitPrice = CatalogItem.Price,\n                    Quantity = 1\n                });\n\n            var basketUpdate = await _appEnvironmentService.BasketService.UpdateBasketAsync(basket);\n\n            WeakReferenceMessenger.Default\n                .Send(new ProductCountChangedMessage(basketUpdate.ItemCount));\n\n            await NavigationService.PopAsync();\n        }\n    }\n\n    [RelayCommand]\n    private async Task DismissAsync()\n    {\n        await NavigationService.PopAsync();\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/ViewModels/CatalogViewModel.cs",
    "content": "#nullable enable\nusing CommunityToolkit.Mvvm.Messaging;\nusing eShop.ClientApp.Messages;\nusing eShop.ClientApp.Models.Catalog;\nusing eShop.ClientApp.Services;\nusing eShop.ClientApp.Services.AppEnvironment;\nusing eShop.ClientApp.ViewModels.Base;\n\nnamespace eShop.ClientApp.ViewModels;\n\npublic partial class CatalogViewModel : ViewModelBase\n{\n    private readonly IAppEnvironmentService _appEnvironmentService;\n    private readonly ObservableCollectionEx<CatalogBrandSelectionViewModel> _brands = new();\n\n    private readonly ObservableCollectionEx<CatalogItem> _products = new();\n    private readonly ObservableCollectionEx<CatalogTypeSelectionViewModel> _types = new();\n\n    [ObservableProperty] private int _badgeCount;\n\n    private bool _initialized;\n\n    [ObservableProperty] private bool _isFiltering;\n\n    [ObservableProperty]\n    [NotifyPropertyChangedFor(nameof(CanFilter))]\n    [NotifyCanExecuteChangedFor(nameof(ApplyFilterCommand))]\n    private CatalogBrand? _selectedBrand;\n\n    [ObservableProperty] private CatalogItem? _selectedProduct;\n\n    [ObservableProperty]\n    [NotifyPropertyChangedFor(nameof(CanFilter))]\n    [NotifyCanExecuteChangedFor(nameof(ApplyFilterCommand))]\n    private CatalogType? _selectedType;\n\n    public CatalogViewModel(\n        IAppEnvironmentService appEnvironmentService,\n        INavigationService navigationService)\n        : base(navigationService)\n    {\n        _appEnvironmentService = appEnvironmentService;\n\n        _products = new ObservableCollectionEx<CatalogItem>();\n        _brands = new ObservableCollectionEx<CatalogBrandSelectionViewModel>();\n        _types = new ObservableCollectionEx<CatalogTypeSelectionViewModel>();\n\n        WeakReferenceMessenger.Default\n            .Register<CatalogViewModel, ProductCountChangedMessage>(\n                this,\n                (_, message) =>\n                {\n                    BadgeCount = message.Value;\n                });\n    }\n\n    public bool CanFilter => SelectedBrand is not null && SelectedType is not null;\n\n    public IReadOnlyList<CatalogItem> Products => _products;\n\n    public IReadOnlyList<CatalogBrandSelectionViewModel> Brands => _brands;\n\n    public IReadOnlyList<CatalogTypeSelectionViewModel> Types => _types;\n\n    public override async Task InitializeAsync()\n    {\n        if (_initialized)\n        {\n            return;\n        }\n\n        _initialized = true;\n        await IsBusyFor(\n            async () =>\n            {\n                // Get Catalog, Brands and Types\n                var products = await _appEnvironmentService.CatalogService.GetCatalogAsync();\n                var brands = await _appEnvironmentService.CatalogService.GetCatalogBrandAsync();\n                var types = await _appEnvironmentService.CatalogService.GetCatalogTypeAsync();\n                var basket = await _appEnvironmentService.BasketService.GetBasketAsync();\n\n                BadgeCount = basket.ItemCount;\n\n                _products.ReloadData(products);\n                _brands.ReloadData(brands.Select(x => new CatalogBrandSelectionViewModel {Value = x}));\n                _types.ReloadData(types.Select(x => new CatalogTypeSelectionViewModel {Value = x}));\n            });\n    }\n\n    [RelayCommand]\n    private async Task ViewCatalogItemAsync(CatalogItem catalogItem)\n    {\n        SelectedProduct = null;\n\n        if (catalogItem is null)\n        {\n            return;\n        }\n\n        await NavigationService.NavigateToAsync(\n            \"ViewCatalogItem\",\n            new Dictionary<string, object> {[\"CatalogItem\"] = catalogItem});\n    }\n\n    [RelayCommand]\n    private void Filter()\n    {\n        IsFiltering = !IsFiltering;\n    }\n\n    [RelayCommand]\n    public void SelectCatalogBrand(CatalogBrand? selectedItem)\n    {\n        foreach (var brand in Brands)\n        {\n            var isSelection = brand.Value == selectedItem;\n\n            if (!isSelection)\n            {\n                brand.Selected = false;\n                continue;\n            }\n\n            if (brand.Selected)\n            {\n                SelectedBrand = null;\n                brand.Selected = false;\n                continue;\n            }\n\n            SelectedBrand = selectedItem;\n            brand.Selected = true;\n        }\n    }\n\n    [RelayCommand]\n    public void SelectCatalogType(CatalogType? selectedItem)\n    {\n        foreach (var type in Types)\n        {\n            var isSelection = type.Value == selectedItem;\n\n            if (!isSelection)\n            {\n                type.Selected = false;\n                continue;\n            }\n\n            if (type.Selected)\n            {\n                SelectedType = null;\n                type.Selected = false;\n                continue;\n            }\n\n            SelectedType = selectedItem;\n            type.Selected = true;\n        }\n    }\n\n    [RelayCommand]\n    private async Task ApplyFilterAsync()\n    {\n        await IsBusyFor(\n            async () =>\n            {\n                if (SelectedBrand is not null && SelectedType is not null)\n                {\n                    var filteredProducts =\n                        await _appEnvironmentService.CatalogService.FilterAsync(SelectedBrand.Id, SelectedType.Id);\n                    _products.ReloadData(filteredProducts);\n                }\n\n                IsFiltering = false;\n            });\n    }\n\n    [RelayCommand]\n    private async Task ClearFilterAsync()\n    {\n        await IsBusyFor(\n            async () =>\n            {\n                SelectCatalogBrand(default);\n                SelectCatalogType(default);\n                var allProducts = await _appEnvironmentService.CatalogService.GetCatalogAsync();\n                _products.ReloadData(allProducts);\n                IsFiltering = false;\n            });\n    }\n\n    [RelayCommand]\n    private async Task ViewBasket()\n    {\n        await NavigationService.NavigateToAsync(\"Basket\");\n    }\n}\n\npublic class CatalogBrandSelectionViewModel : SelectionViewModel<CatalogBrand>\n{\n}\n\npublic class CatalogTypeSelectionViewModel : SelectionViewModel<CatalogType>\n{\n}\n"
  },
  {
    "path": "src/ClientApp/ViewModels/CheckoutViewModel.cs",
    "content": "using CommunityToolkit.Mvvm.Messaging;\nusing eShop.ClientApp.Messages;\nusing eShop.ClientApp.Models.Basket;\nusing eShop.ClientApp.Models.Orders;\nusing eShop.ClientApp.Models.User;\nusing eShop.ClientApp.Services;\nusing eShop.ClientApp.Services.AppEnvironment;\nusing eShop.ClientApp.Services.Settings;\nusing eShop.ClientApp.ViewModels.Base;\n\nnamespace eShop.ClientApp.ViewModels;\n\npublic partial class CheckoutViewModel : ViewModelBase\n{\n    private readonly IAppEnvironmentService _appEnvironmentService;\n\n    private readonly BasketViewModel _basketViewModel;\n    private readonly IDialogService _dialogService;\n    private readonly ISettingsService _settingsService;\n\n    [ObservableProperty] private Order _order;\n\n    [ObservableProperty] private Address _shippingAddress;\n\n    public CheckoutViewModel(\n        BasketViewModel basketViewModel,\n        IAppEnvironmentService appEnvironmentService, IDialogService dialogService, ISettingsService settingsService,\n        INavigationService navigationService)\n        : base(navigationService)\n    {\n        _dialogService = dialogService;\n        _appEnvironmentService = appEnvironmentService;\n        _settingsService = settingsService;\n\n        _basketViewModel = basketViewModel;\n    }\n\n    public override async Task InitializeAsync()\n    {\n        await IsBusyFor(\n            async () =>\n            {\n                var basketItems = _appEnvironmentService.BasketService.LocalBasketItems;\n\n                var userInfo = await _appEnvironmentService.IdentityService.GetUserInfoAsync();\n\n                // Create Shipping Address\n                ShippingAddress = new Address\n                {\n                    Id = !string.IsNullOrEmpty(userInfo?.UserId) ? new Guid(userInfo.UserId) : Guid.NewGuid(),\n                    Street = userInfo?.Street,\n                    ZipCode = userInfo?.ZipCode,\n                    State = userInfo?.State,\n                    Country = userInfo?.Country,\n                    City = userInfo?.Address\n                };\n\n                // Create Payment Info\n                var paymentInfo = new PaymentInfo\n                {\n                    CardNumber = userInfo?.CardNumber,\n                    CardHolderName = userInfo?.CardHolder,\n                    CardType = new CardType {Id = 3, Name = \"MasterCard\"},\n                    SecurityNumber = userInfo?.CardSecurityNumber\n                };\n\n                var orderItems = CreateOrderItems(basketItems);\n\n                // Create new Order\n                Order = new Order\n                {\n                    //TODO: Get a better order number generator\n                    OrderNumber = (int)DateTimeOffset.Now.TimeOfDay.TotalMilliseconds,\n                    UserId = userInfo.UserId,\n                    UserName = userInfo.PreferredUsername,\n                    OrderItems = orderItems,\n                    OrderStatus = \"Submitted\",\n                    OrderDate = DateTime.Now,\n                    CardHolderName = paymentInfo.CardHolderName,\n                    CardNumber = paymentInfo.CardNumber,\n                    CardSecurityNumber = paymentInfo.SecurityNumber,\n                    CardExpiration = DateTime.UtcNow.AddYears(5),\n                    CardTypeId = paymentInfo.CardType.Id,\n                    ShippingState = ShippingAddress.State,\n                    ShippingCountry = ShippingAddress.Country,\n                    ShippingStreet = ShippingAddress.Street,\n                    ShippingCity = ShippingAddress.City,\n                    ShippingZipCode = ShippingAddress.ZipCode,\n                    Total = CalculateTotal(orderItems)\n                };\n\n                if (_settingsService.UseMocks)\n                {\n                    // Get number of orders\n                    var orders = await _appEnvironmentService.OrderService.GetOrdersAsync();\n\n                    // Create the OrderNumber\n                    Order.OrderNumber = orders.Count() + 1;\n                    OnPropertyChanged(nameof(Order));\n                }\n            });\n    }\n\n    [RelayCommand]\n    private async Task CheckoutAsync()\n    {\n        try\n        {\n            var basket = _appEnvironmentService.OrderService.MapOrderToBasket(Order);\n            basket.RequestId = Guid.NewGuid();\n\n            await _appEnvironmentService.OrderService.CreateOrderAsync(Order);\n\n            // Clean Basket\n            await _appEnvironmentService.BasketService.ClearBasketAsync();\n\n            // Reset Basket badge\n            await _basketViewModel.ClearBasketItems();\n            \n            WeakReferenceMessenger.Default\n                .Send(new ProductCountChangedMessage(0));\n\n            // Navigate to Orders\n            await NavigationService.NavigateToAsync(\"//Main/Catalog\");\n\n            // Show Dialog\n            await _dialogService.ShowAlertAsync(\"Order sent successfully!\", \"Checkout\", \"Ok\");\n        }\n        catch (Exception ex)\n        {\n            Console.WriteLine(ex);\n            await _dialogService.ShowAlertAsync(\"An error ocurred. Please, try again.\", \"Oops!\", \"Ok\");\n        }\n    }\n\n    private static List<OrderItem> CreateOrderItems(IEnumerable<BasketItem> basketItems)\n    {\n        var orderItems = new List<OrderItem>();\n\n        foreach (var basketItem in basketItems)\n        {\n            if (!string.IsNullOrEmpty(basketItem.ProductName))\n            {\n                orderItems.Add(new OrderItem\n                {\n                    OrderId = null,\n                    ProductId = basketItem.ProductId,\n                    ProductName = basketItem.ProductName,\n                    PictureUrl = basketItem.PictureUrl,\n                    Quantity = basketItem.Quantity,\n                    UnitPrice = basketItem.UnitPrice\n                });\n            }\n        }\n\n        return orderItems;\n    }\n\n    private static decimal CalculateTotal(List<OrderItem> orderItems)\n    {\n        decimal total = 0;\n\n        foreach (var orderItem in orderItems)\n        {\n            total += orderItem.Quantity * orderItem.UnitPrice;\n        }\n\n        return total;\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/ViewModels/LoginViewModel.cs",
    "content": "using System.Diagnostics;\nusing eShop.ClientApp.Services;\nusing eShop.ClientApp.Services.AppEnvironment;\nusing eShop.ClientApp.Services.OpenUrl;\nusing eShop.ClientApp.Services.Settings;\nusing eShop.ClientApp.Validations;\nusing eShop.ClientApp.ViewModels.Base;\n\nnamespace eShop.ClientApp.ViewModels;\n\npublic partial class LoginViewModel : ViewModelBase\n{\n    private readonly IAppEnvironmentService _appEnvironmentService;\n    private readonly IOpenUrlService _openUrlService;\n    private readonly ISettingsService _settingsService;\n\n    [ObservableProperty] private bool _isLogin;\n\n    [ObservableProperty] private bool _isMock;\n\n    [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(MockSignInCommand))]\n    private bool _isValid;\n\n    [ObservableProperty] private string _loginUrl;\n\n    [ObservableProperty] private ValidatableObject<string> _password = new();\n\n    [ObservableProperty] private ValidatableObject<string> _userName = new();\n\n    public LoginViewModel(\n        IOpenUrlService openUrlService, IAppEnvironmentService appEnvironmentService,\n        INavigationService navigationService, ISettingsService settingsService)\n        : base(navigationService)\n    {\n        _settingsService = settingsService;\n        _openUrlService = openUrlService;\n        _appEnvironmentService = appEnvironmentService;\n\n        InvalidateMock();\n    }\n\n    public override async void ApplyQueryAttributes(IDictionary<string, object> query)\n    {\n        base.ApplyQueryAttributes(query);\n\n        if (query.ValueAsBool(\"Logout\"))\n        {\n            await PerformLogoutAsync();\n        }\n    }\n\n    public override Task InitializeAsync()\n    {\n        return Task.CompletedTask;\n    }\n\n    [RelayCommand(CanExecute = nameof(IsValid))]\n    private async Task MockSignInAsync()\n    {\n        await IsBusyFor(\n            async () =>\n            {\n                var isAuthenticated = false;\n\n                try\n                {\n                    await Task.Delay(1000);\n\n                    isAuthenticated = true;\n                }\n                catch (Exception ex)\n                {\n                    Debug.WriteLine($\"[SignIn] Error signing in: {ex}\");\n                }\n\n                if (isAuthenticated)\n                {\n                    await NavigationService.NavigateToAsync(\"//Main/Catalog\");\n                }\n            });\n    }\n\n    [RelayCommand]\n    private async Task SignInAsync()\n    {\n        await IsBusyFor(\n            async () =>\n            {\n                var loginSuccess = await _appEnvironmentService.IdentityService.SignInAsync();\n\n                if (loginSuccess)\n                {\n                    await NavigationService.NavigateToAsync(\"//Main/Catalog\");\n                }\n            });\n    }\n\n    [RelayCommand]\n    private Task RegisterAsync()\n    {\n        return _openUrlService.OpenUrl(_settingsService.RegistrationEndpoint);\n    }\n\n    [RelayCommand]\n    private async Task PerformLogoutAsync()\n    {\n        await _appEnvironmentService.IdentityService.SignOutAsync();\n\n        _settingsService.UseFakeLocation = false;\n\n        UserName.Value = string.Empty;\n        Password.Value = string.Empty;\n    }\n\n    [RelayCommand]\n    private Task SettingsAsync()\n    {\n        return NavigationService.NavigateToAsync(\"Settings\");\n    }\n\n    [RelayCommand]\n    private void Validate()\n    {\n        IsValid = UserName.Validate() && Password.Validate();\n    }\n\n    private void AddValidations()\n    {\n        UserName.Validations.Add(new IsNotNullOrEmptyRule<string> {ValidationMessage = \"A username is required.\"});\n        Password.Validations.Add(new IsNotNullOrEmptyRule<string> {ValidationMessage = \"A password is required.\"});\n    }\n\n    public void InvalidateMock()\n    {\n        IsMock = false; //_settingsService.UseMocks;\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/ViewModels/MainViewModel.cs",
    "content": "﻿using eShop.ClientApp.Services;\nusing eShop.ClientApp.ViewModels.Base;\n\nnamespace eShop.ClientApp.ViewModels;\n\npublic partial class MainViewModel : ViewModelBase\n{\n    public MainViewModel(INavigationService navigationService)\n        : base(navigationService)\n    {\n    }\n\n    [RelayCommand]\n    private async Task SettingsAsync()\n    {\n        await NavigationService.NavigateToAsync(\"Settings\");\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/ViewModels/MapViewModel.cs",
    "content": "﻿using eShop.ClientApp.Services;\nusing eShop.ClientApp.ViewModels.Base;\n\nnamespace eShop.ClientApp.ViewModels;\n\npublic partial class MapViewModel : ViewModelBase\n{\n    [ObservableProperty] private IEnumerable<Store> _stores;\n\n    public MapViewModel(INavigationService navigationService)\n        : base(navigationService)\n    {\n    }\n\n    public override Task InitializeAsync()\n    {\n        Stores =\n            new[]\n            {\n                new Store\n                {\n                    Address = \"Building 92, Redmond, WA\",\n                    Description = \"Microsoft Visitor Center\",\n                    Location = new Location(47.6423109, -122.1368406)\n                }\n            };\n\n        return Task.CompletedTask;\n    }\n}\n\npublic record Store\n{\n    public Location Location { get; set; }\n    public string Address { get; set; }\n    public string Description { get; set; }\n}\n"
  },
  {
    "path": "src/ClientApp/ViewModels/ObservableCollectionEx.cs",
    "content": "﻿using System.Collections.ObjectModel;\nusing System.Collections.Specialized;\nusing System.ComponentModel;\n\nnamespace eShop.ClientApp.ViewModels;\n\npublic class ObservableCollectionEx<T> : ObservableCollection<T>\n{\n    public ObservableCollectionEx()\n    {\n    }\n\n    public ObservableCollectionEx(IEnumerable<T> collection) : base(collection)\n    {\n    }\n\n    public ObservableCollectionEx(List<T> list) : base(list)\n    {\n    }\n\n    public void ReloadData(IEnumerable<T> items)\n    {\n        ReloadData(\n            innerList =>\n            {\n                foreach (var item in items)\n                {\n                    innerList.Add(item);\n                }\n            });\n    }\n\n    public void ReloadData(Action<IList<T>> innerListAction)\n    {\n        Items.Clear();\n\n        innerListAction(Items);\n\n        OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));\n        OnPropertyChanged(new PropertyChangedEventArgs(\"Items[]\"));\n        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));\n    }\n\n    public async Task ReloadDataAsync(Func<IList<T>, Task> innerListAction)\n    {\n        Items.Clear();\n\n        await innerListAction(Items);\n\n        OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));\n        OnPropertyChanged(new PropertyChangedEventArgs(\"Items[]\"));\n        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/ViewModels/OrderDetailViewModel.cs",
    "content": "using eShop.ClientApp.Models.Orders;\nusing eShop.ClientApp.Services;\nusing eShop.ClientApp.Services.AppEnvironment;\nusing eShop.ClientApp.Services.Settings;\nusing eShop.ClientApp.ViewModels.Base;\n\nnamespace eShop.ClientApp.ViewModels;\n\npublic partial class OrderDetailViewModel : ViewModelBase, IQueryAttributable\n{\n    private readonly IAppEnvironmentService _appEnvironmentService;\n    private readonly ISettingsService _settingsService;\n\n    [ObservableProperty] private bool _isSubmittedOrder;\n\n    [ObservableProperty] private Order _order;\n\n    [ObservableProperty] private int _orderNumber;\n\n    [ObservableProperty] private string _orderStatusText;\n\n    public OrderDetailViewModel(\n        IAppEnvironmentService appEnvironmentService,\n        INavigationService navigationService, ISettingsService settingsService)\n        : base(navigationService)\n    {\n        _appEnvironmentService = appEnvironmentService;\n        _settingsService = settingsService;\n    }\n\n    public override async Task InitializeAsync()\n    {\n        await IsBusyFor(\n            async () =>\n            {\n                // Get order detail info\n                Order = await _appEnvironmentService.OrderService.GetOrderAsync(OrderNumber);\n                IsSubmittedOrder = Order.OrderStatus.Equals(\"Submitted\", StringComparison.OrdinalIgnoreCase);\n                OrderStatusText = Order.OrderStatus;\n            });\n    }\n\n    [RelayCommand]\n    private async Task ToggleCancelOrderAsync()\n    {\n        var result = await _appEnvironmentService.OrderService.CancelOrderAsync(Order.OrderNumber);\n\n        if (result)\n        {\n            OrderStatusText = \"Cancelled\";\n        }\n        else\n        {\n            Order = await _appEnvironmentService.OrderService.GetOrderAsync(Order.OrderNumber);\n            OrderStatusText = Order.OrderStatus;\n        }\n\n        IsSubmittedOrder = false;\n    }\n\n    public override void ApplyQueryAttributes(IDictionary<string, object> query)\n    {\n        if (query.TryGetValue(\"OrderNumber\", out var orderNumber))\n        {\n            if (orderNumber is string orderNumberString && int.TryParse(orderNumberString, out var parsedOrderNumber))\n            {\n                OrderNumber = parsedOrderNumber;\n            }\n            else if (orderNumber is int intOrderNumber)\n            {\n                OrderNumber = intOrderNumber;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/ViewModels/ProfileViewModel.cs",
    "content": "﻿using eShop.ClientApp.Models.Orders;\nusing eShop.ClientApp.Services;\nusing eShop.ClientApp.Services.AppEnvironment;\nusing eShop.ClientApp.Services.Settings;\nusing eShop.ClientApp.ViewModels.Base;\n\nnamespace eShop.ClientApp.ViewModels;\n\npublic partial class ProfileViewModel : ViewModelBase\n{\n    private readonly IAppEnvironmentService _appEnvironmentService;\n    private readonly ObservableCollectionEx<Order> _orders;\n    private readonly ISettingsService _settingsService;\n\n    [ObservableProperty] private Order _selectedOrder;\n\n    public ProfileViewModel(\n        IAppEnvironmentService appEnvironmentService, ISettingsService settingsService,\n        INavigationService navigationService)\n        : base(navigationService)\n    {\n        _appEnvironmentService = appEnvironmentService;\n        _settingsService = settingsService;\n\n        _orders = new ObservableCollectionEx<Order>();\n    }\n\n    public IList<Order> Orders => _orders;\n\n    public override async Task InitializeAsync()\n    {\n        await RefreshAsync();\n    }\n\n    [RelayCommand]\n    private async Task LogoutAsync()\n    {\n        await IsBusyFor(\n            async () =>\n            {\n                // Logout\n                await NavigationService.NavigateToAsync(\n                    \"//Login\",\n                    new Dictionary<string, object> {{\"Logout\", true}});\n            });\n    }\n\n    [RelayCommand]\n    private async Task RefreshAsync()\n    {\n        if (IsBusy)\n        {\n            return;\n        }\n\n        await IsBusyFor(\n            async () =>\n            {\n                // Get orders\n                var orders = await _appEnvironmentService.OrderService.GetOrdersAsync();\n\n                _orders.ReloadData(orders);\n            });\n    }\n\n    [RelayCommand]\n    private async Task OrderDetailAsync(Order order)\n    {\n        if (order is null || IsBusy)\n        {\n            return;\n        }\n\n        await NavigationService.NavigateToAsync(\n            \"OrderDetail\",\n            new Dictionary<string, object> {{nameof(Order.OrderNumber), order.OrderNumber}});\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/ViewModels/SelectionViewModel.cs",
    "content": "﻿namespace eShop.ClientApp.ViewModels;\n\npublic partial class SelectionViewModel<T> : ObservableObject\n{\n    [ObservableProperty] private bool _selected;\n    [ObservableProperty] private T _value;\n}\n"
  },
  {
    "path": "src/ClientApp/ViewModels/SettingsViewModel.cs",
    "content": "﻿using System.ComponentModel;\nusing System.Globalization;\nusing System.Windows.Input;\nusing eShop.ClientApp.Services;\nusing eShop.ClientApp.Services.AppEnvironment;\nusing eShop.ClientApp.Services.Location;\nusing eShop.ClientApp.Services.Settings;\nusing eShop.ClientApp.ViewModels.Base;\nusing Location = eShop.ClientApp.Models.Location.Location;\n\nnamespace eShop.ClientApp.ViewModels;\n\npublic class SettingsViewModel : ViewModelBase\n{\n    //Needed if using Android Emulator Locally. See https://learn.microsoft.com/en-us/dotnet/maui/data-cloud/local-web-services?view=net-maui-8.0#android\n    private static string _baseAddress = DeviceInfo.Platform == DevicePlatform.Android ? \"10.0.2.2\" : \"localhost\";\n    \n    private readonly IAppEnvironmentService _appEnvironmentService;\n    private readonly ILocationService _locationService;\n    private readonly ISettingsService _settingsService;\n    private bool _allowGpsLocation;\n    private string _gatewayBasketEndpoint;\n    private string _gatewayCatalogEndpoint;\n    private string _gatewayOrdersEndpoint;\n    private string _gpsWarningMessage;\n    private string _identityEndpoint;\n    private double _latitude;\n    private double _longitude;\n\n    private bool _useAzureServices;\n    private bool _useFakeLocation;\n\n    public SettingsViewModel(\n        ILocationService locationService, IAppEnvironmentService appEnvironmentService,\n        INavigationService navigationService, ISettingsService settingsService)\n        : base(navigationService)\n    {\n        _settingsService = settingsService;\n        _locationService = locationService;\n        _appEnvironmentService = appEnvironmentService;\n\n        _useAzureServices = !_settingsService.UseMocks;\n        _identityEndpoint = _settingsService.IdentityEndpointBase;\n        _latitude = double.Parse(_settingsService.Latitude, CultureInfo.CurrentCulture);\n        _longitude = double.Parse(_settingsService.Longitude, CultureInfo.CurrentCulture);\n        _useFakeLocation = _settingsService.UseFakeLocation;\n        _allowGpsLocation = _settingsService.AllowGpsLocation;\n        _gpsWarningMessage = string.Empty;\n\n        IdentityEndpoint =\n            !string.IsNullOrEmpty(_settingsService.IdentityEndpointBase)\n                ? _settingsService.IdentityEndpointBase\n                : $\"https://{_baseAddress}:5243\";\n\n        GatewayCatalogEndpoint =\n            !string.IsNullOrEmpty(_settingsService.GatewayCatalogEndpointBase)\n                ? _settingsService.GatewayCatalogEndpointBase\n                : $\"http://{_baseAddress}:11632\";\n\n        GatewayBasketEndpoint =\n            !string.IsNullOrEmpty(_settingsService.GatewayBasketEndpointBase)\n                ? _settingsService.GatewayBasketEndpointBase\n                : $\"http://{_baseAddress}:5221\";\n\n        GatewayOrdersEndpoint =\n            !string.IsNullOrEmpty(_settingsService.GatewayOrdersEndpointBase)\n                ? _settingsService.GatewayOrdersEndpointBase\n                : $\"http://{_baseAddress}:11632\";\n\n        ToggleMockServicesCommand = new RelayCommand(ToggleMockServices);\n\n        ToggleFakeLocationCommand = new RelayCommand(ToggleFakeLocation);\n\n        ToggleSendLocationCommand = new AsyncRelayCommand(ToggleSendLocationAsync);\n\n        ToggleAllowGpsLocationCommand = new RelayCommand(ToggleAllowGpsLocation);\n\n        UseAzureServices = !_settingsService.UseMocks;\n    }\n\n    public string TitleUseAzureServices => \"Use Microservices/Containers from eShop\";\n\n    public string DescriptionUseAzureServices => !UseAzureServices\n        ? \"Currently using mock services that are simulated objects that mimic the behavior of real services using a controlled approach. Toggle on to configure the use of microserivces/containers.\"\n        : \"When enabling the use of microservices/containers, the app will attempt to use real services deployed as Docker/Kubernetes containers at the specified base endpoint, which will must be reachable through the network.\";\n\n    public bool UseAzureServices\n    {\n        get => _useAzureServices;\n        set\n        {\n            SetProperty(ref _useAzureServices, value);\n            UpdateUseAzureServices();\n        }\n    }\n\n    public string TitleUseFakeLocation => !UseFakeLocation\n        ? \"Use Real Location\"\n        : \"Use Fake Location\";\n\n    public string DescriptionUseFakeLocation => !UseFakeLocation\n        ? \"When enabling location, the app will attempt to use the location from the device.\"\n        : \"Fake Location data is added for marketing campaign testing.\";\n\n    public bool UseFakeLocation\n    {\n        get => _useFakeLocation;\n        set\n        {\n            SetProperty(ref _useFakeLocation, value);\n            UpdateFakeLocation();\n        }\n    }\n\n    public string TitleAllowGpsLocation => !AllowGpsLocation\n        ? \"GPS Location Disabled\"\n        : \"GPS Location Enabled\";\n\n    public string DescriptionAllowGpsLocation => !AllowGpsLocation\n        ? \"When disabling location, you won't receive location campaigns based upon your location.\"\n        : \"When enabling location, you'll receive location campaigns based upon your location.\";\n\n    public string GpsWarningMessage\n    {\n        get => _gpsWarningMessage;\n        set => SetProperty(ref _gpsWarningMessage, value);\n    }\n\n    public string IdentityEndpoint\n    {\n        get => _identityEndpoint;\n        set\n        {\n            SetProperty(ref _identityEndpoint, value);\n            if (!string.IsNullOrEmpty(value))\n            {\n                UpdateIdentityEndpoint();\n            }\n        }\n    }\n\n    public string GatewayCatalogEndpoint\n    {\n        get => _gatewayCatalogEndpoint;\n        set\n        {\n            SetProperty(ref _gatewayCatalogEndpoint, value);\n            if (!string.IsNullOrEmpty(value))\n            {\n                UpdateGatewayShoppingEndpoint();\n            }\n        }\n    }\n\n    public string GatewayOrdersEndpoint\n    {\n        get => _gatewayOrdersEndpoint;\n        set\n        {\n            SetProperty(ref _gatewayOrdersEndpoint, value);\n            if (!string.IsNullOrEmpty(value))\n            {\n                UpdateGatewayOrdersEndpoint();\n            }\n        }\n    }\n\n    public string GatewayBasketEndpoint\n    {\n        get => _gatewayBasketEndpoint;\n        set\n        {\n            SetProperty(ref _gatewayBasketEndpoint, value);\n            if (!string.IsNullOrEmpty(value))\n            {\n                UpdateGatewayBasketEndpoint();\n            }\n        }\n    }\n\n    public double Latitude\n    {\n        get => _latitude;\n        set\n        {\n            SetProperty(ref _latitude, value);\n            UpdateLatitude();\n        }\n    }\n\n    public double Longitude\n    {\n        get => _longitude;\n        set\n        {\n            SetProperty(ref _longitude, value);\n            UpdateLongitude();\n        }\n    }\n\n    public bool AllowGpsLocation\n    {\n        get => _allowGpsLocation;\n        set => SetProperty(ref _allowGpsLocation, value);\n    }\n\n    public ICommand ToggleMockServicesCommand { get; }\n\n    public ICommand ToggleFakeLocationCommand { get; }\n\n    public ICommand ToggleSendLocationCommand { get; }\n\n    public ICommand ToggleAllowGpsLocationCommand { get; }\n\n    protected override async void OnPropertyChanged(PropertyChangedEventArgs e)\n    {\n        base.OnPropertyChanged(e);\n\n        if (e.PropertyName == nameof(AllowGpsLocation))\n        {\n            await UpdateAllowGpsLocation();\n        }\n    }\n\n    private void ToggleMockServices()\n    {\n        _appEnvironmentService.UpdateDependencies(!UseAzureServices);\n\n        OnPropertyChanged(nameof(TitleUseAzureServices));\n        OnPropertyChanged(nameof(DescriptionUseAzureServices));\n    }\n\n    private void ToggleFakeLocation()\n    {\n        _appEnvironmentService.UpdateDependencies(!UseAzureServices);\n        OnPropertyChanged(nameof(TitleUseFakeLocation));\n        OnPropertyChanged(nameof(DescriptionUseFakeLocation));\n    }\n\n    private async Task ToggleSendLocationAsync()\n    {\n        if (!_settingsService.UseMocks)\n        {\n            var locationRequest = new Location {Latitude = _latitude, Longitude = _longitude};\n\n            await _locationService.UpdateUserLocation(locationRequest);\n        }\n    }\n\n    private void ToggleAllowGpsLocation()\n    {\n        OnPropertyChanged(nameof(TitleAllowGpsLocation));\n        OnPropertyChanged(nameof(DescriptionAllowGpsLocation));\n    }\n\n    private void UpdateUseAzureServices()\n    {\n        // Save use mocks services to local storage\n        _settingsService.UseMocks = !UseAzureServices;\n    }\n\n    private void UpdateIdentityEndpoint()\n    {\n        // Update remote endpoint (save to local storage)\n        _settingsService.IdentityEndpointBase = _identityEndpoint;\n    }\n\n    private void UpdateGatewayShoppingEndpoint()\n    {\n        _settingsService.GatewayCatalogEndpointBase = _gatewayCatalogEndpoint;\n    }\n    \n    private void UpdateGatewayOrdersEndpoint()\n    {\n        _settingsService.GatewayOrdersEndpointBase = _gatewayOrdersEndpoint;\n    }\n    \n    private void UpdateGatewayBasketEndpoint()\n    {\n        _settingsService.GatewayBasketEndpointBase = _gatewayBasketEndpoint;\n    }\n\n    private void UpdateFakeLocation()\n    {\n        _settingsService.UseFakeLocation = _useFakeLocation;\n    }\n\n    private void UpdateLatitude()\n    {\n        // Update fake latitude (save to local storage)\n        _settingsService.Latitude = _latitude.ToString();\n    }\n\n    private void UpdateLongitude()\n    {\n        // Update fake longitude (save to local storage)\n        _settingsService.Longitude = _longitude.ToString();\n    }\n\n    private async Task UpdateAllowGpsLocation()\n    {\n        if (_allowGpsLocation)\n        {\n            bool hasWhenInUseLocationPermissions;\n            bool hasBackgroundLocationPermissions;\n\n            if (await Permissions.CheckStatusAsync<Permissions.LocationWhenInUse>() != PermissionStatus.Granted)\n            {\n                hasWhenInUseLocationPermissions = await Permissions.RequestAsync<Permissions.LocationWhenInUse>() ==\n                                                  PermissionStatus.Granted;\n            }\n            else\n            {\n                hasWhenInUseLocationPermissions = true;\n            }\n\n            if (await Permissions.CheckStatusAsync<Permissions.LocationAlways>() != PermissionStatus.Granted)\n            {\n                hasBackgroundLocationPermissions = await Permissions.RequestAsync<Permissions.LocationAlways>() ==\n                                                   PermissionStatus.Granted;\n            }\n            else\n            {\n                hasBackgroundLocationPermissions = true;\n            }\n\n\n            if (!hasWhenInUseLocationPermissions || !hasBackgroundLocationPermissions)\n            {\n                _allowGpsLocation = false;\n                GpsWarningMessage = \"Enable the GPS sensor on your device\";\n            }\n            else\n            {\n                _settingsService.AllowGpsLocation = _allowGpsLocation;\n                GpsWarningMessage = string.Empty;\n            }\n        }\n        else\n        {\n            _settingsService.AllowGpsLocation = _allowGpsLocation;\n        }\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Views/BadgeView.cs",
    "content": "﻿using Microsoft.Maui.Controls.Shapes;\n\nnamespace eShop.ClientApp.Views;\n\n[ContentProperty(nameof(Content))]\npublic class BadgeView : Grid\n{\n    public static BindableProperty ContentProperty =\n        BindableProperty.Create(nameof(Content), typeof(View), typeof(BadgeView),\n            propertyChanged: OnLayoutPropertyChanged);\n\n    public static BindableProperty TextProperty =\n        BindableProperty.Create(nameof(Text), typeof(string), typeof(BadgeView),\n            propertyChanged: OnLayoutPropertyChanged);\n\n    public static BindableProperty TextColorProperty =\n        BindableProperty.Create(nameof(TextColor), typeof(Color), typeof(BadgeView),\n            propertyChanged: OnLayoutPropertyChanged);\n\n    public static BindableProperty FontSizeProperty =\n        BindableProperty.Create(nameof(FontSize), typeof(double), typeof(BadgeView), 10.0d,\n            propertyChanged: OnLayoutPropertyChanged);\n\n    public static BindableProperty BadgeColorProperty =\n        BindableProperty.Create(nameof(BadgeColor), typeof(Color), typeof(BadgeView),\n            propertyChanged: OnLayoutPropertyChanged);\n\n    private readonly Label _badgeIndicator;\n    private readonly Border _border;\n    private readonly RoundRectangle _borderShape;\n\n    public BadgeView()\n    {\n        _badgeIndicator =\n            new Label\n            {\n                Padding = 4,\n                HorizontalTextAlignment = TextAlignment.Center,\n                VerticalTextAlignment = TextAlignment.Center\n            };\n\n        _borderShape = new RoundRectangle();\n\n        _border =\n            new Border\n            {\n                StrokeShape = _borderShape,\n                Content = _badgeIndicator,\n                HorizontalOptions = LayoutOptions.End,\n                VerticalOptions = LayoutOptions.Start,\n                ZIndex = 10\n            };\n\n        Children.Add(_border);\n\n        UpdateLayout();\n    }\n\n    public View Content\n    {\n        get => (View)GetValue(ContentProperty);\n        set => SetValue(ContentProperty, value);\n    }\n\n    public string Text\n    {\n        get => (string)GetValue(TextProperty);\n        set => SetValue(TextProperty, value);\n    }\n\n    public Color TextColor\n    {\n        get => (Color)GetValue(TextColorProperty);\n        set => SetValue(TextColorProperty, value);\n    }\n\n    public double FontSize\n    {\n        get => (double)GetValue(FontSizeProperty);\n        set => SetValue(FontSizeProperty, value);\n    }\n\n    public Color BadgeColor\n    {\n        get => (Color)GetValue(BadgeColorProperty);\n        set => SetValue(BadgeColorProperty, value);\n    }\n\n    private static void OnLayoutPropertyChanged(BindableObject bindable, object oldValue, object newValue)\n    {\n        (bindable as BadgeView)?.UpdateLayout();\n    }\n\n    protected override void OnHandlerChanging(HandlerChangingEventArgs args)\n    {\n        base.OnHandlerChanging(args);\n\n        _border.SizeChanged -= BadgeIndicatorSizeChanged;\n\n        if (args.NewHandler is not null)\n        {\n            _border.SizeChanged += BadgeIndicatorSizeChanged;\n        }\n    }\n\n    private void BadgeIndicatorSizeChanged(object sender, EventArgs e)\n    {\n        var halfHeight = _border.Height * .5f;\n        _border.MinimumWidthRequest = _border.Height;\n        _borderShape.CornerRadius = halfHeight;\n\n        if (Content is not null)\n        {\n            Content.Margin = halfHeight;\n        }\n    }\n\n    private void UpdateLayout()\n    {\n        BatchBegin();\n        _border.BatchBegin();\n        _badgeIndicator.BatchBegin();\n\n        if (Content is not null && Content.Parent != this)\n        {\n            Content.ZIndex = 1;\n            Children.Add(Content);\n        }\n\n        _border.BackgroundColor = BadgeColor;\n\n        _badgeIndicator.Text = Text;\n        _badgeIndicator.TextColor = TextColor;\n        _badgeIndicator.FontSize = FontSize;\n\n        _border.BatchCommit();\n        _badgeIndicator.BatchCommit();\n        BatchCommit();\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Views/BasketView.xaml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<views:ContentPageBase\n    x:Class=\"eShop.ClientApp.Views.BasketView\"\n    xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n    xmlns:templates=\"clr-namespace:eShop.ClientApp.Views.Templates\"\n    xmlns:viewmodels=\"clr-namespace:eShop.ClientApp.ViewModels\"\n    xmlns:views=\"clr-namespace:eShop.ClientApp.Views\"\n    Title=\"CART\"\n    x:DataType=\"viewmodels:BasketViewModel\">\n    <ContentPage.Resources>\n        <ResourceDictionary />\n    </ContentPage.Resources>\n    <Grid RowDefinitions=\"*,Auto\">\n        <!--  CART ITEMS  -->\n        <CollectionView\n            Grid.Row=\"0\"\n            ItemsSource=\"{Binding BasketItems}\"\n            SelectionChangedCommand=\"{Binding AddCommand}\"\n            SelectionChangedCommandParameter=\"{Binding SelectedItem, Source={RelativeSource Self}, x:DataType=CollectionView}\">\n            <CollectionView.ItemTemplate>\n                <DataTemplate>\n                    <templates:BasketItemTemplate />\n                </DataTemplate>\n            </CollectionView.ItemTemplate>\n            <CollectionView.EmptyView>\n                <!--  EMPTY SHOPPING CART  -->\n                <Label\n                    HorizontalTextAlignment=\"Center\"\n                    Text=\"EMPTY SHOPPING CART\"\n                    VerticalTextAlignment=\"Center\" />\n            </CollectionView.EmptyView>\n        </CollectionView>\n        <!--  CHECKOUT  -->\n        <Grid\n            Grid.Row=\"1\"\n            Padding=\"16\"\n            BackgroundColor=\"{StaticResource LightGrayColor}\"\n            ColumnDefinitions=\"40,*,40\"\n            ColumnSpacing=\"4\"\n            RowDefinitions=\"auto,3,auto,auto\"\n            RowSpacing=\"4\">\n            <Image\n                Grid.Row=\"0\"\n                Grid.Column=\"0\"\n                HeightRequest=\"32\"\n                Source=\"cart.png\"\n                WidthRequest=\"32\" />\n            <Label\n                Grid.Row=\"0\"\n                Grid.Column=\"1\"\n                HorizontalTextAlignment=\"Center\"\n                Text=\"Your Shopping bag\"\n                VerticalTextAlignment=\"Center\" />\n            <Border\n                Grid.Row=\"0\"\n                Grid.Column=\"2\"\n                BackgroundColor=\"Black\"\n                HorizontalOptions=\"Center\"\n                MinimumHeightRequest=\"32\"\n                MinimumWidthRequest=\"32\"\n                StrokeShape=\"RoundRectangle 16\"\n                VerticalOptions=\"Center\">\n                <Label\n                    HorizontalTextAlignment=\"Center\"\n                    Text=\"{Binding BasketItems.Count}\"\n                    TextColor=\"White\"\n                    VerticalTextAlignment=\"Center\" />\n            </Border>\n            <BoxView\n                Grid.Row=\"1\"\n                Grid.Column=\"0\"\n                Grid.ColumnSpan=\"3\"\n                HeightRequest=\"1\"\n                VerticalOptions=\"End\"\n                Color=\"Black\" />\n            <Label\n                Grid.Row=\"2\"\n                Grid.Column=\"0\"\n                HorizontalTextAlignment=\"Center\"\n                Text=\"Total\" />\n            <Label\n                Grid.Row=\"2\"\n                Grid.Column=\"0\"\n                Grid.ColumnSpan=\"3\"\n                HorizontalTextAlignment=\"End\"\n                Text=\"{Binding Total, StringFormat='${0:N2}'}\" />\n            <Button\n                Grid.Row=\"2\"\n                Grid.Column=\"0\"\n                Grid.ColumnSpan=\"3\"\n                BackgroundColor=\"Black\"\n                Command=\"{Binding CheckoutCommand}\"\n                CornerRadius=\"4\"\n                Text=\"Check out\" />\n        </Grid>\n    </Grid>\n</views:ContentPageBase>"
  },
  {
    "path": "src/ClientApp/Views/BasketView.xaml.cs",
    "content": "﻿namespace eShop.ClientApp.Views;\n\npublic partial class BasketView\n{\n    public BasketView(BasketViewModel viewModel)\n    {\n        BindingContext = viewModel;\n        InitializeComponent();\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Views/CatalogItemView.xaml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n\n<views:ContentPageBase\n    x:Class=\"eShop.ClientApp.Views.CatalogItemView\"\n    xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n    xmlns:animations=\"clr-namespace:eShop.ClientApp.Animations\"\n    xmlns:triggers=\"clr-namespace:eShop.ClientApp.Triggers\"\n    xmlns:viewmodels=\"clr-namespace:eShop.ClientApp.ViewModels\"\n    xmlns:views=\"clr-namespace:eShop.ClientApp.Views\"\n    Padding=\"24\"\n    x:DataType=\"viewmodels:CatalogItemViewModel\"\n    BackgroundColor=\"#aa000000\"\n    Shell.NavBarIsVisible=\"False\"\n    Shell.PresentationMode=\"ModalNotAnimated\">\n    <views:ContentPageBase.Resources>\n        <ResourceDictionary>\n            <animations:StoryBoard x:Key=\"DisplayAnimation\" Target=\"{x:Reference ContentContainer}\">\n                <animations:FadeInAnimation Direction=\"Up\" Duration=\"600\" />\n            </animations:StoryBoard>\n        </ResourceDictionary>\n    </views:ContentPageBase.Resources>\n    <views:ContentPageBase.Triggers>\n        <EventTrigger Event=\"Appearing\">\n            <triggers:BeginAnimation Animation=\"{StaticResource DisplayAnimation}\" />\n        </EventTrigger>\n    </views:ContentPageBase.Triggers>\n    <Border\n        x:Name=\"ContentContainer\"\n        BackgroundColor=\"Gray\"\n        StrokeShape=\"RoundRectangle 8,8,8,8\"\n        StrokeThickness=\"1\">\n        <ScrollView>\n        <Grid\n            Padding=\"16\"\n            BackgroundColor=\"White\"\n            ColumnDefinitions=\"*,Auto\"\n            RowDefinitions=\"*,Auto,Auto,Auto,Auto,Auto,Auto\"\n            RowSpacing=\"8\">\n            <Image\n                Grid.Row=\"0\"\n                Grid.Column=\"0\"\n                Grid.ColumnSpan=\"2\"\n                MaximumHeightRequest=\"500\"\n                Margin=\"-16,-16,-16,0\"\n                Aspect=\"AspectFit\"\n                HorizontalOptions=\"Center\"\n                VerticalOptions=\"Start\"\n                Source=\"{Binding CatalogItem.PictureUri, Mode=OneWay}\" />\n\n            <ImageButton\n                Grid.Row=\"0\"\n                Grid.Column=\"0\"\n                Margin=\"16\"\n                Aspect=\"AspectFill\"\n                BackgroundColor=\"White\"\n                Command=\"{Binding DismissCommand, Mode=OneTime}\"\n                CornerRadius=\"16\"\n                HeightRequest=\"32\"\n                HorizontalOptions=\"Start\"\n                Source=\"arrow_left.png\"\n                VerticalOptions=\"Start\"\n                WidthRequest=\"32\" />\n            <Label\n                Grid.Row=\"1\"\n                Grid.Column=\"0\"\n                Grid.ColumnSpan=\"2\"\n                FontAttributes=\"Bold\"\n                FontSize=\"{StaticResource ExtraBigSize}\"\n                Style=\"{StaticResource LargeSizeFontStyle}\"\n                Text=\"{Binding CatalogItem.Name, Mode=OneWay}\" />\n            <Label\n                Grid.Row=\"2\"\n                Grid.Column=\"0\"\n                Grid.ColumnSpan=\"2\"\n                Style=\"{StaticResource MediumSizeFontStyle}\"\n                Text=\"{Binding CatalogItem.CatalogBrand.Brand, Mode=OneWay}\" />\n            <Label\n                Grid.Row=\"3\"\n                Grid.Column=\"0\"\n                Grid.ColumnSpan=\"2\"\n                Text=\"{Binding CatalogItem.Description, Mode=OneWay}\" />\n            <Label\n                Grid.Row=\"4\"\n                Grid.Column=\"0\"\n                Text=\"{Binding CatalogItem.Price, StringFormat='{0:C2}', Mode=OneWay}\"\n                VerticalTextAlignment=\"Center\" />\n\n            <Button\n                Grid.Row=\"4\"\n                Grid.Column=\"1\"\n                Margin=\"8\"\n                BackgroundColor=\"White\"\n                BorderColor=\"Black\"\n                BorderWidth=\"2\"\n                Command=\"{Binding AddCatalogItemCommand, Mode=OneTime}\"\n                CornerRadius=\"8\"\n                ImageSource=\"cart.png\"\n                Text=\"Add to shopping bag\"\n                TextColor=\"Black\" />\n        </Grid>\n        </ScrollView>\n    </Border>\n</views:ContentPageBase>"
  },
  {
    "path": "src/ClientApp/Views/CatalogItemView.xaml.cs",
    "content": "﻿namespace eShop.ClientApp.Views;\n\npublic partial class CatalogItemView\n{\n    public CatalogItemView(CatalogItemViewModel viewModel)\n    {\n        BindingContext = viewModel;\n        InitializeComponent();\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Views/CatalogView.xaml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<views:ContentPageBase\n    x:Class=\"eShop.ClientApp.Views.CatalogView\"\n    xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n    xmlns:animations=\"clr-namespace:eShop.ClientApp.Animations\"\n    xmlns:mct=\"http://schemas.microsoft.com/dotnet/2022/maui/toolkit\"\n    xmlns:templates=\"clr-namespace:eShop.ClientApp.Views.Templates\"\n    xmlns:triggers=\"clr-namespace:eShop.ClientApp.Triggers\"\n    xmlns:viewmodels=\"clr-namespace:eShop.ClientApp.ViewModels\"\n    xmlns:views=\"clr-namespace:eShop.ClientApp.Views\"\n    x:DataType=\"viewmodels:CatalogViewModel\"\n    Shell.BackgroundColor=\"White\"\n    Shell.NavBarHasShadow=\"False\">\n    <Shell.TitleView>\n        <Grid BackgroundColor=\"White\" ColumnDefinitions=\"40,*,40\">\n            <BoxView\n                Grid.Column=\"0\"\n                Grid.ColumnSpan=\"3\"\n                Background=\"White\" />\n            <Image\n                Grid.Column=\"0\"\n                Grid.ColumnSpan=\"3\"\n                Margin=\"4\"\n                HeightRequest=\"44\"\n                Source=\"logo_header.png\" />\n            <ImageButton\n                Grid.Column=\"2\"\n                Aspect=\"AspectFit\"\n                Command=\"{Binding FilterCommand, Mode=OneTime}\"\n                HeightRequest=\"24\"\n                HorizontalOptions=\"Center\"\n                Source=\"{DynamicResource FilterIconForTitleImageSource}\"\n                VerticalOptions=\"Center\"\n                WidthRequest=\"24\" />\n        </Grid>\n    </Shell.TitleView>\n    <ContentPage.Resources>\n        <ResourceDictionary>\n\n            <animations:StoryBoard x:Key=\"ProductsAnimation\" Target=\"{x:Reference Products}\">\n                <animations:FadeInAnimation\n                    Delay=\"250\"\n                    Direction=\"Up\"\n                    Duration=\"1500\" />\n            </animations:StoryBoard>\n\n        </ResourceDictionary>\n    </ContentPage.Resources>\n    <ContentPage.Triggers>\n        <EventTrigger Event=\"Appearing\">\n            <triggers:BeginAnimation Animation=\"{StaticResource ProductsAnimation}\" />\n        </EventTrigger>\n    </ContentPage.Triggers>\n    <Grid\n        ColumnSpacing=\"0\"\n        RowDefinitions=\"Auto,*\"\n        RowSpacing=\"0\">\n        <Image\n            x:Name=\"HeaderImage\"\n            Grid.RowSpan=\"2\"\n            Margin=\"0,60,0,0\"\n            Aspect=\"AspectFill\"\n            Source=\"header_home.png\"\n            VerticalOptions=\"Start\" />\n\n        <!--  CATALOG  -->\n        <CollectionView\n            x:Name=\"Products\"\n            Grid.Row=\"1\"\n            ItemSizingStrategy=\"MeasureFirstItem\"\n            ItemsSource=\"{Binding Products, Mode=OneTime}\"\n            Scrolled=\"Products_OnScrolled\"\n            SelectedItem=\"{Binding SelectedProduct, Mode=TwoWay}\"\n            SelectionChangedCommand=\"{Binding ViewCatalogItemCommand, Mode=OneTime}\"\n            SelectionChangedCommandParameter=\"{Binding SelectedItem, Source={RelativeSource Self}, x:DataType=CollectionView}\"\n            SelectionMode=\"Single\"\n            VerticalScrollBarVisibility=\"Never\">\n            <CollectionView.Header>\n                <Grid\n                    Padding=\"8\"\n                    MinimumHeightRequest=\"200\"\n                    RowDefinitions=\"*,Auto,Auto\"\n                    RowSpacing=\"4\">\n                    <Label\n                        Grid.Row=\"1\"\n                        FontAttributes=\"Bold\"\n                        FontSize=\"{StaticResource ExtraBigSize}\"\n                        Text=\"Ready for a new adventure?\" />\n                    <Label\n                        Grid.Row=\"2\"\n                        FontSize=\"{StaticResource MediumSize}\"\n                        Text=\"Start the season with the latest in clothing and equipment.\" />\n                </Grid>\n            </CollectionView.Header>\n            <CollectionView.EmptyView>\n                <Label\n                    HorizontalOptions=\"Center\"\n                    Text=\"NO PRODUCTS FOUND\"\n                    VerticalOptions=\"Center\" />\n            </CollectionView.EmptyView>\n            <CollectionView.ItemsLayout>\n                <GridItemsLayout Orientation=\"Vertical\" Span=\"{OnIdiom 2, Desktop=4}\" />\n            </CollectionView.ItemsLayout>\n            <CollectionView.ItemTemplate>\n                <DataTemplate>\n                    <templates:ProductTemplate />\n                </DataTemplate>\n            </CollectionView.ItemTemplate>\n        </CollectionView>\n\n        <views:BadgeView\n            x:Name=\"badge\"\n            Grid.Row=\"1\"\n            Margin=\"16\"\n            BadgeColor=\"{AppThemeBinding Light={StaticResource DarkBackgroundColor},\n                                         Dark={StaticResource LightBackgroundColor}}\"\n            HorizontalOptions=\"End\"\n            Text=\"{Binding BadgeCount, Mode=OneWay}\"\n            TextColor=\"{AppThemeBinding Light={StaticResource DarkForegroundColor},\n                                        Dark={StaticResource LightForegroundColor}}\"\n            VerticalOptions=\"End\">\n            <Button\n                Padding=\"8\"\n                BackgroundColor=\"{StaticResource LightGrayColor}\"\n                Command=\"{Binding ViewBasketCommand, Mode=OneTime}\"\n                CornerRadius=\"8\"\n                HeightRequest=\"56\"\n                ImageSource=\"{DynamicResource BasketIconForTitleImageSource}\"\n                WidthRequest=\"56\" />\n        </views:BadgeView>\n\n        <mct:Expander\n            Grid.Row=\"1\"\n            IsExpanded=\"{Binding IsFiltering, Mode=OneWay}\"\n            VerticalOptions=\"Start\">\n            <mct:Expander.Shadow>\n                <Shadow\n                    Brush=\"Black\"\n                    Opacity=\"1.0\"\n                    Radius=\"200\"\n                    Offset=\"0,20\" />\n            </mct:Expander.Shadow>\n            <mct:Expander.Content>\n                <Border StrokeShape=\"RoundRectangle 0,0,12,12\" StrokeThickness=\"0\">\n                    <ScrollView BackgroundColor=\"White\" VerticalOptions=\"Fill\">\n                        <VerticalStackLayout Padding=\"8,8,8,16\" Spacing=\"4\">\n                            <Label FontAttributes=\"Bold\" FontSize=\"{StaticResource LargerSize}\">Filters</Label>\n                            <BoxView\n                                Margin=\"4\"\n                                HeightRequest=\"1\"\n                                Color=\"Black\" />\n                            <Label FontAttributes=\"Bold\" Style=\"{StaticResource MediumSizeFontStyle}\">Brands</Label>\n                            <FlexLayout\n                                AlignContent=\"Start\"\n                                AlignItems=\"Start\"\n                                BindableLayout.ItemsSource=\"{Binding Brands, Mode=OneWay}\"\n                                Direction=\"Row\"\n                                Wrap=\"Wrap\">\n                                <BindableLayout.ItemTemplate>\n                                    <DataTemplate x:DataType=\"viewmodels:CatalogBrandSelectionViewModel\">\n                                        <Button\n                                            Margin=\"4\"\n                                            Command=\"{Binding Source={RelativeSource AncestorType={x:Type viewmodels:CatalogViewModel}}, Path=SelectCatalogBrandCommand, x:DataType=viewmodels:CatalogViewModel}\"\n                                            CommandParameter=\"{Binding Value}\"\n                                            Style=\"{StaticResource TransparentButtonStyle}\"\n                                            Text=\"{Binding Value.Brand, Mode=OneTime}\">\n                                            <Button.Triggers>\n                                                <DataTrigger\n                                                    Binding=\"{Binding Selected, x:DataType=viewmodels:CatalogBrandSelectionViewModel}\"\n                                                    TargetType=\"Button\"\n                                                    Value=\"True\">\n                                                    <Setter Property=\"TextColor\" Value=\"{StaticResource HighlightColor}\" />\n                                                </DataTrigger>\n                                            </Button.Triggers>\n                                        </Button>\n                                    </DataTemplate>\n                                </BindableLayout.ItemTemplate>\n                                <BindableLayout.EmptyView>\n                                    <Label HorizontalOptions=\"Fill\" Text=\"Loading\" />\n                                </BindableLayout.EmptyView>\n                            </FlexLayout>\n                            <Label FontAttributes=\"Bold\" Style=\"{StaticResource MediumSizeFontStyle}\">Types</Label>\n                            <FlexLayout\n                                AlignContent=\"Start\"\n                                AlignItems=\"Start\"\n                                BindableLayout.ItemsSource=\"{Binding Types, Mode=OneWay}\"\n                                Direction=\"Row\"\n                                Wrap=\"Wrap\">\n                                <BindableLayout.ItemTemplate>\n                                    <DataTemplate x:DataType=\"viewmodels:CatalogTypeSelectionViewModel\">\n                                        <Button\n                                            Margin=\"4\"\n                                            Command=\"{Binding Source={RelativeSource AncestorType={x:Type viewmodels:CatalogViewModel}}, Path=SelectCatalogTypeCommand, x:DataType=viewmodels:CatalogViewModel}\"\n                                            CommandParameter=\"{Binding Value}\"\n                                            Style=\"{StaticResource TransparentButtonStyle}\"\n                                            Text=\"{Binding Value.Type, Mode=OneTime}\">\n                                            <Button.Triggers>\n                                                <DataTrigger\n                                                    Binding=\"{Binding Selected, x:DataType=viewmodels:CatalogTypeSelectionViewModel}\"\n                                                    TargetType=\"Button\"\n                                                    Value=\"True\">\n                                                    <Setter Property=\"TextColor\" Value=\"{StaticResource HighlightColor}\" />\n                                                </DataTrigger>\n                                            </Button.Triggers>\n                                        </Button>\n                                    </DataTemplate>\n                                </BindableLayout.ItemTemplate>\n                                <BindableLayout.EmptyView>\n                                    <Label HorizontalOptions=\"Fill\" Text=\"Loading\" />\n                                </BindableLayout.EmptyView>\n                            </FlexLayout>\n                            <HorizontalStackLayout HorizontalOptions=\"End\">\n                                <Button\n                                    Command=\"{Binding ClearFilterCommand}\"\n                                    Style=\"{StaticResource TransparentButtonStyle}\"\n                                    Text=\"Clear\" />\n                                <Button\n                                    Command=\"{Binding ApplyFilterCommand}\"\n                                    CornerRadius=\"4\"\n                                    Text=\"Apply\" />\n                            </HorizontalStackLayout>\n                        </VerticalStackLayout>\n                    </ScrollView>\n                </Border>\n            </mct:Expander.Content>\n        </mct:Expander>\n\n\n        <!--  INDICATOR  -->\n        <ActivityIndicator\n            Grid.Row=\"1\"\n            HorizontalOptions=\"Center\"\n            IsRunning=\"{Binding IsBusy}\"\n            IsVisible=\"{Binding IsBusy}\"\n            VerticalOptions=\"Center\"\n            Color=\"{StaticResource BlackColor}\" />\n    </Grid>\n</views:ContentPageBase>"
  },
  {
    "path": "src/ClientApp/Views/CatalogView.xaml.cs",
    "content": "﻿using CommunityToolkit.Mvvm.Messaging;\nusing eShop.ClientApp.Messages;\n\nnamespace eShop.ClientApp.Views;\n\npublic partial class CatalogView\n{\n    public CatalogView(CatalogViewModel viewModel)\n    {\n        BindingContext = viewModel;\n\n        InitializeComponent();\n    }\n\n    protected override void OnAppearing()\n    {\n        base.OnAppearing();\n\n        WeakReferenceMessenger.Default\n            .Register<CatalogView, ProductCountChangedMessage>(\n                this,\n                async (recipient, message) =>\n                {\n                    await recipient.Dispatcher.DispatchAsync(\n                        async () =>\n                        {\n                            await recipient.badge.ScaleTo(1.2);\n                            await recipient.badge.ScaleTo(1.0);\n                        });\n                });\n    }\n\n    protected override void OnDisappearing()\n    {\n        base.OnDisappearing();\n\n        WeakReferenceMessenger.Default.Unregister<ProductCountChangedMessage>(this);\n    }\n\n    private void Products_OnScrolled(object sender, ItemsViewScrolledEventArgs e)\n    {\n        if (e.VerticalOffset < 0)\n        {\n            return;\n        }\n\n        if (e.VerticalOffset > 200)\n        {\n            HeaderImage.Opacity = 0d;\n            HeaderImage.Scale = 2.0;\n            return;\n        }\n\n        HeaderImage.Opacity = 1.0d - (e.VerticalOffset / 200d);\n        HeaderImage.Scale = 1.0d + (1.0d * (e.VerticalOffset / 200d));\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Views/CheckoutView.xaml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n\n<views:ContentPageBase\n    x:Class=\"eShop.ClientApp.Views.CheckoutView\"\n    xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n    xmlns:templates=\"clr-namespace:eShop.ClientApp.Views.Templates\"\n    xmlns:viewmodels=\"clr-namespace:eShop.ClientApp.ViewModels\"\n    xmlns:views=\"clr-namespace:eShop.ClientApp.Views\"\n    Title=\"CHECKOUT\"\n    x:DataType=\"viewmodels:CheckoutViewModel\">\n    <ContentPage.Resources>\n        <ResourceDictionary>\n\n            <Style x:Key=\"OrderTitleStyle\" TargetType=\"{x:Type Label}\">\n                <Setter Property=\"FontFamily\" Value=\"Montserrat-Regular\" />\n                <Setter Property=\"FontSize\" Value=\"{StaticResource MediumSize}\" />\n                <Setter Property=\"TextColor\" Value=\"Gray\" />\n            </Style>\n\n            <Style x:Key=\"OrderContentStyle\" TargetType=\"{x:Type Label}\">\n                <Setter Property=\"FontFamily\" Value=\"Montserrat-Regular\" />\n                <Setter Property=\"FontSize\" Value=\"{StaticResource MediumSize}\" />\n            </Style>\n\n            <Style\n                x:Key=\"ShippingAddressStyle\"\n                BasedOn=\"{StaticResource OrderTitleStyle}\"\n                TargetType=\"{x:Type Label}\">\n                <Setter Property=\"FontSize\" Value=\"{StaticResource LargeSize}\" />\n            </Style>\n\n            <Style\n                x:Key=\"AddressStyle\"\n                BasedOn=\"{StaticResource OrderContentStyle}\"\n                TargetType=\"{x:Type Label}\">\n                <Setter Property=\"FontSize\" Value=\"{StaticResource LittleSize}\" />\n            </Style>\n\n        </ResourceDictionary>\n    </ContentPage.Resources>\n    <Grid>\n        <Grid.RowDefinitions>\n            <RowDefinition Height=\"Auto\" />\n            <RowDefinition Height=\"*\" />\n            <RowDefinition Height=\"Auto\" />\n        </Grid.RowDefinitions>\n        <!--  ORDER INFO  -->\n        <Grid Grid.Row=\"0\" Padding=\"8\">\n            <Grid.ColumnDefinitions>\n                <ColumnDefinition Width=\"*\" />\n                <ColumnDefinition Width=\"*\" />\n            </Grid.ColumnDefinitions>\n            <Grid.RowDefinitions>\n                <RowDefinition Height=\"Auto\" />\n                <RowDefinition Height=\"Auto\" />\n                <RowDefinition Height=\"8\" />\n                <RowDefinition Height=\"Auto\" />\n                <RowDefinition Height=\"Auto\" />\n                <RowDefinition Height=\"8\" />\n                <RowDefinition Height=\"Auto\" />\n                <RowDefinition Height=\"Auto\" />\n            </Grid.RowDefinitions>\n\n            <Label\n                Grid.Row=\"0\"\n                Grid.Column=\"0\"\n                Style=\"{StaticResource OrderTitleStyle}\"\n                Text=\"ORDER NUMBER\" />\n            <Label\n                Grid.Row=\"1\"\n                Grid.Column=\"0\"\n                Style=\"{StaticResource OrderContentStyle}\"\n                Text=\"{Binding Order.OrderNumber}\" />\n\n            <Label\n                Grid.Row=\"3\"\n                Grid.Column=\"0\"\n                Style=\"{StaticResource OrderTitleStyle}\"\n                Text=\"TOTAL\" />\n            <Label\n                Grid.Row=\"4\"\n                Grid.Column=\"0\"\n                Style=\"{StaticResource OrderContentStyle}\"\n                Text=\"{Binding Order.Total, StringFormat='${0:N2}'}\" />\n\n            <Label\n                Grid.Row=\"0\"\n                Grid.Column=\"1\"\n                Style=\"{StaticResource OrderTitleStyle}\"\n                Text=\"DATE\" />\n            <Label\n                Grid.Row=\"1\"\n                Grid.Column=\"1\"\n                Style=\"{StaticResource OrderContentStyle}\"\n                Text=\"{Binding Order.OrderDate, StringFormat={StaticResource DateTimeFormat}}\" />\n\n            <Label\n                Grid.Row=\"3\"\n                Grid.Column=\"1\"\n                Style=\"{StaticResource OrderTitleStyle}\"\n                Text=\"STATUS\" />\n            <Label\n                Grid.Row=\"4\"\n                Grid.Column=\"1\"\n                Style=\"{StaticResource OrderContentStyle}\"\n                Text=\"{Binding Order.OrderStatus}\" />\n\n            <!--  SHIPPING ADDRESS  -->\n            <Label\n                Grid.Row=\"6\"\n                Grid.Column=\"0\"\n                Grid.ColumnSpan=\"2\"\n                Style=\"{StaticResource ShippingAddressStyle}\"\n                Text=\"SHIPPING ADDRESS\" />\n\n            <VerticalStackLayout\n                Grid.Row=\"7\"\n                Grid.Column=\"0\"\n                Grid.ColumnSpan=\"2\">\n                <Label Style=\"{StaticResource AddressStyle}\" Text=\"{Binding ShippingAddress.Street}\" />\n                <Label Style=\"{StaticResource AddressStyle}\" Text=\"{Binding ShippingAddress.ZipCode}\" />\n                <Label Style=\"{StaticResource AddressStyle}\" Text=\"{Binding ShippingAddress.State}\" />\n                <Label Style=\"{StaticResource AddressStyle}\" Text=\"{Binding ShippingAddress.Country}\" />\n            </VerticalStackLayout>\n        </Grid>\n        <!--  ORDER ITEMS  -->\n        <CollectionView\n            Grid.Row=\"1\"\n            ItemSizingStrategy=\"MeasureAllItems\"\n            ItemsSource=\"{Binding Order.OrderItems}\">\n            <CollectionView.ItemTemplate>\n                <DataTemplate>\n                    <templates:OrderItemTemplate />\n                </DataTemplate>\n            </CollectionView.ItemTemplate>\n        </CollectionView>\n        <!--  CHECKOUT  -->\n        <Grid\n            Grid.Row=\"2\"\n            Padding=\"16\"\n            BackgroundColor=\"{StaticResource LightGrayColor}\"\n            ColumnDefinitions=\"*\"\n            RowDefinitions=\"auto\">\n            <Button\n                Grid.Row=\"0\"\n                Grid.Column=\"0\"\n                BackgroundColor=\"Black\"\n                Command=\"{Binding CheckoutCommand}\"\n                CornerRadius=\"4\"\n                Text=\"Place order\" />\n        </Grid>\n        <!--  INDICATOR  -->\n        <ActivityIndicator\n            Grid.Row=\"0\"\n            Grid.RowSpan=\"2\"\n            HorizontalOptions=\"Center\"\n            IsRunning=\"{Binding IsBusy}\"\n            IsVisible=\"{Binding IsBusy}\"\n            VerticalOptions=\"Center\"\n            Color=\"{StaticResource HighlightColor}\">\n            <ActivityIndicator.WidthRequest>\n                <OnPlatform x:TypeArguments=\"x:Double\">\n                    <On Platform=\"iOS, Android\" Value=\"100\" />\n                    <On Platform=\"WinUI\" Value=\"400\" />\n                </OnPlatform>\n            </ActivityIndicator.WidthRequest>\n        </ActivityIndicator>\n    </Grid>\n</views:ContentPageBase>"
  },
  {
    "path": "src/ClientApp/Views/CheckoutView.xaml.cs",
    "content": "﻿namespace eShop.ClientApp.Views;\n\npublic partial class CheckoutView\n{\n    public CheckoutView(CheckoutViewModel viewModel)\n    {\n        BindingContext = viewModel;\n        InitializeComponent();\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Views/ContentPageBase.cs",
    "content": "using eShop.ClientApp.ViewModels.Base;\n\nnamespace eShop.ClientApp.Views;\n\npublic abstract class ContentPageBase : ContentPage\n{\n    public ContentPageBase()\n    {\n        NavigationPage.SetBackButtonTitle(this, string.Empty);\n    }\n\n    protected override async void OnAppearing()\n    {\n        base.OnAppearing();\n\n        if (BindingContext is not IViewModelBase ivmb)\n        {\n            return;\n        }\n\n        await ivmb.InitializeAsyncCommand.ExecuteAsync(null);\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Views/CustomNavigationView.xaml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n\n<NavigationPage\n    x:Class=\"eShop.ClientApp.Views.CustomNavigationView\"\n    xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n    BackgroundColor=\"Transparent\"\n    BarBackgroundColor=\"{StaticResource ImportantColor}\"\n    BarTextColor=\"{StaticResource WhiteColor}\" />"
  },
  {
    "path": "src/ClientApp/Views/CustomNavigationView.xaml.cs",
    "content": "﻿namespace eShop.ClientApp.Views;\n\npublic partial class CustomNavigationView : NavigationPage\n{\n    public CustomNavigationView()\n    {\n        InitializeComponent();\n    }\n\n    public CustomNavigationView(Page root) : base(root)\n    {\n        InitializeComponent();\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Views/FiltersView.xaml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n\n<ContentPage\n    x:Class=\"eShop.ClientApp.Views.FiltersView\"\n    xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n    xmlns:ios=\"clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls\"\n    xmlns:viewmodels=\"clr-namespace:eShop.ClientApp.ViewModels\"\n    x:DataType=\"viewmodels:CatalogViewModel\"\n    BackgroundColor=\"#01FFFFFF\"\n    Shell.PresentationMode=\"ModalAnimated\">\n\n    <Grid ColumnDefinitions=\"*, 2*, *\" VerticalOptions=\"Center\">\n        <Border\n            Grid.Row=\"0\"\n            Grid.Column=\"1\"\n            Background=\"{AppThemeBinding Light={StaticResource LightBackgroundColor},\n                                         Dark={StaticResource DarkBackgroundColor}}\">\n            <Border.StrokeShape>\n                <RoundRectangle CornerRadius=\"8\" />\n            </Border.StrokeShape>\n            <VerticalStackLayout\n                Padding=\"8\"\n                BackgroundColor=\"{AppThemeBinding Light={StaticResource LightBackgroundColor},\n                                                  Dark={StaticResource DarkBackgroundColor}}\"\n                Spacing=\"8\">\n                <!--  BRAND  -->\n                <Picker\n                    Title=\"BRAND\"\n                    ios:Picker.UpdateMode=\"WhenFinished\"\n                    ItemDisplayBinding=\"{Binding Value.Brand, x:DataType=viewmodels:CatalogBrandSelectionViewModel}\"\n                    ItemsSource=\"{Binding Brands}\"\n                    SelectedItem=\"{Binding SelectedBrand, Mode=TwoWay}\" />\n                <!--  TYPE  -->\n                <Picker\n                    Title=\"TYPE\"\n                    ios:Picker.UpdateMode=\"WhenFinished\"\n                    ItemDisplayBinding=\"{Binding Value.Type, x:DataType=viewmodels:CatalogTypeSelectionViewModel}\"\n                    ItemsSource=\"{Binding Types}\"\n                    SelectedItem=\"{Binding SelectedType, Mode=TwoWay}\" />\n                <Button Command=\"{Binding FilterCommand}\" Text=\"Apply\">\n                    <VisualStateManager.VisualStateGroups>\n                        <VisualStateGroup x:Name=\"CommonStates\">\n                            <VisualState x:Name=\"Normal\">\n                                <VisualState.Setters>\n                                    <Setter Property=\"Opacity\" Value=\"1\" />\n                                </VisualState.Setters>\n                            </VisualState>\n                            <VisualState x:Name=\"Disabled\">\n                                <VisualState.Setters>\n                                    <Setter Property=\"Opacity\" Value=\".5\" />\n                                </VisualState.Setters>\n                            </VisualState>\n                        </VisualStateGroup>\n                    </VisualStateManager.VisualStateGroups>\n                </Button>\n                <Button Command=\"{Binding ClearFilterCommand}\" Text=\"Clear\">\n                    <VisualStateManager.VisualStateGroups>\n                        <VisualStateGroup x:Name=\"CommonStates\">\n                            <VisualState x:Name=\"Normal\">\n                                <VisualState.Setters>\n                                    <Setter Property=\"Opacity\" Value=\"1\" />\n                                </VisualState.Setters>\n                            </VisualState>\n                            <VisualState x:Name=\"Disabled\">\n                                <VisualState.Setters>\n                                    <Setter Property=\"Opacity\" Value=\".5\" />\n                                </VisualState.Setters>\n                            </VisualState>\n                        </VisualStateGroup>\n                    </VisualStateManager.VisualStateGroups>\n                </Button>\n            </VerticalStackLayout>\n        </Border>\n    </Grid>\n</ContentPage>"
  },
  {
    "path": "src/ClientApp/Views/FiltersView.xaml.cs",
    "content": "﻿namespace eShop.ClientApp.Views;\n\npublic partial class FiltersView : ContentPage\n{\n    public FiltersView(CatalogViewModel viewModel)\n    {\n        BindingContext = viewModel;\n\n        InitializeComponent();\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Views/LoginView.xaml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n\n<views:ContentPageBase\n    x:Class=\"eShop.ClientApp.Views.LoginView\"\n    xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n    xmlns:animations=\"clr-namespace:eShop.ClientApp.Animations\"\n    xmlns:ios=\"clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls\"\n    xmlns:triggers=\"clr-namespace:eShop.ClientApp.Triggers\"\n    xmlns:viewmodels=\"clr-namespace:eShop.ClientApp.ViewModels\"\n    xmlns:views=\"clr-namespace:eShop.ClientApp.Views\"\n    ios:Page.UseSafeArea=\"False\"\n    x:DataType=\"viewmodels:LoginViewModel\"\n    BackgroundColor=\"{StaticResource LightGrayColor}\"\n    Shell.FlyoutBehavior=\"Disabled\"\n    Shell.NavBarIsVisible=\"False\"\n    Shell.TabBarIsVisible=\"False\">\n    <ContentPage.Title>\n        <OnPlatform x:TypeArguments=\"x:String\">\n            <On Platform=\"iOS, WinUI\" Value=\"AdventureWorks\" />\n        </OnPlatform>\n    </ContentPage.Title>\n    <ContentPage.Resources>\n        <ResourceDictionary>\n\n            <animations:StoryBoard x:Key=\"LoginAnimation\" Target=\"{x:Reference LoginPanel}\">\n                <animations:FadeInAnimation Direction=\"Up\" Duration=\"1500\" />\n            </animations:StoryBoard>\n\n        </ResourceDictionary>\n    </ContentPage.Resources>\n    <ContentPage.Triggers>\n        <EventTrigger Event=\"Appearing\">\n            <triggers:BeginAnimation Animation=\"{StaticResource LoginAnimation}\" />\n        </EventTrigger>\n    </ContentPage.Triggers>\n\n    <Grid\n        x:Name=\"LoginPanel\"\n        ColumnSpacing=\"0\"\n        IgnoreSafeArea=\"True\"\n        IsVisible=\"{Binding IsMock, Converter={StaticResource InverseBoolConverter}}\"\n        RowDefinitions=\"*,Auto,Auto,120\"\n        RowSpacing=\"0\">\n        <Image\n            x:Name=\"Banner\"\n            Aspect=\"AspectFill\"\n            Grid.RowSpan=\"4\"\n            Source=\"header.png\" />\n\n        <Image\n            Grid.Row=\"0\"\n            Grid.Column=\"0\"\n            Aspect=\"AspectFit\"\n            WidthRequest=\"250\"\n            HorizontalOptions=\"Center\"\n            Source=\"logo_header.png\"\n            VerticalOptions=\"Center\" />\n\n        <ImageButton\n            Grid.Row=\"0\"\n            Margin=\"16,60\"\n            Command=\"{Binding SettingsCommand}\"\n            HorizontalOptions=\"End\"\n            Source=\"{StaticResource SettingsIconDarkImageSource}\"\n            VerticalOptions=\"Start\" />\n\n        <Button\n              Grid.Row=\"2\"\n              Grid.Column=\"0\"\n              Margin=\"8\"\n              BackgroundColor=\"Black\"\n              Command=\"{Binding SignInCommand}\"\n              CornerRadius=\"16\"\n              Text=\"Login\"\n              TextColor=\"White\" />\n\n        <ActivityIndicator\n            HorizontalOptions=\"Center\"\n            IsRunning=\"{Binding IsBusy}\"\n            IsVisible=\"{Binding IsBusy}\"\n            VerticalOptions=\"Center\"\n            Color=\"{StaticResource BlackColor}\" />\n    </Grid>\n</views:ContentPageBase>"
  },
  {
    "path": "src/ClientApp/Views/LoginView.xaml.cs",
    "content": "﻿namespace eShop.ClientApp.Views;\n\npublic partial class LoginView\n{\n    private readonly LoginViewModel _viewModel;\n\n    public LoginView(LoginViewModel viewModel)\n    {\n        BindingContext = _viewModel = viewModel;\n        InitializeComponent();\n        //BannerScroll.ScrollToAsync(0, BannerScroll.ContentSize.Height, false);\n    }\n\n    protected override void OnAppearing()\n    {\n        var content = Content;\n        Content = null;\n        Content = content;\n\n        _viewModel.InvalidateMock();\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Views/MapView.xaml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n\n<views:ContentPageBase\n    x:Class=\"eShop.ClientApp.Views.MapView\"\n    xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n    xmlns:maps=\"http://schemas.microsoft.com/dotnet/2021/maui/maps\"\n    xmlns:sensors=\"clr-namespace:Microsoft.Maui.Devices.Sensors;assembly=Microsoft.Maui.Essentials\"\n    xmlns:views=\"clr-namespace:eShop.ClientApp.Views\"\n    xmlns:vm=\"clr-namespace:eShop.ClientApp.ViewModels\"\n    Title=\"Microsoft Visitor Center\"\n    x:DataType=\"vm:MapViewModel\">\n    <maps:Map\n        IsScrollEnabled=\"False\"\n        IsZoomEnabled=\"False\"\n        ItemsSource=\"{Binding Stores, Mode=OneWay}\">\n        <x:Arguments>\n            <maps:MapSpan>\n                <x:Arguments>\n                    <sensors:Location>\n                        <x:Arguments>\n                            <x:Double>47.6423109</x:Double>\n                            <x:Double>-122.1368406</x:Double>\n                        </x:Arguments>\n                    </sensors:Location>\n                    <x:Double>0.01</x:Double>\n                    <x:Double>0.01</x:Double>\n                </x:Arguments>\n            </maps:MapSpan>\n        </x:Arguments>\n        <maps:Map.ItemTemplate>\n            <DataTemplate>\n                <maps:Pin\n                    x:DataType=\"vm:Store\"\n                    Address=\"{Binding Address}\"\n                    Label=\"{Binding Description}\"\n                    Location=\"{Binding Location}\"\n                    MarkerClicked=\"Pin_MarkerClicked\" />\n            </DataTemplate>\n        </maps:Map.ItemTemplate>\n    </maps:Map>\n</views:ContentPageBase>"
  },
  {
    "path": "src/ClientApp/Views/MapView.xaml.cs",
    "content": "﻿using Microsoft.Maui.Controls.Maps;\nusing Microsoft.Maui.Maps;\nusing Map = Microsoft.Maui.Controls.Maps.Map;\n\nnamespace eShop.ClientApp.Views;\n\npublic partial class MapView\n{\n    public MapView(MapViewModel viewModel)\n    {\n        BindingContext = viewModel;\n        InitializeComponent();\n    }\n\n    private async void Pin_MarkerClicked(Object sender, PinClickedEventArgs e)\n    {\n        e.HideInfoWindow = true;\n\n        if (sender is not Pin pin)\n        {\n            return;\n        }\n\n        await pin.Location.OpenMapsAsync();\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Views/MauiAuthenticationBrowser.cs",
    "content": "using IdentityModel.Client;\nusing IdentityModel.OidcClient.Browser;\nusing IBrowser = IdentityModel.OidcClient.Browser.IBrowser;\n\nnamespace eShop.ClientApp.Views;\n\npublic class MauiAuthenticationBrowser : IBrowser\n{\n    public async Task<BrowserResult> InvokeAsync(BrowserOptions options, CancellationToken cancellationToken = default)\n    {\n        try\n        {\n            var result = await WebAuthenticator.Default.AuthenticateAsync(\n                new Uri(options.StartUrl),\n                new Uri(options.EndUrl));\n\n            var url = new RequestUrl(\"maui://authcallback\")\n                .Create(new Parameters(result.Properties));\n\n            return new BrowserResult {Response = url, ResultType = BrowserResultType.Success};\n        }\n        catch (TaskCanceledException)\n        {\n            return new BrowserResult {ResultType = BrowserResultType.UserCancel};\n        }\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Views/OrderDetailView.xaml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n\n<views:ContentPageBase\n    x:Class=\"eShop.ClientApp.Views.OrderDetailView\"\n    xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n    xmlns:animations=\"clr-namespace:eShop.ClientApp.Animations\"\n    xmlns:templates=\"clr-namespace:eShop.ClientApp.Views.Templates\"\n    xmlns:triggers=\"clr-namespace:eShop.ClientApp.Triggers\"\n    xmlns:viewmodels=\"clr-namespace:eShop.ClientApp.ViewModels\"\n    xmlns:views=\"clr-namespace:eShop.ClientApp.Views\"\n    Title=\"{Binding Order.OrderNumber, StringFormat='ORDER {0}'}\"\n    x:DataType=\"viewmodels:OrderDetailViewModel\">\n    <ContentPage.Resources>\n        <ResourceDictionary>\n\n            <Style\n                x:Key=\"TitleStyle\"\n                BasedOn=\"{StaticResource MediumSizeFontStyle}\"\n                TargetType=\"{x:Type Label}\">\n                <Setter Property=\"TextColor\" Value=\"Gray\" />\n            </Style>\n\n            <Style x:Key=\"OrderContentStyle\" TargetType=\"{x:Type Label}\" />\n\n            <Style x:Key=\"AddressStyle\" TargetType=\"{x:Type Label}\">\n                <Setter Property=\"FontSize\" Value=\"{StaticResource LittleSize}\" />\n            </Style>\n\n            <Style\n                x:Key=\"OrderTotalStyle\"\n                BasedOn=\"{StaticResource MediumSizeFontStyle}\"\n                TargetType=\"{x:Type Label}\">\n                <Setter Property=\"FontFamily\" Value=\"Montserrat-Regular\" />\n                <Setter Property=\"FontSize\" Value=\"{StaticResource LargerSize}\" />\n                <Setter Property=\"HorizontalOptions\" Value=\"End\" />\n            </Style>\n\n            <Style x:Key=\"CancelOrderButtonStyle\" TargetType=\"{x:Type Button}\">\n                <Setter Property=\"TextColor\" Value=\"{StaticResource WhiteColor}\" />\n                <Setter Property=\"BackgroundColor\" Value=\"{StaticResource HighlightColor}\" />\n\n            </Style>\n\n            <animations:StoryBoard x:Key=\"OrderInfoAnimation\" Target=\"{x:Reference OrderInfo}\">\n                <animations:FadeToAnimation\n                    Delay=\"100\"\n                    Opacity=\"1\"\n                    Duration=\"500\" />\n            </animations:StoryBoard>\n\n            <animations:StoryBoard x:Key=\"OrderItemsAnimation\" Target=\"{x:Reference OrderItems}\">\n                <animations:FadeToAnimation\n                    Delay=\"300\"\n                    Opacity=\"1\"\n                    Duration=\"1500\" />\n            </animations:StoryBoard>\n\n        </ResourceDictionary>\n    </ContentPage.Resources>\n    <ContentPage.Triggers>\n        <EventTrigger Event=\"Appearing\">\n            <triggers:BeginAnimation Animation=\"{StaticResource OrderInfoAnimation}\" />\n            <triggers:BeginAnimation Animation=\"{StaticResource OrderItemsAnimation}\" />\n        </EventTrigger>\n    </ContentPage.Triggers>\n    <Grid>\n        <Grid.RowDefinitions>\n            <RowDefinition Height=\"Auto\" />\n            <RowDefinition Height=\"Auto\" />\n            <RowDefinition Height=\"*\" />\n        </Grid.RowDefinitions>\n        <StackLayout\n            Grid.Row=\"0\"\n            HeightRequest=\"50\"\n            IsVisible=\"{Binding IsSubmittedOrder}\">\n            <Button\n                Command=\"{Binding ToggleCancelOrderCommand}\"\n                Style=\"{StaticResource CancelOrderButtonStyle}\"\n                Text=\"CANCEL ORDER\" />\n        </StackLayout>\n        <Grid Grid.Row=\"1\">\n            <Grid.RowDefinitions>\n                <RowDefinition Height=\"Auto\" />\n                <RowDefinition Height=\"Auto\" />\n            </Grid.RowDefinitions>\n            <!--  ORDER INFO  -->\n            <Grid\n                x:Name=\"OrderInfo\"\n                Padding=\"8\"\n                Opacity=\"0\">\n                <Grid.ColumnDefinitions>\n                    <ColumnDefinition />\n                    <ColumnDefinition />\n                </Grid.ColumnDefinitions>\n                <Grid.RowDefinitions>\n                    <RowDefinition Height=\"Auto\" />\n                    <RowDefinition Height=\"Auto\" />\n                    <RowDefinition Height=\"8\" />\n                    <RowDefinition Height=\"Auto\" />\n                    <RowDefinition Height=\"Auto\" />\n                    <RowDefinition Height=\"8\" />\n                    <RowDefinition Height=\"Auto\" />\n                    <RowDefinition Height=\"Auto\" />\n                </Grid.RowDefinitions>\n\n                <Label\n                    Grid.Row=\"0\"\n                    Grid.Column=\"0\"\n                    Style=\"{StaticResource TitleStyle}\"\n                    Text=\"ORDER NUMBER\" />\n                <Label\n                    Grid.Row=\"1\"\n                    Grid.Column=\"0\"\n                    Style=\"{StaticResource OrderContentStyle}\"\n                    Text=\"{Binding Order.OrderNumber}\" />\n\n                <Label\n                    Grid.Row=\"3\"\n                    Grid.Column=\"0\"\n                    Style=\"{StaticResource TitleStyle}\"\n                    Text=\"TOTAL\" />\n                <Label\n                    Grid.Row=\"4\"\n                    Grid.Column=\"0\"\n                    Style=\"{StaticResource OrderContentStyle}\"\n                    Text=\"{Binding Order.Total, StringFormat='${0:N2}'}\" />\n\n                <Label\n                    Grid.Row=\"0\"\n                    Grid.Column=\"1\"\n                    Style=\"{StaticResource TitleStyle}\"\n                    Text=\"DATE\" />\n                <Label\n                    Grid.Row=\"1\"\n                    Grid.Column=\"1\"\n                    Style=\"{StaticResource OrderContentStyle}\"\n                    Text=\"{Binding Order.OrderDate, StringFormat={StaticResource DateTimeFormat}}\" />\n\n                <Label\n                    Grid.Row=\"3\"\n                    Grid.Column=\"1\"\n                    Style=\"{StaticResource TitleStyle}\"\n                    Text=\"STATUS\" />\n                <Label\n                    Grid.Row=\"4\"\n                    Grid.Column=\"1\"\n                    Style=\"{StaticResource OrderContentStyle}\"\n                    Text=\"{Binding Order.OrderStatus}\" />\n\n                <!--  SHIPPING ADDRESS  -->\n                <Label\n                    Grid.Row=\"6\"\n                    Grid.Column=\"0\"\n                    Grid.ColumnSpan=\"2\"\n                    Style=\"{StaticResource TitleStyle}\"\n                    Text=\"SHIPPING ADDRESS\" />\n                <VerticalStackLayout\n                    Grid.Row=\"7\"\n                    Grid.Column=\"0\"\n                    Grid.ColumnSpan=\"2\">\n                    <Label Style=\"{StaticResource AddressStyle}\" Text=\"{Binding Order.ShippingStreet}\" />\n                    <Label Style=\"{StaticResource AddressStyle}\" Text=\"{Binding Order.ShippingCity}\" />\n                    <Label Style=\"{StaticResource AddressStyle}\" Text=\"{Binding Order.ShippingState}\" />\n                    <Label Style=\"{StaticResource AddressStyle}\" Text=\"{Binding Order.ShippingCountry}\" />\n                </VerticalStackLayout>\n            </Grid>\n        </Grid>\n        <!--  ORDER ITEMS  -->\n        <CollectionView\n            x:Name=\"OrderItems\"\n            Grid.Row=\"2\"\n            ItemSizingStrategy=\"MeasureAllItems\"\n            ItemsSource=\"{Binding Order.OrderItems}\">\n            <CollectionView.ItemTemplate>\n                <DataTemplate>\n                    <templates:OrderItemTemplate />\n                </DataTemplate>\n            </CollectionView.ItemTemplate>\n            <CollectionView.Footer>\n                <VerticalStackLayout Padding=\"8\">\n                    <Label Style=\"{StaticResource OrderTotalStyle}\" Text=\"TOTAL\" />\n                    <Label\n                        Style=\"{StaticResource OrderTotalStyle}\"\n                        Text=\"{Binding Order.Total, StringFormat='${0:N2}'}\"\n                        TextColor=\"{StaticResource ImportantColor}\" />\n                </VerticalStackLayout>\n            </CollectionView.Footer>\n        </CollectionView>\n        <!--  INDICATOR  -->\n        <ActivityIndicator\n            Grid.Row=\"1\"\n            HorizontalOptions=\"Center\"\n            IsRunning=\"{Binding IsBusy}\"\n            IsVisible=\"{Binding IsBusy}\"\n            VerticalOptions=\"Center\"\n            Color=\"{StaticResource HighlightColor}\">\n            <ActivityIndicator.WidthRequest>\n                <OnPlatform x:TypeArguments=\"x:Double\">\n                    <On Platform=\"iOS, Android\" Value=\"100\" />\n                    <On Platform=\"WinUI\" Value=\"400\" />\n                </OnPlatform>\n            </ActivityIndicator.WidthRequest>\n        </ActivityIndicator>\n    </Grid>\n</views:ContentPageBase>"
  },
  {
    "path": "src/ClientApp/Views/OrderDetailView.xaml.cs",
    "content": "﻿namespace eShop.ClientApp.Views;\n\npublic partial class OrderDetailView\n{\n    public OrderDetailView(OrderDetailViewModel viewModel)\n    {\n        BindingContext = viewModel;\n        InitializeComponent();\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Views/ProfileView.xaml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n\n<views:ContentPageBase\n    x:Class=\"eShop.ClientApp.Views.ProfileView\"\n    xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n    xmlns:templates=\"clr-namespace:eShop.ClientApp.Views.Templates\"\n    xmlns:viewmodels=\"clr-namespace:eShop.ClientApp.ViewModels\"\n    xmlns:views=\"clr-namespace:eShop.ClientApp.Views\"\n    Title=\"Orders\"\n    x:DataType=\"viewmodels:ProfileViewModel\">\n    <ContentPage.Resources>\n        <ResourceDictionary>\n\n            <Style x:Key=\"MyOrdersLabelStyle\" TargetType=\"{x:Type Label}\">\n                <Setter Property=\"FontFamily\" Value=\"Montserrat-Regular\" />\n                <Setter Property=\"FontSize\" Value=\"{StaticResource MediumSize}\" />\n                <Setter Property=\"HorizontalTextAlignment\" Value=\"Center\" />\n                <Setter Property=\"VerticalTextAlignment\" Value=\"Center\" />\n                <Setter Property=\"Margin\" Value=\"0,12\" />\n            </Style>\n\n        </ResourceDictionary>\n    </ContentPage.Resources>\n    <ContentPage.ToolbarItems>\n        <ToolbarItem Command=\"{Binding LogoutCommand}\" Text=\"LOGOUT\" />\n    </ContentPage.ToolbarItems>\n\n    <!--  ORDERS  -->\n    <Grid RowDefinitions=\"*,Auto\">\n        <Grid IsVisible=\"{Binding IsBusy, Converter={StaticResource InverseBoolConverter}}\">\n            <Label\n                HorizontalOptions=\"Center\"\n                IsVisible=\"{Binding Orders.Count, Mode=OneWay, Converter={StaticResource DoesNotHaveCountConverter}}\"\n                Text=\"NO ORDERS\"\n                VerticalOptions=\"Center\" />\n        </Grid>\n        <CollectionView\n            ItemSizingStrategy=\"MeasureAllItems\"\n            ItemsSource=\"{Binding Orders}\"\n            SelectedItem=\"{Binding SelectedOrder, Mode=TwoWay}\"\n            SelectionChangedCommand=\"{Binding OrderDetailCommand}\"\n            SelectionChangedCommandParameter=\"{Binding SelectedItem, Source={RelativeSource Self}, x:DataType=CollectionView}\"\n            SelectionMode=\"Single\">\n            <CollectionView.ItemTemplate>\n                <DataTemplate>\n                    <templates:OrderTemplate />\n                </DataTemplate>\n            </CollectionView.ItemTemplate>\n        </CollectionView>\n    </Grid>\n</views:ContentPageBase>"
  },
  {
    "path": "src/ClientApp/Views/ProfileView.xaml.cs",
    "content": "﻿namespace eShop.ClientApp.Views;\n\npublic partial class ProfileView\n{\n    private readonly ProfileViewModel _viewModel;\n\n    public ProfileView(ProfileViewModel viewModel)\n    {\n        _viewModel = viewModel;\n        BindingContext = viewModel;\n        InitializeComponent();\n    }\n\n    protected override void OnAppearing()\n    {\n        base.OnAppearing();\n\n        if (_viewModel.IsInitialized)\n        {\n            _viewModel.RefreshCommand.Execute(null);\n        }\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Views/SettingsView.xaml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n\n<ContentPage\n    x:Class=\"eShop.ClientApp.Views.SettingsView\"\n    xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n    xmlns:animations=\"clr-namespace:eShop.ClientApp.Animations\"\n    xmlns:controls=\"clr-namespace:eShop.ClientApp.Controls\"\n    xmlns:converters=\"clr-namespace:eShop.ClientApp.Converters\"\n    xmlns:triggers=\"clr-namespace:eShop.ClientApp.Triggers\"\n    xmlns:viewmodels=\"clr-namespace:eShop.ClientApp.ViewModels\"\n    Title=\"SETTINGS\"\n    x:DataType=\"viewmodels:SettingsViewModel\"\n    Shell.FlyoutBehavior=\"Disabled\"\n    Shell.NavBarIsVisible=\"True\"\n    Shell.TabBarIsVisible=\"False\">\n    <ContentPage.Resources>\n        <ResourceDictionary>\n\n            <Style x:Key=\"SettingsStackLayoutStyle\" TargetType=\"{x:Type StackLayout}\">\n                <Setter Property=\"Margin\" Value=\"6\" />\n            </Style>\n\n            <Style\n                x:Key=\"SettingsTitleStyle\"\n                BasedOn=\"{StaticResource MediumSizeFontStyle}\"\n                TargetType=\"{x:Type Label}\">\n                <Setter Property=\"HorizontalOptions\" Value=\"Start\" />\n                <Setter Property=\"VerticalOptions\" Value=\"Center\" />\n                <Setter Property=\"Margin\" Value=\"12, 0\" />\n                <Setter Property=\"TextColor\" Value=\"{StaticResource ImportantColor}\" />\n            </Style>\n\n            <Style\n                x:Key=\"SettingsDescriptionStyle\"\n                BasedOn=\"{StaticResource LittleSizeFontStyle}\"\n                TargetType=\"{x:Type Label}\">\n                <Setter Property=\"HorizontalOptions\" Value=\"Start\" />\n                <Setter Property=\"VerticalOptions\" Value=\"Center\" />\n                <Setter Property=\"Margin\" Value=\"12, 0\" />\n            </Style>\n\n            <Style x:Key=\"SettingsToggleButtonStyle\" TargetType=\"{x:Type controls:ToggleButton}\">\n                <Setter Property=\"HeightRequest\" Value=\"48\" />\n                <Setter Property=\"WidthRequest\" Value=\"48\" />\n                <Setter Property=\"VerticalOptions\" Value=\"Center\" />\n                <Setter Property=\"HorizontalOptions\" Value=\"Center\" />\n                <Setter Property=\"Margin\" Value=\"12,0\" />\n                <Setter Property=\"Animate\" Value=\"True\" />\n            </Style>\n\n            <Style\n                x:Key=\"SettingsEntryStyle\"\n                BasedOn=\"{StaticResource EntryStyle}\"\n                TargetType=\"{x:Type Entry}\">\n                <Setter Property=\"Margin\" Value=\"12, 0\" />\n            </Style>\n\n            <Style\n                x:Key=\"SettingsWinUIEntryStyle\"\n                BasedOn=\"{StaticResource WinUIEntryStyle}\"\n                TargetType=\"{x:Type Entry}\">\n                <Setter Property=\"Margin\" Value=\"12, 0\" />\n            </Style>\n\n            <Style x:Key=\"HeaderLabelStyle\" TargetType=\"{x:Type Label}\">\n                <Setter Property=\"FontFamily\" Value=\"Montserrat-Regular\" />\n                <Setter Property=\"FontSize\" Value=\"{StaticResource LittleSize}\" />\n                <Setter Property=\"TextColor\" Value=\"{StaticResource GreenColor}\" />\n                <Setter Property=\"HorizontalOptions\" Value=\"Start\" />\n                <Setter Property=\"Margin\" Value=\"12,0\" />\n            </Style>\n\n            <animations:StoryBoard x:Key=\"MockServicesAnimation\" Target=\"{x:Reference MockServices}\">\n                <animations:FadeInAnimation Direction=\"Up\">\n                    <animations:FadeInAnimation.Duration>\n                        <OnPlatform x:TypeArguments=\"x:String\">\n                            <On Platform=\"iOS\" Value=\"0\" />\n                            <On Platform=\"Android, WinUI\" Value=\"500\" />\n                        </OnPlatform>\n                    </animations:FadeInAnimation.Duration>\n                </animations:FadeInAnimation>\n            </animations:StoryBoard>\n\n        </ResourceDictionary>\n    </ContentPage.Resources>\n    <ContentPage.Triggers>\n        <EventTrigger Event=\"Appearing\">\n            <triggers:BeginAnimation Animation=\"{StaticResource MockServicesAnimation}\" />\n        </EventTrigger>\n    </ContentPage.Triggers>\n\n    <!--  SETTINGS  -->\n    <Grid x:Name=\"MockServices\">\n        <Grid.ColumnDefinitions>\n            <ColumnDefinition Width=\"*\" />\n            <ColumnDefinition Width=\"Auto\" />\n        </Grid.ColumnDefinitions>\n        <Grid.RowDefinitions>\n            <RowDefinition Height=\"Auto\" />\n            <RowDefinition Height=\"Auto\" />\n            <RowDefinition Height=\"Auto\" />\n            <RowDefinition Height=\"Auto\" />\n            <RowDefinition Height=\"Auto\" />\n        </Grid.RowDefinitions>\n        <!--  MOCK SERVICES  -->\n        <StackLayout Style=\"{StaticResource SettingsStackLayoutStyle}\">\n            <Label Style=\"{StaticResource SettingsTitleStyle}\" Text=\"{Binding TitleUseAzureServices}\" />\n            <Label Style=\"{StaticResource SettingsDescriptionStyle}\" Text=\"{Binding DescriptionUseAzureServices}\" />\n        </StackLayout>\n        <!--  ON / OFF  -->\n        <controls:ToggleButton\n            Grid.Column=\"1\"\n            Checked=\"{Binding UseAzureServices, Mode=TwoWay}\"\n            CheckedImage=\"{StaticResource ToggleOnImageSource}\"\n            Command=\"{Binding ToggleMockServicesCommand}\"\n            Style=\"{StaticResource SettingsToggleButtonStyle}\"\n            UnCheckedImage=\"{StaticResource ToggleOffImageSource}\" />\n        <!--  ENDPOINT  -->\n        <StackLayout\n            Grid.Row=\"1\"\n            Grid.ColumnSpan=\"2\"\n            IsVisible=\"{Binding UseAzureServices}\"\n            Style=\"{StaticResource SettingsStackLayoutStyle}\">\n            <Label Style=\"{StaticResource HeaderLabelStyle}\" Text=\"Identity Url\" />\n            <Entry Text=\"{Binding IdentityEndpoint, Mode=TwoWay}\">\n                <Entry.Style>\n                    <OnPlatform x:TypeArguments=\"Style\">\n                        <On Platform=\"iOS, Android\" Value=\"{StaticResource SettingsEntryStyle}\" />\n                        <On Platform=\"WinUI\" Value=\"{StaticResource SettingsWinUIEntryStyle}\" />\n                    </OnPlatform>\n                </Entry.Style>\n            </Entry>\n\n            <Label Style=\"{StaticResource HeaderLabelStyle}\" Text=\"Gateway Catalog Url\" />\n            <Entry Text=\"{Binding GatewayCatalogEndpoint, Mode=TwoWay}\">\n                <Entry.Style>\n                    <OnPlatform x:TypeArguments=\"Style\">\n                        <On Platform=\"iOS, Android\" Value=\"{StaticResource SettingsEntryStyle}\" />\n                        <On Platform=\"WinUI\" Value=\"{StaticResource SettingsWinUIEntryStyle}\" />\n                    </OnPlatform>\n                </Entry.Style>\n            </Entry>\n\n            <Label Style=\"{StaticResource HeaderLabelStyle}\" Text=\"Gateway Orders Url\" />\n            <Entry Text=\"{Binding GatewayOrdersEndpoint, Mode=TwoWay}\">\n                <Entry.Style>\n                    <OnPlatform x:TypeArguments=\"Style\">\n                        <On Platform=\"iOS, Android\" Value=\"{StaticResource SettingsEntryStyle}\" />\n                        <On Platform=\"WinUI\" Value=\"{StaticResource SettingsWinUIEntryStyle}\" />\n                    </OnPlatform>\n                </Entry.Style>\n            </Entry>\n\n            <Label Style=\"{StaticResource HeaderLabelStyle}\" Text=\"Gateway Basket Url\" />\n            <Entry Text=\"{Binding GatewayBasketEndpoint, Mode=TwoWay}\">\n                <Entry.Style>\n                    <OnPlatform x:TypeArguments=\"Style\">\n                        <On Platform=\"iOS, Android\" Value=\"{StaticResource SettingsEntryStyle}\" />\n                        <On Platform=\"WinUI\" Value=\"{StaticResource SettingsWinUIEntryStyle}\" />\n                    </OnPlatform>\n                </Entry.Style>\n            </Entry>\n        </StackLayout>\n        <!--  USE LOCATIONS  -->\n        <!--\n        <StackLayout\n            Grid.Row=\"2\"\n            IsVisible=\"{Binding UserIsLogged}\"\n            Style=\"{StaticResource SettingsStackLayoutStyle}\">\n            <Label\n                Text=\"{Binding TitleUseFakeLocation}\"\n                Style=\"{StaticResource SettingsTitleStyle}\"/>\n            <Label\n                Text=\"{Binding DescriptionUseFakeLocation}\"\n                Style=\"{StaticResource SettingsDescriptionStyle}\"/>\n        </StackLayout>\n        -->\n        <!--  ON / OFF  -->\n        <!--\n        <controls:ToggleButton\n            Grid.Column=\"1\"\n            Grid.Row=\"2\"\n            Checked=\"{Binding UseFakeLocation, Mode=TwoWay}\"\n            Command=\"{Binding ToggleFakeLocationCommand}\"\n            Style=\"{StaticResource SettingsToggleButtonStyle}\"\n            IsVisible=\"{Binding UserIsLogged}\"\n            CheckedImage=\"{StaticResource ToggleOnImageSource}\"\n            UnCheckedImage=\"{StaticResource ToggleOffImageSource}\" />\n        -->\n        <!--  FAKE LOCATIONS  -->\n        <!--\n        <StackLayout\n            Grid.Row=\"3\"\n            Grid.ColumnSpan=\"2\"\n            Style=\"{StaticResource SettingsStackLayoutStyle}\"\n            IsVisible=\"{Binding UseFakeLocation}\">\n            <Label\n                Text=\"Latitude\"\n                Style=\"{StaticResource HeaderLabelStyle}\"/>\n            <Entry\n                Text=\"{Binding Latitude, Mode=TwoWay, Converter={StaticResource DoubleConverter}}\"\n                Keyboard=\"Text\">\n                <Entry.Style>\n                    <OnPlatform x:TypeArguments=\"Style\">\n                        <On Platform=\"iOS, Android\" Value=\"{StaticResource SettingsEntryStyle}\" />\n                        <On Platform=\"WinUI\" Value=\"{StaticResource SettingsWinUIEntryStyle}\" />\n                    </OnPlatform>\n                </Entry.Style>\n            </Entry>\n            <Label\n                Text=\"Longitude\"\n                Style=\"{StaticResource HeaderLabelStyle}\"/>\n            <Entry\n                Text=\"{Binding Longitude, Mode=TwoWay, Converter={StaticResource DoubleConverter}}\"\n                Keyboard=\"Text\">\n                <Entry.Style>\n                    <OnPlatform x:TypeArguments=\"Style\">\n                        <On Platform=\"iOS, Android\" Value=\"{StaticResource SettingsEntryStyle}\" />\n                        <On Platform=\"WinUI\" Value=\"{StaticResource SettingsWinUIEntryStyle}\" />\n                    </OnPlatform>\n                </Entry.Style>\n            </Entry>\n            <Button\n                Command=\"{Binding ToggleSendLocationCommand}\"\n                Text=\"Send Location\"/>\n        </StackLayout>\n        -->\n        <!--  ALLOW GPS LOCATION  -->\n        <!--\n        <StackLayout\n            Grid.Row=\"4\"\n            Style=\"{StaticResource SettingsStackLayoutStyle}\"\n            IsVisible=\"{Binding UseFakeLocation, Converter={StaticResource InverseBoolConverter}}\">\n            <Label\n                Text=\"{Binding TitleAllowGpsLocation}\"\n                Style=\"{StaticResource SettingsTitleStyle}\"/>\n            <Label\n                Text=\"{Binding DescriptionAllowGpsLocation}\"\n                Style=\"{StaticResource SettingsDescriptionStyle}\"/>\n            <Label\n                Text=\"{Binding GpsWarningMessage}\"\n                Style=\"{StaticResource SettingsWarningMessageStyle}\"/>\n        </StackLayout>\n        -->\n        <!--  ON / OFF  -->\n        <!--\n        <controls:ToggleButton\n            Grid.Column=\"1\"\n            Grid.Row=\"4\"\n            Checked=\"{Binding AllowGpsLocation, Mode=TwoWay}\"\n            Command=\"{Binding ToggleAllowGpsLocationCommand}\"\n            Style=\"{StaticResource SettingsToggleButtonStyle}\"\n            IsVisible=\"{Binding UseFakeLocation, Converter={StaticResource InverseBoolConverter}}\"\n            CheckedImage=\"{StaticResource ToggleOnImageSource}\"\n            UnCheckedImage=\"{StaticResource ToggleOffImageSource}\" />-->\n    </Grid>\n</ContentPage>"
  },
  {
    "path": "src/ClientApp/Views/SettingsView.xaml.cs",
    "content": "﻿namespace eShop.ClientApp.Views;\n\npublic partial class SettingsView : ContentPage\n{\n    public SettingsView(SettingsViewModel viewModel)\n    {\n        BindingContext = viewModel;\n        InitializeComponent();\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Views/Templates/BasketItemTemplate.xaml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<SwipeView\n    x:Class=\"eShop.ClientApp.Views.Templates.BasketItemTemplate\"\n    xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n    xmlns:models=\"clr-namespace:eShop.ClientApp.Models.Basket\"\n    xmlns:vm=\"clr-namespace:eShop.ClientApp.ViewModels\"\n    Padding=\"4\"\n    x:DataType=\"models:BasketItem\"\n    HeightRequest=\"88\">\n    <SwipeView.Resources>\n        <ResourceDictionary>\n            <Style x:Key=\"OrderItemUnitPriceStyle\" TargetType=\"{x:Type Label}\">\n                <Setter Property=\"FontFamily\" Value=\"Montserrat-Regular\" />\n                <Setter Property=\"FontSize\" Value=\"{StaticResource MidMediumSize}\" />\n                <Setter Property=\"HorizontalOptions\" Value=\"Start\" />\n            </Style>\n\n            <Style x:Key=\"OrderTotalStyle\" TargetType=\"{x:Type Label}\">\n                <Setter Property=\"FontFamily\" Value=\"Montserrat-Regular\" />\n                <Setter Property=\"FontSize\" Value=\"{StaticResource LargerSize}\" />\n                <Setter Property=\"TextColor\" Value=\"{StaticResource ImportantColor}\" />\n                <Setter Property=\"HorizontalOptions\" Value=\"End\" />\n            </Style>\n\n            <Style x:Key=\"QuantityStyle\" TargetType=\"{x:Type Label}\">\n                <Setter Property=\"FontFamily\" Value=\"Montserrat-Regular\" />\n                <Setter Property=\"HorizontalOptions\" Value=\"End\" />\n                <Setter Property=\"VerticalOptions\" Value=\"Fill\" />\n            </Style>\n\n        </ResourceDictionary>\n    </SwipeView.Resources>\n    <SwipeView.RightItems>\n        <SwipeItems>\n            <SwipeItem\n                BackgroundColor=\"Red\"\n                Command=\"{Binding Source={RelativeSource AncestorType={x:Type vm:BasketViewModel}}, Path=DeleteCommand, x:DataType=vm:BasketViewModel}\"\n                CommandParameter=\"{Binding}\"\n                Text=\"Delete\" />\n        </SwipeItems>\n    </SwipeView.RightItems>\n    <Grid BackgroundColor=\"{AppThemeBinding Dark={StaticResource DarkBackgroundColor}, Light={StaticResource LightBackgroundColor}}\">\n        <Grid.Padding>\n            <OnPlatform x:TypeArguments=\"Thickness\">\n                <On Platform=\"iOS, Android\" Value=\"0\" />\n                <On Platform=\"WinUI\" Value=\"12, 0\" />\n            </OnPlatform>\n        </Grid.Padding>\n        <Grid.ColumnDefinitions>\n            <ColumnDefinition Width=\"Auto\" />\n            <ColumnDefinition Width=\"*\" />\n        </Grid.ColumnDefinitions>\n        <Grid.RowDefinitions>\n            <RowDefinition Height=\"*\" />\n            <RowDefinition Height=\"Auto\" />\n            <RowDefinition Height=\"1\" />\n        </Grid.RowDefinitions>\n        <!--  IMAGE  -->\n        <Image\n            Grid.Row=\"0\"\n            Grid.Column=\"0\"\n            Margin=\"12,0,0,0\"\n            Aspect=\"AspectFit\"\n            HeightRequest=\"74\"\n            Source=\"{Binding PictureUrl}\"\n            VerticalOptions=\"Center\"\n            WidthRequest=\"74\" />\n        <Grid\n            Grid.Row=\"0\"\n            Grid.Column=\"1\"\n            Margin=\"6\">\n            <Grid.RowDefinitions>\n                <RowDefinition />\n                <RowDefinition />\n                <RowDefinition />\n            </Grid.RowDefinitions>\n            <!--  NAME  -->\n            <Label Grid.Row=\"0\" Text=\"{Binding ProductName, Converter={StaticResource ToUpperConverter}}\" />\n            <Grid Grid.Row=\"1\">\n                <Grid.ColumnDefinitions>\n                    <ColumnDefinition />\n                    <ColumnDefinition />\n                </Grid.ColumnDefinitions>\n                <!--  PRICE  -->\n                <Label\n                    Grid.Column=\"0\"\n                    Style=\"{StaticResource OrderItemUnitPriceStyle}\"\n                    Text=\"{Binding UnitPrice, StringFormat='${0:N2}'}\" />\n                <!--  QUANTITY  -->\n                <Label\n                    Grid.Column=\"1\"\n                    Style=\"{StaticResource QuantityStyle}\"\n                    Text=\"{Binding Quantity}\" />\n            </Grid>\n            <!--  TOTAL  -->\n            <Label\n                Grid.Row=\"2\"\n                Style=\"{StaticResource OrderTotalStyle}\"\n                Text=\"{Binding Total, StringFormat='${0:N2}'}\" />\n            <!--  DELETE BUTTON IF ON DESKTOP  -->\n            <Button\n                Grid.Row=\"2\"\n                Command=\"{Binding Source={RelativeSource AncestorType={x:Type vm:BasketViewModel}}, Path=DeleteCommand, x:DataType=vm:BasketViewModel}\"\n                CommandParameter=\"{Binding}\"\n                HorizontalOptions=\"Start\"\n                IsVisible=\"{OnIdiom False,\n                                    Desktop=True}\"\n                Text=\"Delete\" />\n        </Grid>\n        <Grid\n            Grid.Row=\"1\"\n            Grid.Column=\"0\"\n            Grid.ColumnSpan=\"2\"\n            BackgroundColor=\"#F0AD4E\"\n            IsVisible=\"{Binding HasNewPrice}\">\n            <Label\n                HeightRequest=\"60\"\n                HorizontalOptions=\"Fill\"\n                HorizontalTextAlignment=\"Center\"\n                Text=\"{Binding OldUnitPrice, StringFormat='Note that the price of this article changed in our Catalog. The old price when you originally added it to the basket was $ {0:N2}'}\"\n                VerticalOptions=\"Fill\"\n                VerticalTextAlignment=\"Center\" />\n        </Grid>\n        <Grid\n            Grid.Row=\"2\"\n            Grid.Column=\"0\"\n            Grid.ColumnSpan=\"2\"\n            BackgroundColor=\"Gray\" />\n    </Grid>\n</SwipeView>"
  },
  {
    "path": "src/ClientApp/Views/Templates/BasketItemTemplate.xaml.cs",
    "content": "﻿namespace eShop.ClientApp.Views.Templates;\n\npublic partial class BasketItemTemplate\n{\n    public BasketItemTemplate()\n    {\n        InitializeComponent();\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Views/Templates/CampaignTemplate.xaml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n\n<ContentView\n    x:Class=\"eShop.ClientApp.Views.Templates.CampaignTemplate\"\n    xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n    xmlns:models=\"clr-namespace:eShop.ClientApp.Models.Marketing\"\n    x:DataType=\"models:CampaignItem\">\n    <ContentView.Resources>\n        <ResourceDictionary>\n\n            <Style x:Key=\"CampaignNameStyle\" TargetType=\"{x:Type Label}\">\n                <Setter Property=\"FontFamily\" Value=\"Montserrat-Regular\" />\n                <Setter Property=\"FontSize\" Value=\"{StaticResource LargeSize}\" />\n                <Setter Property=\"HorizontalOptions\" Value=\"Center\" />\n                <Setter Property=\"Margin\" Value=\"0, 12, 0, 6\" />\n            </Style>\n\n        </ResourceDictionary>\n    </ContentView.Resources>\n    <ContentView.Content>\n        <Grid Margin=\"0\" Padding=\"0\">\n            <Grid.RowDefinitions>\n                <RowDefinition Height=\"250\" />\n                <RowDefinition Height=\"Auto\" />\n            </Grid.RowDefinitions>\n            <!--  IMAGE  -->\n            <Image\n                Grid.Row=\"0\"\n                Aspect=\"AspectFill\"\n                Source=\"{Binding PictureUri, TargetNullValue='logo_header.png'}\" />\n            <!--  NAME  -->\n            <Label\n                Grid.Row=\"1\"\n                Style=\"{StaticResource CampaignNameStyle}\"\n                Text=\"{Binding Name, Converter={StaticResource ToUpperConverter}}\" />\n        </Grid>\n    </ContentView.Content>\n</ContentView>"
  },
  {
    "path": "src/ClientApp/Views/Templates/CampaignTemplate.xaml.cs",
    "content": "﻿namespace eShop.ClientApp.Views.Templates;\n\npublic partial class CampaignTemplate : ContentView\n{\n    public CampaignTemplate()\n    {\n        InitializeComponent();\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Views/Templates/OrderItemTemplate.xaml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n\n<ContentView\n    x:Class=\"eShop.ClientApp.Views.Templates.OrderItemTemplate\"\n    xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n    xmlns:models=\"clr-namespace:eShop.ClientApp.Models.Orders\"\n    x:DataType=\"models:OrderItem\">\n    <ContentView.Resources>\n        <ResourceDictionary>\n\n            <Style\n                x:Key=\"OrderItemTitleStyle\"\n                BasedOn=\"{StaticResource MediumSizeFontStyle}\"\n                TargetType=\"{x:Type Label}\">\n                <Setter Property=\"LineBreakMode\" Value=\"HeadTruncation\" />\n            </Style>\n\n            <Style x:Key=\"OrderItemUnitPriceStyle\" TargetType=\"{x:Type Label}\">\n                <Setter Property=\"FontFamily\" Value=\"Montserrat-Regular\" />\n                <Setter Property=\"FontSize\" Value=\"{StaticResource MidMediumSize}\" />\n                <Setter Property=\"HorizontalOptions\" Value=\"Start\" />\n            </Style>\n\n            <Style x:Key=\"OrderItemQuantityStyle\" TargetType=\"{x:Type Label}\">\n                <Setter Property=\"FontFamily\" Value=\"Montserrat-Regular\" />\n                <Setter Property=\"FontSize\" Value=\"{StaticResource MidMediumSize}\" />\n                <Setter Property=\"HorizontalOptions\" Value=\"End\" />\n            </Style>\n\n            <Style x:Key=\"OrderTotalStyle\" TargetType=\"{x:Type Label}\">\n                <Setter Property=\"FontFamily\" Value=\"Montserrat-Regular\" />\n                <Setter Property=\"FontSize\" Value=\"{StaticResource LargerSize}\" />\n                <Setter Property=\"TextColor\" Value=\"{StaticResource ImportantColor}\" />\n                <Setter Property=\"HorizontalOptions\" Value=\"End\" />\n            </Style>\n\n        </ResourceDictionary>\n    </ContentView.Resources>\n    <ContentView.Content>\n        <Grid HeightRequest=\"120\">\n            <Grid.ColumnDefinitions>\n                <ColumnDefinition Width=\"Auto\" />\n                <ColumnDefinition Width=\"*\" />\n            </Grid.ColumnDefinitions>\n            <Grid.RowDefinitions>\n                <RowDefinition Height=\"*\" />\n                <RowDefinition Height=\"1\" />\n            </Grid.RowDefinitions>\n            <!--  IMAGE  -->\n            <Image\n                Grid.Row=\"0\"\n                Grid.Column=\"0\"\n                Margin=\"12,0,0,0\"\n                Aspect=\"AspectFit\"\n                HeightRequest=\"108\"\n                Source=\"{Binding PictureUrl}\"\n                VerticalOptions=\"Start\"\n                WidthRequest=\"108\" />\n            <Grid\n                Grid.Row=\"0\"\n                Grid.Column=\"1\"\n                Margin=\"6\">\n                <Grid.RowDefinitions>\n                    <RowDefinition />\n                    <RowDefinition />\n                    <RowDefinition />\n                </Grid.RowDefinitions>\n                <!--  NAME  -->\n                <Label\n                    Grid.Row=\"0\"\n                    Style=\"{StaticResource OrderItemTitleStyle}\"\n                    Text=\"{Binding ProductName, Converter={StaticResource ToUpperConverter}}\" />\n                <Grid Grid.Row=\"1\">\n                    <Grid.ColumnDefinitions>\n                        <ColumnDefinition />\n                        <ColumnDefinition />\n                    </Grid.ColumnDefinitions>\n                    <!--  PRICE  -->\n                    <Label\n                        Grid.Column=\"0\"\n                        Style=\"{StaticResource OrderItemUnitPriceStyle}\"\n                        Text=\"{Binding UnitPrice, StringFormat='${0:N2}'}\" />\n                    <Label\n                        Grid.Column=\"1\"\n                        Style=\"{StaticResource OrderItemQuantityStyle}\"\n                        Text=\"{Binding Quantity}\" />\n                </Grid>\n                <!--  TOTAL  -->\n                <Label\n                    Grid.Row=\"2\"\n                    Style=\"{StaticResource OrderTotalStyle}\"\n                    Text=\"{Binding Total, StringFormat='${0:N2}'}\" />\n            </Grid>\n            <Grid\n                Grid.Row=\"1\"\n                Grid.Column=\"0\"\n                Grid.ColumnSpan=\"2\"\n                BackgroundColor=\"{StaticResource SeparatorLineColor}\" />\n        </Grid>\n    </ContentView.Content>\n</ContentView>"
  },
  {
    "path": "src/ClientApp/Views/Templates/OrderItemTemplate.xaml.cs",
    "content": "﻿namespace eShop.ClientApp.Views.Templates;\n\npublic partial class OrderItemTemplate : ContentView\n{\n    public OrderItemTemplate()\n    {\n        InitializeComponent();\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Views/Templates/OrderTemplate.xaml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n\n<ContentView\n    x:Class=\"eShop.ClientApp.Views.Templates.OrderTemplate\"\n    xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n    xmlns:models=\"clr-namespace:eShop.ClientApp.Models.Orders\"\n    x:DataType=\"models:Order\">\n    <ContentView.Resources>\n        <ResourceDictionary>\n\n            <Style\n                x:Key=\"OrderTitleStyle\"\n                BasedOn=\"{StaticResource MediumSizeFontStyle}\"\n                TargetType=\"{x:Type Label}\">\n                <Setter Property=\"TextColor\" Value=\"Gray\" />\n            </Style>\n\n        </ResourceDictionary>\n    </ContentView.Resources>\n    <ContentView.Content>\n        <Grid>\n            <Grid.ColumnDefinitions>\n                <ColumnDefinition />\n                <ColumnDefinition />\n            </Grid.ColumnDefinitions>\n            <Grid.RowDefinitions>\n                <RowDefinition />\n                <RowDefinition />\n                <RowDefinition Height=\"1\" />\n            </Grid.RowDefinitions>\n            <StackLayout\n                Grid.Row=\"0\"\n                Grid.Column=\"0\"\n                Margin=\"12\">\n                <Label Style=\"{StaticResource OrderTitleStyle}\" Text=\"ORDER NUMBER\" />\n                <Label Text=\"{Binding OrderNumber}\" />\n            </StackLayout>\n            <StackLayout\n                Grid.Row=\"1\"\n                Grid.Column=\"0\"\n                Margin=\"12\">\n                <Label Style=\"{StaticResource OrderTitleStyle}\" Text=\"TOTAL\" />\n                <Label Text=\"{Binding Total, StringFormat='${0:N2}'}\" />\n            </StackLayout>\n            <StackLayout\n                Grid.Row=\"0\"\n                Grid.Column=\"1\"\n                Margin=\"12\">\n                <Label Style=\"{StaticResource OrderTitleStyle}\" Text=\"DATE\" />\n                <Label Text=\"{Binding OrderDate, StringFormat={StaticResource DateTimeFormat}}\" />\n            </StackLayout>\n            <StackLayout\n                Grid.Row=\"1\"\n                Grid.Column=\"1\"\n                Margin=\"12\">\n                <Label Style=\"{StaticResource OrderTitleStyle}\" Text=\"STATUS\" />\n                <Label Text=\"{Binding OrderStatus}\" />\n            </StackLayout>\n            <Grid\n                Grid.Row=\"2\"\n                Grid.Column=\"0\"\n                Grid.ColumnSpan=\"2\"\n                BackgroundColor=\"Gray\" />\n        </Grid>\n    </ContentView.Content>\n</ContentView>"
  },
  {
    "path": "src/ClientApp/Views/Templates/OrderTemplate.xaml.cs",
    "content": "﻿namespace eShop.ClientApp.Views.Templates;\n\npublic partial class OrderTemplate : ContentView\n{\n    public OrderTemplate()\n    {\n        InitializeComponent();\n    }\n}\n"
  },
  {
    "path": "src/ClientApp/Views/Templates/ProductTemplate.xaml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n\n<ContentView\n    x:Class=\"eShop.ClientApp.Views.Templates.ProductTemplate\"\n    xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n    xmlns:models=\"clr-namespace:eShop.ClientApp.Models.Catalog\"\n    x:DataType=\"models:CatalogItem\"\n    HeightRequest=\"260\">\n    <ContentView.Resources>\n        <ResourceDictionary>\n\n            <Style x:Key=\"ProductNameStyle\" TargetType=\"{x:Type Label}\">\n                <Setter Property=\"FontFamily\" Value=\"Montserrat-Regular\" />\n                <Setter Property=\"FontSize\" Value=\"{StaticResource MediumSize}\" />\n                <Setter Property=\"HorizontalOptions\" Value=\"Center\" />\n                <Setter Property=\"Margin\" Value=\"0, 12, 0, 6\" />\n            </Style>\n\n            <Style x:Key=\"ProductPriceStyle\" TargetType=\"{x:Type Label}\">\n                <Setter Property=\"FontAttributes\" Value=\"Bold\" />\n                <Setter Property=\"FontFamily\" Value=\"Montserrat-Regular\" />\n                <Setter Property=\"FontSize\" Value=\"{StaticResource MidMediumSize}\" />\n                <Setter Property=\"HorizontalOptions\" Value=\"Center\" />\n                <Setter Property=\"Margin\" Value=\"0, 6, 0, 12\" />\n            </Style>\n\n        </ResourceDictionary>\n    </ContentView.Resources>\n    <ContentView.Content>\n        <Grid Margin=\"0\" Padding=\"10\" ColumnSpacing=\"10\" ColumnDefinitions=\"*,Auto\"\n              RowDefinitions=\"Auto,*\">\n            <!--  IMAGE  -->\n            <Border\n                Grid.Row=\"0\"\n                Grid.Column=\"0\"\n                Grid.ColumnSpan=\"2\"\n                Margin=\"16\"\n                BackgroundColor=\"Gray\"\n                HeightRequest=\"150\"\n                StrokeShape=\"RoundRectangle 8,8,8,8\"\n                StrokeThickness=\"1\"\n                VerticalOptions=\"Start\"\n                WidthRequest=\"150\">\n                <Image Aspect=\"AspectFill\" Source=\"{Binding PictureUri, Mode=OneTime, TargetNullValue='logo_header.png'}\" />\n            </Border>\n\n            <!--  NAME  -->\n            <Label\n                Grid.Row=\"2\"\n                Grid.Column=\"0\"\n                HorizontalOptions=\"Fill\"\n                Style=\"{StaticResource ProductNameStyle}\"\n                Text=\"{Binding Name, Mode=OneTime, Converter={StaticResource ToUpperConverter}}\"\n                VerticalTextAlignment=\"Start\"\n                VerticalOptions=\"Start\"\n                />\n            <!--  PRICE  -->\n            <Label\n                Grid.Row=\"1\"\n                Grid.Column=\"1\"\n                MaxLines=\"1\"\n                Style=\"{StaticResource ProductPriceStyle}\"\n                Text=\"{Binding Price, Mode=OneTime, StringFormat='${0:N2}'}\"\n                VerticalTextAlignment=\"Start\" />\n        </Grid>\n    </ContentView.Content>\n    <VisualStateManager.VisualStateGroups>\n        <VisualStateGroup Name=\"CommonStates\">\n            <VisualState Name=\"Selected\">\n                <VisualState.Setters>\n                    <Setter Property=\"BackgroundColor\" Value=\"Transparent\" />\n                </VisualState.Setters>\n            </VisualState>\n        </VisualStateGroup>\n    </VisualStateManager.VisualStateGroups>\n</ContentView>"
  },
  {
    "path": "src/ClientApp/Views/Templates/ProductTemplate.xaml.cs",
    "content": "﻿namespace eShop.ClientApp.Views.Templates;\n\npublic partial class ProductTemplate : ContentView\n{\n    public ProductTemplate()\n    {\n        InitializeComponent();\n    }\n}\n"
  },
  {
    "path": "src/EventBus/Abstractions/EventBusSubscriptionInfo.cs",
    "content": "﻿using System.Text.Json;\nusing System.Text.Json.Serialization.Metadata;\n\nnamespace eShop.EventBus.Abstractions;\n\npublic class EventBusSubscriptionInfo\n{\n    public Dictionary<string, Type> EventTypes { get; } = [];\n\n    public JsonSerializerOptions JsonSerializerOptions { get; } = new(DefaultSerializerOptions);\n\n    internal static readonly JsonSerializerOptions DefaultSerializerOptions = new()\n    {\n        TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault ? CreateDefaultTypeResolver() : JsonTypeInfoResolver.Combine()\n    };\n\n#pragma warning disable IL2026\n#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.\n    private static IJsonTypeInfoResolver CreateDefaultTypeResolver()\n        => new DefaultJsonTypeInfoResolver();\n#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.\n#pragma warning restore IL2026\n}\n"
  },
  {
    "path": "src/EventBus/Abstractions/IEventBus.cs",
    "content": "﻿namespace eShop.EventBus.Abstractions;\n\npublic interface IEventBus\n{\n    Task PublishAsync(IntegrationEvent @event);\n}\n"
  },
  {
    "path": "src/EventBus/Abstractions/IEventBusBuilder.cs",
    "content": "﻿namespace Microsoft.Extensions.DependencyInjection;\n\npublic interface IEventBusBuilder\n{\n    public IServiceCollection Services { get; }\n}\n"
  },
  {
    "path": "src/EventBus/Abstractions/IIntegrationEventHandler.cs",
    "content": "﻿namespace eShop.EventBus.Abstractions;\n\npublic interface IIntegrationEventHandler<in TIntegrationEvent> : IIntegrationEventHandler\n    where TIntegrationEvent : IntegrationEvent\n{\n    Task Handle(TIntegrationEvent @event);\n\n    Task IIntegrationEventHandler.Handle(IntegrationEvent @event) => Handle((TIntegrationEvent)@event);\n}\n\npublic interface IIntegrationEventHandler\n{\n    Task Handle(IntegrationEvent @event);\n}\n"
  },
  {
    "path": "src/EventBus/EventBus.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <RootNamespace>eShop.EventBus</RootNamespace>\n    <IsAotCompatible>true</IsAotCompatible>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.Extensions.Options\" />\n  </ItemGroup>\n\n</Project>"
  },
  {
    "path": "src/EventBus/Events/IntegrationEvent.cs",
    "content": "﻿namespace eShop.EventBus.Events;\n\npublic record IntegrationEvent\n{\n    public IntegrationEvent()\n    {\n        Id = Guid.NewGuid();\n        CreationDate = DateTime.UtcNow;\n    }\n\n    [JsonInclude]\n    public Guid Id { get; set; }\n\n    [JsonInclude]\n    public DateTime CreationDate { get; set; }\n}\n"
  },
  {
    "path": "src/EventBus/Extensions/EventBusBuilderExtensions.cs",
    "content": "﻿using System.Diagnostics.CodeAnalysis;\nusing System.Text.Json;\nusing eShop.EventBus.Abstractions;\nusing eShop.EventBus.Extensions;\n\nnamespace Microsoft.Extensions.DependencyInjection;\n\npublic static class EventBusBuilderExtensions\n{\n    public static IEventBusBuilder ConfigureJsonOptions(this IEventBusBuilder eventBusBuilder, Action<JsonSerializerOptions> configure)\n    {\n        eventBusBuilder.Services.Configure<EventBusSubscriptionInfo>(o =>\n        {\n            configure(o.JsonSerializerOptions);\n        });\n\n        return eventBusBuilder;\n    }\n\n    public static IEventBusBuilder AddSubscription<T, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TH>(this IEventBusBuilder eventBusBuilder)\n        where T : IntegrationEvent\n        where TH : class, IIntegrationEventHandler<T>\n    {\n        // Use keyed services to register multiple handlers for the same event type\n        // the consumer can use IKeyedServiceProvider.GetKeyedService<IIntegrationEventHandler>(typeof(T)) to get all\n        // handlers for the event type.\n        eventBusBuilder.Services.AddKeyedTransient<IIntegrationEventHandler, TH>(typeof(T));\n\n        eventBusBuilder.Services.Configure<EventBusSubscriptionInfo>(o =>\n        {\n            // Keep track of all registered event types and their name mapping. We send these event types over the message bus\n            // and we don't want to do Type.GetType, so we keep track of the name mapping here.\n\n            // This list will also be used to subscribe to events from the underlying message broker implementation.\n            o.EventTypes[typeof(T).Name] = typeof(T);\n        });\n\n        return eventBusBuilder;\n    }\n}\n"
  },
  {
    "path": "src/EventBus/Extensions/GenericTypeExtensions.cs",
    "content": "﻿namespace eShop.EventBus.Extensions;\n\npublic static class GenericTypeExtensions\n{\n    public static string GetGenericTypeName(this Type type)\n    {\n        string typeName;\n\n        if (type.IsGenericType)\n        {\n            var genericTypes = string.Join(\",\", type.GetGenericArguments().Select(t => t.Name).ToArray());\n            typeName = $\"{type.Name.Remove(type.Name.IndexOf('`'))}<{genericTypes}>\";\n        }\n        else\n        {\n            typeName = type.Name;\n        }\n\n        return typeName;\n    }\n\n    public static string GetGenericTypeName(this object @object)\n    {\n        return @object.GetType().GetGenericTypeName();\n    }\n}\n"
  },
  {
    "path": "src/EventBus/GlobalUsings.cs",
    "content": "﻿global using System.Text.Json.Serialization;\nglobal using eShop.EventBus.Events;\n"
  },
  {
    "path": "src/EventBusRabbitMQ/EventBusOptions.cs",
    "content": "﻿namespace eShop.EventBusRabbitMQ;\n\npublic class EventBusOptions\n{\n    public string SubscriptionClientName { get; set; }\n    public int RetryCount { get; set; } = 10;\n}\n"
  },
  {
    "path": "src/EventBusRabbitMQ/EventBusRabbitMQ.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <RootNamespace>eShop.EventBusRabbitMQ</RootNamespace>\n    <IsAotCompatible>true</IsAotCompatible>\n    <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <Compile Include=\"..\\Shared\\ActivityExtensions.cs\" Link=\"ActivityExtensions.cs\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Aspire.RabbitMQ.Client\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\EventBus\\EventBus.csproj\" />\n  </ItemGroup>\n\n</Project>"
  },
  {
    "path": "src/EventBusRabbitMQ/GlobalUsings.cs",
    "content": "﻿global using System.Net.Sockets;\nglobal using System.Text;\nglobal using System.Text.Json;\nglobal using eShop.EventBus.Abstractions;\nglobal using eShop.EventBus.Events;\nglobal using Microsoft.Extensions.Logging;\nglobal using Polly;\nglobal using RabbitMQ.Client;\nglobal using RabbitMQ.Client.Events;\nglobal using RabbitMQ.Client.Exceptions;\n"
  },
  {
    "path": "src/EventBusRabbitMQ/RabbitMQEventBus.cs",
    "content": "﻿namespace eShop.EventBusRabbitMQ;\n\nusing System.Diagnostics;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Threading;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Options;\nusing OpenTelemetry;\nusing OpenTelemetry.Context.Propagation;\nusing Polly.Retry;\n\npublic sealed class RabbitMQEventBus(\n    ILogger<RabbitMQEventBus> logger,\n    IServiceProvider serviceProvider,\n    IOptions<EventBusOptions> options,\n    IOptions<EventBusSubscriptionInfo> subscriptionOptions,\n    RabbitMQTelemetry rabbitMQTelemetry) : IEventBus, IDisposable, IHostedService\n{\n    private const string ExchangeName = \"eshop_event_bus\";\n\n    private readonly ResiliencePipeline _pipeline = CreateResiliencePipeline(options.Value.RetryCount);\n    private readonly TextMapPropagator _propagator = rabbitMQTelemetry.Propagator;\n    private readonly ActivitySource _activitySource = rabbitMQTelemetry.ActivitySource;\n    private readonly string _queueName = options.Value.SubscriptionClientName;\n    private readonly EventBusSubscriptionInfo _subscriptionInfo = subscriptionOptions.Value;\n    private IConnection _rabbitMQConnection;\n\n    private IChannel _consumerChannel;\n\n    public async Task PublishAsync(IntegrationEvent @event)\n    {\n        var routingKey = @event.GetType().Name;\n\n        if (logger.IsEnabled(LogLevel.Trace))\n        {\n            logger.LogTrace(\"Creating RabbitMQ channel to publish event: {EventId} ({EventName})\", @event.Id, routingKey);\n        }\n\n        using var channel = (await _rabbitMQConnection?.CreateChannelAsync()) ?? throw new InvalidOperationException(\"RabbitMQ connection is not open\");\n\n        if (logger.IsEnabled(LogLevel.Trace))\n        {\n            logger.LogTrace(\"Declaring RabbitMQ exchange to publish event: {EventId}\", @event.Id);\n        }\n\n        await channel.ExchangeDeclareAsync(\n            exchange: ExchangeName, \n            type: \"direct\");\n\n        var body = SerializeMessage(@event);\n\n        // Start an activity with a name following the semantic convention of the OpenTelemetry messaging specification.\n        // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/messaging/messaging-spans.md\n        var activityName = $\"{routingKey} publish\";\n\n        await _pipeline.Execute(async () =>\n        {\n            using var activity = _activitySource.StartActivity(activityName, ActivityKind.Client);\n\n            // Depending on Sampling (and whether a listener is registered or not), the activity above may not be created.\n            // If it is created, then propagate its context. If it is not created, the propagate the Current context, if any.\n\n            ActivityContext contextToInject = default;\n\n            if (activity != null)\n            {\n                contextToInject = activity.Context;\n            }\n            else if (Activity.Current != null)\n            {\n                contextToInject = Activity.Current.Context;\n            }\n\n            var properties = new BasicProperties()\n            {\n                DeliveryMode = DeliveryModes.Persistent\n            };\n\n            static void InjectTraceContextIntoBasicProperties(IBasicProperties props, string key, string value)\n            {\n                props.Headers ??= new Dictionary<string, object>();\n                props.Headers[key] = value;\n            }\n\n            _propagator.Inject(new PropagationContext(contextToInject, Baggage.Current), properties, InjectTraceContextIntoBasicProperties);\n\n            SetActivityContext(activity, routingKey, \"publish\");\n\n            if (logger.IsEnabled(LogLevel.Trace))\n            {\n                logger.LogTrace(\"Publishing event to RabbitMQ: {EventId}\", @event.Id);\n            }\n\n            try\n            {\n                await channel.BasicPublishAsync(\n                    exchange: ExchangeName,\n                    routingKey: routingKey,\n                    mandatory: true,\n                    basicProperties: properties,\n                    body: body);\n            }\n            catch (Exception ex)\n            {\n                activity.SetExceptionTags(ex);\n\n                throw;\n            }\n        });\n    }\n\n    private static void SetActivityContext(Activity activity, string routingKey, string operation)\n    {\n        if (activity is not null)\n        {\n            // These tags are added demonstrating the semantic conventions of the OpenTelemetry messaging specification\n            // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/messaging/messaging-spans.md\n            activity.SetTag(\"messaging.system\", \"rabbitmq\");\n            activity.SetTag(\"messaging.destination_kind\", \"queue\");\n            activity.SetTag(\"messaging.operation\", operation);\n            activity.SetTag(\"messaging.destination.name\", routingKey);\n            activity.SetTag(\"messaging.rabbitmq.routing_key\", routingKey);\n        }\n    }\n\n    public void Dispose()\n    {\n        _consumerChannel?.Dispose();\n    }\n\n    private async Task OnMessageReceived(object sender, BasicDeliverEventArgs eventArgs)\n    {\n        static IEnumerable<string> ExtractTraceContextFromBasicProperties(IReadOnlyBasicProperties props, string key)\n        {\n            if (props.Headers.TryGetValue(key, out var value))\n            {\n                var bytes = value as byte[];\n                return [Encoding.UTF8.GetString(bytes)];\n            }\n            return [];\n        }\n\n        // Extract the PropagationContext of the upstream parent from the message headers.\n        var parentContext = _propagator.Extract(default, eventArgs.BasicProperties, ExtractTraceContextFromBasicProperties);\n        Baggage.Current = parentContext.Baggage;\n\n        // Start an activity with a name following the semantic convention of the OpenTelemetry messaging specification.\n        // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/messaging/messaging-spans.md\n        var activityName = $\"{eventArgs.RoutingKey} receive\";\n\n        using var activity = _activitySource.StartActivity(activityName, ActivityKind.Client, parentContext.ActivityContext);\n\n        SetActivityContext(activity, eventArgs.RoutingKey, \"receive\");\n\n        var eventName = eventArgs.RoutingKey;\n        var message = Encoding.UTF8.GetString(eventArgs.Body.Span);\n\n        try\n        {\n            activity?.SetTag(\"message\", message);\n\n            if (message.Contains(\"throw-fake-exception\", StringComparison.InvariantCultureIgnoreCase))\n            {\n                throw new InvalidOperationException($\"Fake exception requested: \\\"{message}\\\"\");\n            }\n\n            await ProcessEvent(eventName, message);\n        }\n        catch (Exception ex)\n        {\n            logger.LogWarning(ex, \"Error Processing message \\\"{Message}\\\"\", message);\n\n            activity.SetExceptionTags(ex);\n        }\n\n        // Even on exception we take the message off the queue.\n        // in a REAL WORLD app this should be handled with a Dead Letter Exchange (DLX). \n        // For more information see: https://www.rabbitmq.com/dlx.html\n        await _consumerChannel.BasicAckAsync(eventArgs.DeliveryTag, multiple: false);\n    }\n\n    private async Task ProcessEvent(string eventName, string message)\n    {\n        if (logger.IsEnabled(LogLevel.Trace))\n        {\n            logger.LogTrace(\"Processing RabbitMQ event: {EventName}\", eventName);\n        }\n\n        await using var scope = serviceProvider.CreateAsyncScope();\n\n        if (!_subscriptionInfo.EventTypes.TryGetValue(eventName, out var eventType))\n        {\n            logger.LogWarning(\"Unable to resolve event type for event name {EventName}\", eventName);\n            return;\n        }\n\n        // Deserialize the event\n        var integrationEvent = DeserializeMessage(message, eventType);\n        \n        // REVIEW: This could be done in parallel\n\n        // Get all the handlers using the event type as the key\n        foreach (var handler in scope.ServiceProvider.GetKeyedServices<IIntegrationEventHandler>(eventType))\n        {\n            await handler.Handle(integrationEvent);\n        }\n    }\n\n    [UnconditionalSuppressMessage(\"Trimming\", \"IL2026:RequiresUnreferencedCode\",\n        Justification = \"The 'JsonSerializer.IsReflectionEnabledByDefault' feature switch, which is set to false by default for trimmed .NET apps, ensures the JsonSerializer doesn't use Reflection.\")]\n    [UnconditionalSuppressMessage(\"AOT\", \"IL3050:RequiresDynamicCode\", Justification = \"See above.\")]\n    private IntegrationEvent DeserializeMessage(string message, Type eventType)\n    {\n        return JsonSerializer.Deserialize(message, eventType, _subscriptionInfo.JsonSerializerOptions) as IntegrationEvent;\n    }\n\n    [UnconditionalSuppressMessage(\"Trimming\", \"IL2026:RequiresUnreferencedCode\",\n        Justification = \"The 'JsonSerializer.IsReflectionEnabledByDefault' feature switch, which is set to false by default for trimmed .NET apps, ensures the JsonSerializer doesn't use Reflection.\")]\n    [UnconditionalSuppressMessage(\"AOT\", \"IL3050:RequiresDynamicCode\", Justification = \"See above.\")]\n    private byte[] SerializeMessage(IntegrationEvent @event)\n    {\n        return JsonSerializer.SerializeToUtf8Bytes(@event, @event.GetType(), _subscriptionInfo.JsonSerializerOptions);\n    }\n\n    public Task StartAsync(CancellationToken cancellationToken)\n    {\n        // Messaging is async so we don't need to wait for it to complete.\n        _ = Task.Factory.StartNew(async () =>\n        {\n            try\n            {\n                logger.LogInformation(\"Starting RabbitMQ connection on a background thread\");\n\n                _rabbitMQConnection = serviceProvider.GetRequiredService<IConnection>();\n                if (!_rabbitMQConnection.IsOpen)\n                {\n                    return;\n                }\n\n                if (logger.IsEnabled(LogLevel.Trace))\n                {\n                    logger.LogTrace(\"Creating RabbitMQ consumer channel\");\n                }\n\n                _consumerChannel = await _rabbitMQConnection.CreateChannelAsync();\n\n                _consumerChannel.CallbackExceptionAsync += (sender, ea) =>\n                {\n                    logger.LogWarning(ea.Exception, \"Error with RabbitMQ consumer channel\");\n                    return Task.CompletedTask;\n                };\n\n                await _consumerChannel.ExchangeDeclareAsync(\n                    exchange: ExchangeName,\n                    type: \"direct\");\n\n                await _consumerChannel.QueueDeclareAsync(\n                    queue: _queueName,\n                    durable: true,\n                    exclusive: false,\n                    autoDelete: false,\n                    arguments: null);\n\n                if (logger.IsEnabled(LogLevel.Trace))\n                {\n                    logger.LogTrace(\"Starting RabbitMQ basic consume\");\n                }\n\n                var consumer = new AsyncEventingBasicConsumer(_consumerChannel);\n\n                consumer.ReceivedAsync += OnMessageReceived;\n\n                await _consumerChannel.BasicConsumeAsync(\n                    queue: _queueName,\n                    autoAck: false,\n                    consumer: consumer);\n\n                foreach (var (eventName, _) in _subscriptionInfo.EventTypes)\n                {\n                    await _consumerChannel.QueueBindAsync(\n                        queue: _queueName,\n                        exchange: ExchangeName,\n                        routingKey: eventName);\n                }\n            }\n            catch (Exception ex)\n            {\n                logger.LogError(ex, \"Error starting RabbitMQ connection\");\n            }\n        },\n        TaskCreationOptions.LongRunning);\n\n        return Task.CompletedTask;\n    }\n\n    public Task StopAsync(CancellationToken cancellationToken)\n    {\n        return Task.CompletedTask;\n    }\n\n    private static ResiliencePipeline CreateResiliencePipeline(int retryCount)\n    {\n        // See https://www.pollydocs.org/strategies/retry.html\n        var retryOptions = new RetryStrategyOptions\n        {\n            ShouldHandle = new PredicateBuilder().Handle<BrokerUnreachableException>().Handle<SocketException>(),\n            MaxRetryAttempts = retryCount,\n            DelayGenerator = (context) => ValueTask.FromResult(GenerateDelay(context.AttemptNumber))\n        };\n\n        return new ResiliencePipelineBuilder()\n            .AddRetry(retryOptions)\n            .Build();\n\n        static TimeSpan? GenerateDelay(int attempt)\n        {\n            return TimeSpan.FromSeconds(Math.Pow(2, attempt));\n        }\n    }\n}\n"
  },
  {
    "path": "src/EventBusRabbitMQ/RabbitMQTelemetry.cs",
    "content": "﻿using System.Diagnostics;\nusing OpenTelemetry.Context.Propagation;\n\nnamespace eShop.EventBusRabbitMQ;\n\npublic class RabbitMQTelemetry\n{\n    public static string ActivitySourceName = \"EventBusRabbitMQ\";\n\n    public ActivitySource ActivitySource { get; } = new(ActivitySourceName);\n    public TextMapPropagator Propagator { get; } = Propagators.DefaultTextMapPropagator;\n}\n"
  },
  {
    "path": "src/EventBusRabbitMQ/RabbitMqDependencyInjectionExtensions.cs",
    "content": "﻿using eShop.EventBusRabbitMQ;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Microsoft.Extensions.Hosting;\n\npublic static class RabbitMqDependencyInjectionExtensions\n{\n    // {\n    //   \"EventBus\": {\n    //     \"SubscriptionClientName\": \"...\",\n    //     \"RetryCount\": 10\n    //   }\n    // }\n\n    private const string SectionName = \"EventBus\";\n\n    public static IEventBusBuilder AddRabbitMqEventBus(this IHostApplicationBuilder builder, string connectionName)\n    {\n        ArgumentNullException.ThrowIfNull(builder);\n\n        builder.AddRabbitMQClient(connectionName);\n\n        // RabbitMQ.Client doesn't have built-in support for OpenTelemetry, so we need to add it ourselves\n        builder.Services.AddOpenTelemetry()\n           .WithTracing(tracing =>\n           {\n               tracing.AddSource(RabbitMQTelemetry.ActivitySourceName);\n           });\n\n        // Options support\n        builder.Services.Configure<EventBusOptions>(builder.Configuration.GetSection(SectionName));\n\n        // Abstractions on top of the core client API\n        builder.Services.AddSingleton<RabbitMQTelemetry>();\n        builder.Services.AddSingleton<IEventBus, RabbitMQEventBus>();\n        // Start consuming messages as soon as the application starts\n        builder.Services.AddSingleton<IHostedService>(sp => (RabbitMQEventBus)sp.GetRequiredService<IEventBus>());\n\n        return new EventBusBuilder(builder.Services);\n    }\n\n    private class EventBusBuilder(IServiceCollection services) : IEventBusBuilder\n    {\n        public IServiceCollection Services => services;\n    }\n}\n"
  },
  {
    "path": "src/HybridApp/App.xaml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<Application xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n             xmlns:local=\"clr-namespace:eShop.HybridApp\"\n             x:Class=\"eShop.HybridApp.App\">\n    <Application.Resources>\n        <ResourceDictionary>\n\n            <Color x:Key=\"PageBackgroundColor\">#512bdf</Color>\n            <Color x:Key=\"PrimaryTextColor\">White</Color>\n\n            <Style TargetType=\"Label\">\n                <Setter Property=\"TextColor\" Value=\"{DynamicResource PrimaryTextColor}\" />\n                <Setter Property=\"FontFamily\" Value=\"OpenSansRegular\" />\n            </Style>\n\n            <Style TargetType=\"Button\">\n                <Setter Property=\"TextColor\" Value=\"{DynamicResource PrimaryTextColor}\" />\n                <Setter Property=\"FontFamily\" Value=\"OpenSansRegular\" />\n                <Setter Property=\"BackgroundColor\" Value=\"#2b0b98\" />\n                <Setter Property=\"Padding\" Value=\"14,10\" />\n            </Style>\n\n        </ResourceDictionary>\n    </Application.Resources>\n</Application>"
  },
  {
    "path": "src/HybridApp/App.xaml.cs",
    "content": "﻿namespace eShop.HybridApp;\n\npublic partial class App : Application\n{\n\tpublic App()\n\t{\n\t\tInitializeComponent();\n\t}\n\n    protected override Window CreateWindow(IActivationState? activationState)\n    {\n        return new Window(new MainPage()) { Title = \"eShop\" };\n    }\n}\n"
  },
  {
    "path": "src/HybridApp/Components/Layout/FooterBar.razor",
    "content": "﻿<footer class='eshop-footer'>\n  <div class='eshop-footer-content'>\n    <div class='eshop-footer-row'>\n      <img src='images/logo-footer.svg' class='logo logo-footer' />\n      <p>© AdventureWorks</p>\n    </div>\n  </div>\n</footer>"
  },
  {
    "path": "src/HybridApp/Components/Layout/FooterBar.razor.css",
    "content": "﻿.eshop-footer {\n  margin-top: 3.5rem;\n  background-color: #000;\n  width: 100%;\n}\n\n.eshop-footer-content {\n  max-width: 120rem;\n  margin: auto;\n}\n\n.eshop-footer-row {\n  padding: 3.5rem 10rem;\n  color: white;\n  display: flex;\n  justify-content: flex-end;\n  align-items: center;\n}\n\n.eshop-footer .logo-footer {\n  color: white;\n  margin-right: auto;\n}"
  },
  {
    "path": "src/HybridApp/Components/Layout/HeaderBar.razor",
    "content": "﻿@* @using Microsoft.AspNetCore.Components.Endpoints *@\n\n<div class=\"eshop-header @(IsCatalog? \"home\" : \"\")\">\n    <div class=\"eshop-header-hero\">\n        @{\n            var headerImage = IsCatalog ? \"images/header-home.webp\" : \"images/header.webp\";\n        }\n        <img src=\"@headerImage\" />\n    </div>\n    <div class=\"eshop-header-container\">\n        <nav class=\"eshop-header-navbar\">\n            <a class=\"logo logo-header\" href=\"\">\n                <img src=\"images/logo-header.svg\" class=\"logo logo-header\" />\n            </a>\n            \n@*             <UserMenu />\n            <CartMenu />\n *@        </nav>\n        <div class=\"eshop-header-intro\">\n            <h1><SectionOutlet SectionName=\"page-header-title\" /></h1>\n            <p><SectionOutlet SectionName=\"page-header-subtitle\" /></p>\n        </div>\n    </div>\n</div>\n\n@code {\n    // [CascadingParameter]\n    // public HttpContext? HttpContext { get; set; }\n\n    // // We can use Endpoint Metadata to determine the page currently being visited\n    // private Type? PageComponentType => HttpContext?.GetEndpoint()?.Metadata.OfType<ComponentTypeMetadata>().FirstOrDefault()?.Type;\n    // private bool IsCatalog => PageComponentType == typeof(Pages.Catalog.Catalog);\n    private bool IsCatalog { get; } = true;\n}\n"
  },
  {
    "path": "src/HybridApp/Components/Layout/HeaderBar.razor.css",
    "content": "﻿.eshop-header {\n    position: relative;\n    max-width: 120rem;\n    margin: auto;\n}\n\n.eshop-header.home .eshop-header-container {\n    height: 38rem;\n    margin-bottom: 0;\n}\n\n.eshop-header .eshop-header-container {\n    height: 15rem;\n    margin-bottom: 4rem;\n}\n\n.eshop-header-hero {\n    overflow: hidden;\n    position: absolute;\n    max-width: 100%;\n    left: 0;\n    top: 0;\n}\n\n.eshop-header-container {\n    position: relative;\n    margin: auto;\n    margin: 0 10rem;\n}\n\n.eshop-header-intro {\n    position: absolute;\n    max-width: 48rem;\n    bottom: 3rem;\n    white-space: nowrap;\n}\n\n.eshop-header-intro h1 {\n    color: #000;\n    font-size: 3.5rem;\n    font-style: normal;\n    font-weight: 700;\n    line-height: 100%;\n    margin: 0;\n}\n\n.eshop-header-intro p {\n    color: #000;\n    font-size: 2rem;\n    font-style: normal;\n    font-weight: 700;\n    line-height: 125%;\n    margin: 0;\n}\n\n.eshop-header .logo-header {\n    color: black;\n    margin-right: auto;\n}\n\n.eshop-header-navbar {\n    display: flex;\n    flex-direction: row;\n    justify-content: flex-end;\n    align-items: center;\n    margin-top: 1.25rem;\n    gap: 1.5rem;\n}\n"
  },
  {
    "path": "src/HybridApp/Components/Layout/MainLayout.razor",
    "content": "﻿@* @using Microsoft.eShop.WebApp.Components.Chatbot *@\n@inherits LayoutComponentBase\n\n<HeaderBar />\n@Body\n@* <ShowChatbotButton /> *@\n<FooterBar />\n\n<div id=\"blazor-error-ui\">\n    An unhandled error has occurred.\n    <a href=\"\" class=\"reload\">Reload</a>\n    <a class=\"dismiss\">🗙</a>\n</div>\n"
  },
  {
    "path": "src/HybridApp/Components/Layout/MainLayout.razor.css",
    "content": "#blazor-error-ui {\n    background: lightyellow;\n    bottom: 0;\n    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);\n    display: none;\n    left: 0;\n    padding: 0.6rem 1.25rem 0.7rem 1.25rem;\n    position: fixed;\n    width: 100%;\n    z-index: 1000;\n}\n\n    #blazor-error-ui .dismiss {\n        cursor: pointer;\n        position: absolute;\n        right: 0.75rem;\n        top: 0.5rem;\n    }\n"
  },
  {
    "path": "src/HybridApp/Components/Pages/Catalog/Catalog.razor",
    "content": "﻿@page \"/\"\n@inject NavigationManager Nav\n@inject eShop.HybridApp.Services.CatalogService CatalogService\n\n<PageTitle>AdventureWorks</PageTitle>\n<SectionContent SectionName=\"page-header-title\">Ready for a new adventure?</SectionContent>\n<SectionContent SectionName=\"page-header-subtitle\">Start the season with the latest in clothing and equipment.</SectionContent>\n\n<div class=\"catalog\">\n    <eShop.HybridApp.Components.Pages.Catalog.CatalogSearch BrandId=\"@BrandId\" ItemTypeId=\"@ItemTypeId\" />\n\n    @if (catalogResult is null)\n    {\n        <p>Loading...</p>\n    }\n    else\n    {\n        <div>\n            <div class=\"catalog-items\">\n                @foreach (var item in catalogResult.Data)\n                {\n                    <CatalogListItem Item=\"@item\" />\n                }\n            </div>\n\n            <div class=\"page-links\">\n                @foreach (var pageIndex in GetVisiblePageIndexes(catalogResult))\n                {\n                    <NavLink ActiveClass=\"active-page\" Match=\"@NavLinkMatch.All\" href=\"@Nav.GetUriWithQueryParameter(\"page\", pageIndex == 1 ? null : pageIndex)\">@pageIndex</NavLink>\n                }\n            </div>\n        </div>\n    }\n</div>\n\n@code {\n    const int PageSize = 9;\n\n    [SupplyParameterFromQuery]\n    public int? Page { get; set; }\n\n    [SupplyParameterFromQuery(Name = \"brand\")]\n    public int? BrandId { get; set; }\n\n    [SupplyParameterFromQuery(Name = \"type\")]\n    public int? ItemTypeId { get; set; }\n\n    CatalogResult? catalogResult;\n\n    static IEnumerable<int> GetVisiblePageIndexes(CatalogResult result)\n        => Enumerable.Range(1, (int)Math.Ceiling(1.0 * result.Count / PageSize));\n\n    protected override async Task OnParametersSetAsync()\n    {\n        catalogResult = await CatalogService.GetCatalogItems(\n            Page.GetValueOrDefault(1) - 1,\n            PageSize,\n            BrandId,\n            ItemTypeId);\n    }\n}\n"
  },
  {
    "path": "src/HybridApp/Components/Pages/Catalog/Catalog.razor.css",
    "content": "﻿.catalog {\n    padding: 0 10rem;\n    display: flex;\n    gap: 6rem;\n}\n\n.catalog .catalog-filter {\n    flex-grow: 1;\n    min-width: 14rem;\n}\n\n.catalog .catalog-filter .catalog-filter-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    align-self: stretch;\n    gap: 0.7rem;\n}\n\n.catalog .catalog-filter .filter-reset {\n    margin-left: auto;\n}\n\n.catalog .catalog-filter .filter-reset:hover {\n    cursor: pointer;\n}\n\n.catalog .catalog-filter .filter-badge {\n    background: #000;\n    color: #FFF;\n    font-size: 1rem;\n    font-weight: 600;\n    border-radius: 0.75rem;\n    width: 1.5rem;\n    height: 1.5rem;\n    line-height: 100%;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.catalog .catalog-filter-group h3 {\n    color: #000;\n    font-size: 1rem;\n    font-weight: 600;\n    line-height: 150%;\n}\n\n.catalog .catalog-filter-group .catalog-filter-group-tags {\n    border-top: 1px solid #404040;\n    display: flex;\n    padding: 0.75rem 0;\n    align-items: center;\n    align-content: center;\n    gap: 0.25rem;\n    align-self: stretch;\n    flex-wrap: wrap;\n}\n\n.catalog-filter-group-tags .catalog-filter-tag {\n    display: flex;\n    padding: 0.5rem 0.75rem;\n    justify-content: center;\n    align-items: center;\n    gap: 0.25rem;\n    border-radius: 1.25rem;\n    color: #404040;\n    font-family: 'Open Sans';\n    font-size: 1rem;\n    font-style: normal;\n    font-weight: 400;\n    line-height: 150%;\n}\n\n.catalog-filter-group-tags .catalog-filter-tag:hover {\n    cursor: pointer;\n}\n\n.catalog-filter-group-tags .catalog-filter-tag.active {\n    background: #000;\n    color: #FFF;\n}\n\n.catalog .catalog-items {\n    display: flex;\n    align-items: flex-start;\n    align-content: flex-start;\n    gap: 2.5rem;\n    flex-wrap: wrap;\n    flex-grow: 1;\n}\n\n.page-links {\n    display: flex;\n    align-items: center;\n    gap: 0.5rem;\n    justify-content: center;\n    margin-top: 1.5rem;\n}\n\n::deep a {\n    display: flex;\n    padding: 12px 20px;\n    flex-direction: column;\n    justify-content: center;\n    align-items: center;\n    gap: 4px;\n    background: #F7F7F7;\n    color: #000;\n    text-decoration: none;\n}\n\n.page-links ::deep a.active-page {\n    color: #F7F7F7;\n    background-color: #000;\n}\n"
  },
  {
    "path": "src/HybridApp/Components/Pages/Catalog/CatalogSearch.razor",
    "content": "﻿@inject eShop.HybridApp.Services.CatalogService CatalogService\n@inject NavigationManager Nav\n\n@if (catalogBrands is not null && catalogItemTypes is not null)\n{\n    <div class=\"catalog-search\">\n        <div class=\"catalog-search-header\">\n            <img role=\"presentation\" src=\"icons/filters.svg\" />\n            Filters\n        </div>\n        <div class=\"catalog-search-types\">\n            <div class=\"catalog-search-group\">\n                <h3>Brand</h3>\n                <div class=\"catalog-search-group-tags\">\n                    <a href=\"@BrandUri(null)\"\n                    class=\"catalog-search-tag @(BrandId == null ? \"active \" : \"\")\">\n                        All\n                    </a>\n                    @foreach (var brand in catalogBrands)\n                    {\n                        <a href=\"@BrandUri(brand.Id)\"\n                        class=\"catalog-search-tag @(BrandId == brand.Id ? \"active \" : \"\")\">\n                            @brand.Brand\n                        </a>\n                    }\n                </div>\n            </div>\n            <div class=\"catalog-search-group\">\n                <h3>Type</h3>\n\n                <div class=\"catalog-search-group-tags\">\n                    <a href=\"@TypeUri(null)\"\n                    class=\"catalog-search-tag @(ItemTypeId == null ? \"active \" : \"\")\">\n                    All\n                    </a>\n                    @foreach (var itemType in catalogItemTypes)\n                    {\n                        <a href=\"@TypeUri(itemType.Id)\"\n                        class=\"catalog-search-tag @(ItemTypeId == itemType.Id ? \"active \" : \"\")\">\n                            @itemType.Type\n                        </a>\n                    }\n                </div>\n            </div>\n        </div>\n    </div>\n}\n\n@code {\n    IEnumerable<CatalogBrand>? catalogBrands;\n    IEnumerable<CatalogItemType>? catalogItemTypes;\n    [Parameter] public int? BrandId { get; set; }\n    [Parameter] public int? ItemTypeId { get; set; }\n\n    protected override async Task OnInitializedAsync()\n    {\n        var brandsTask = CatalogService.GetBrands();\n        var itemTypesTask = CatalogService.GetTypes();\n        await Task.WhenAll(brandsTask, itemTypesTask);\n        catalogBrands = brandsTask.Result;\n        catalogItemTypes = itemTypesTask.Result;\n    }\n\n    private string BrandUri(int? brandId) => Nav.GetUriWithQueryParameters(new Dictionary<string, object?>()\n    {\n        { \"page\", null },\n        { \"brand\", brandId },\n    });\n\n    private string TypeUri(int? typeId) => Nav.GetUriWithQueryParameters(new Dictionary<string, object?>()\n    {\n        { \"page\", null },\n        { \"type\", typeId },\n    });\n}\n"
  },
  {
    "path": "src/HybridApp/Components/Pages/Catalog/CatalogSearch.razor.css",
    "content": "﻿.catalog-search {\n  flex-shrink: 0;\n  width: 14rem;\n}\n\n.catalog-search .catalog-search-header {\n  display: flex;\n  align-items: center;\n  align-self: stretch;\n  gap: 0.7rem;\n}\n\n.catalog-search .search-badge {\n  background: #000;\n  color: #FFF;\n  font-size: 1rem;\n  font-weight: 600;\n  border-radius: 0.75rem;\n  width: 1.5rem;\n  height: 1.5rem;\n  line-height: 100%;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.catalog-search-group h3 {\n  color: #000;\n  font-size: 1rem;\n  font-weight: 600;\n  line-height: 150%;\n}\n\n.catalog-search-group .catalog-search-group-tags {\n  border-top: 1px solid #404040;\n  display: flex;\n  padding: 0.75rem 0;\n  align-items: center;\n  align-content: center;\n  gap: 0.25rem;\n  align-self: stretch;\n  flex-wrap: wrap;\n  min-width: 12rem;\n}\n\n.catalog-search-tag {\n  display: flex;\n  padding: 0.5rem 0.75rem;\n  justify-content: center;\n  align-items: center;\n  gap: 0.25rem;\n  border-radius: 1.25rem;\n  color: #404040;\n  font-family: 'Open Sans';\n  font-size: 1rem;\n  font-style: normal;\n  font-weight: 400;\n  line-height: 150%;\n  text-decoration: none;\n}\n\n.catalog-search-tag:hover {\n    cursor: pointer;\n    background: #ddd;\n}\n\n.catalog-search-tag.active {\n  background: #000;\n  color: #FFF;\n}\n\n.catalog-search.button {\n  width: 100%;\n  margin-top: 1rem;\n}\n\n@media only screen and (max-width: 480px) { \n  .catalog-search {\n      width: 100%;\n  }\n\n  .catalog-search .catalog-search-header {\n    display: none;\n  }\n\n  .catalog-search-group .catalog-search-group-tags {\n    justify-content: space-between;\n  }\n}\n\n@media only screen and (min-width: 481px) and (max-width: 1024px) { \n  .catalog-search {\n    width: 100%;\n  }\n\n  .catalog-search-types {\n    display: flex;\n    gap: 3rem;\n  }\n\n  .catalog-search-group {\n    flex-basis: calc(50% - 3rem);\n  }\n\n  .catalog-search-group .catalog-search-group-tags {\n    justify-content: space-between;\n  }\n}"
  },
  {
    "path": "src/HybridApp/Components/Pages/Item/ItemPage.razor",
    "content": "﻿@page \"/item/{itemId:int}\"\n@using System.Net\n@inject eShop.HybridApp.Services.CatalogService CatalogService\n@* @inject BasketState BasketState *@\n@inject NavigationManager Nav\n@inject IProductImageUrlProvider ProductImages\n\n@if (item is not null)\n{\n    <PageTitle>@item.Name | AdventureWorks</PageTitle>\n    <SectionContent SectionName=\"page-header-title\">@item.Name</SectionContent>\n    <SectionContent SectionName=\"page-header-subtitle\">@item.CatalogBrand?.Brand</SectionContent>\n\n    <div class=\"item-details\">\n        <img src=\"@ProductImages.GetProductImageUrl(item.Id)\" />\n        <div class=\"description\">\n            <p>@item.Description</p>\n            <p>\n                Brand: <strong>@item.CatalogBrand?.Brand</strong>\n            </p>\n            <form class=\"add-to-cart\" >\n                <span class=\"price\">$@item.Price.ToString(\"0.00\")</span>\n            </form>\n\n        @*     @if (numInCart > 0)\n            {\n                <p><strong>@numInCart</strong> in <a href=\"cart\">shopping bag</a></p>\n            } *@\n        </div>\n    </div>\n}\nelse if (notFound)\n{\n    <SectionContent SectionName=\"page-header-title\">Not found</SectionContent>\n    <div class=\"item-details\">\n        <p>Sorry, we couldn't find any such product.</p>\n    </div>\n}\n\n@code {\n    private CatalogItem? item;\n    // private int numInCart;\n    // private bool isLoggedIn;\n    private bool notFound;\n\n    [Parameter]\n    public int ItemId { get; set; }\n\n    // [CascadingParameter]\n    // public HttpContext? HttpContext { get; set; }\n\n    protected override async Task OnInitializedAsync()\n    {\n        try\n        {\n            // isLoggedIn = HttpContext?.User.Identity?.IsAuthenticated == true;\n            item = await CatalogService.GetCatalogItem(ItemId);\n            // await UpdateNumInCartAsync();\n        }\n        catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)\n        {\n            // HttpContext!.Response.StatusCode = 404;\n            notFound = true;\n        }\n    }\n\n    // private async Task AddToCartAsync()\n    // {\n    //     if (!isLoggedIn)\n    //     {\n    //         Nav.NavigateTo(Pages.User.LogIn.Url(Nav));\n    //         return;\n    //     }\n\n    //     if (item is not null)\n    //     {\n    //         await BasketState.AddAsync(item);\n    //         await UpdateNumInCartAsync();\n    //     }\n    // }\n\n    // private async Task UpdateNumInCartAsync()\n    // {\n    //     var items = await BasketState.GetBasketItemsAsync();\n    //     numInCart = items.FirstOrDefault(row => row.ProductId == ItemId)?.Quantity ?? 0;\n    // }\n}\n"
  },
  {
    "path": "src/HybridApp/Components/Pages/Item/ItemPage.razor.css",
    "content": "﻿.item-details {\n    padding: 0 5rem 0 10rem;\n    display: flex;\n    align-items: flex-start;\n    gap: 4rem;\n    line-height: 1.7rem;\n}\n\np:first-of-type {\n    margin-top: 0;\n}\n\nimg {\n    width: 25rem;\n}\n\n.description {\n    max-width: 30rem;\n}\n\n.add-to-cart {\n    display: flex;\n    align-items: center;\n    gap: 1.2rem;\n}\n\n.price {\n    font-size: 1.6rem;\n    font-weight: 600;\n}\n\n.add-to-cart button {\n    background-color: black;\n    color: white;\n    border-radius: .25rem;\n    border: 0;\n    padding: 0.5rem 1.25rem;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    gap: 0.5rem;\n}\n\n.add-to-cart button:hover {\n    background-color: #666;\n}\n\n    .add-to-cart button:active {\n        background-color: #333;\n    }\n\na {\n    color: black;\n}"
  },
  {
    "path": "src/HybridApp/Components/Routes.razor",
    "content": "﻿<Router AppAssembly=\"@typeof(MauiProgram).Assembly\">\n    <Found Context=\"routeData\">\n        <RouteView RouteData=\"@routeData\" DefaultLayout=\"@typeof(Components.Layout.MainLayout)\" />\n        <FocusOnNavigate RouteData=\"@routeData\" Selector=\"h1\" />\n    </Found>\n    <NotFound>\n        <PageTitle>Not found</PageTitle>\n        <LayoutView Layout=\"@typeof(Components.Layout.MainLayout)\">\n            <p role=\"alert\">Sorry, there's nothing at this address.</p>\n        </LayoutView>\n    </NotFound>\n</Router>\n"
  },
  {
    "path": "src/HybridApp/Components/_Imports.razor",
    "content": "﻿@using System.Net.Http\n@using System.Net.Http.Json\n@using Microsoft.AspNetCore.Components.Forms\n@using Microsoft.AspNetCore.Components.Routing\n@using Microsoft.AspNetCore.Components.Sections\n@using Microsoft.AspNetCore.Components.Web\n@using Microsoft.AspNetCore.Components.Web.Virtualization\n@using Microsoft.JSInterop\n@using eShop.HybridApp\n@using eShop.HybridApp.Components\n@using eShop.WebAppComponents.Services\n@using eShop.WebAppComponents.Item\n@using eShop.WebAppComponents.Catalog\n"
  },
  {
    "path": "src/HybridApp/GlobalSuppressions.cs",
    "content": "// Suppressions for Razor-generated routing code in MAUI Blazor apps\n// \n// CONTEXT: After upgrading to .NET 10, the enhanced trimming analysis now flags Razor-generated \n// code for Router and LayoutView components as potentially unsafe. However, these warnings are \n// false positives in the context of MAUI Blazor Hybrid apps.\n//\n// TECHNICAL DETAILS:\n// - IL2111: Router.NotFoundPage.set and LayoutView.Layout.set use reflection for component discovery\n// - IL2110: LayoutView internal fields are accessed via reflection during layout resolution\n// \n// SAFETY: These suppressions are safe because:\n// 1. Layout components are explicitly referenced in Routes.razor and preserved by Razor compilation\n// 2. MAUI Blazor hybrid apps don't use aggressive trimming that would remove referenced components\n// 3. The Router and LayoutView are core Blazor components designed to work with reflection\n//\n// ALTERNATIVES ATTEMPTED:\n// - Adding DynamicallyAccessedMembers attributes to layout components (failed)\n// - Restructuring Router configuration to avoid LayoutView (failed) \n// - Using direct component references instead of typeof() (failed)\n//\n// This is a known limitation documented in the official MAUI repository.\n// Tracking issue: https://github.com/dotnet/maui/issues/22368\n\n[assembly: System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage(\n    \"Trimming\", \n    \"IL2111\", \n    Justification = \"Blazor Router and LayoutView components use safe reflection patterns that are preserved by the MAUI build process.\")]\n\n[assembly: System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage(\n    \"Trimming\", \n    \"IL2110\", \n    Justification = \"Blazor LayoutView internal fields are accessed via reflection in a controlled manner that's safe for MAUI hybrid apps.\")]"
  },
  {
    "path": "src/HybridApp/HybridApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Razor\">\n\n    <PropertyGroup>\n        <TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>\n        <TargetFrameworks Condition=\"$([MSBuild]::IsOSPlatform('windows'))\">$(TargetFrameworks);net10.0-windows10.0.19041.0</TargetFrameworks>\n        <!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->\n        <!-- <TargetFrameworks>$(TargetFrameworks);net10.0-tizen</TargetFrameworks> -->\n\n         <!-- Note for MacCatalyst:\n            The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.\n            When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifer>.\n            The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated;\n            either BOTH runtimes must be indicated or ONLY macatalyst-x64. -->\n         <!-- ex. <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->\n\n        <OutputType>Exe</OutputType>\n        <RootNamespace>eShop.HybridApp</RootNamespace>\n        <UseMaui>true</UseMaui>\n        <SingleProject>true</SingleProject>\n        <ImplicitUsings>enable</ImplicitUsings>\n        <Nullable>enable</Nullable>\n        <EnableDefaultCssItems>false</EnableDefaultCssItems>\n        <ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>\n        \n        <!-- Suppress trimming warnings for Razor-generated code -->\n        <SuppressedTrimAnalysisWarnings>$(SuppressedTrimAnalysisWarnings);IL2111</SuppressedTrimAnalysisWarnings>\n\n        <!-- Display name -->\n        <ApplicationTitle>eShop App</ApplicationTitle>\n\n        <!-- App Identifier -->\n        <ApplicationId>com.companyname.eshop.hybridapp</ApplicationId>\n\n        <!-- Versions -->\n        <ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>\n        <ApplicationVersion>1</ApplicationVersion>\n\n\n      <SupportedOSPlatformVersion Condition=\"$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'\">15.0</SupportedOSPlatformVersion>\n      <SupportedOSPlatformVersion Condition=\"$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'\">15.0</SupportedOSPlatformVersion>\n      <SupportedOSPlatformVersion Condition=\"$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'\">24.0</SupportedOSPlatformVersion>\n      <SupportedOSPlatformVersion Condition=\"$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'\">10.0.17763.0</SupportedOSPlatformVersion>\n      <TargetPlatformMinVersion Condition=\"$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'\">10.0.17763.0</TargetPlatformMinVersion>\n      <SupportedOSPlatformVersion Condition=\"$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'\">6.5</SupportedOSPlatformVersion>\n    </PropertyGroup>\n\n    <ItemGroup>\n        <!-- App Icon -->\n        <MauiIcon Include=\"Resources\\AppIcon\\appicon.svg\" ForegroundFile=\"Resources\\AppIcon\\appiconfg.svg\" Color=\"#512BD4\" />\n\n        <!-- Splash Screen -->\n        <MauiSplashScreen Include=\"Resources\\Splash\\splash.svg\" Color=\"#512BD4\" BaseSize=\"128,128\" />\n\n        <!-- Images -->\n        <MauiImage Include=\"Resources\\Images\\*\" />\n        <MauiImage Update=\"Resources\\Images\\dotnet_bot.svg\" BaseSize=\"168,208\" />\n\n        <!-- Custom Fonts -->\n        <MauiFont Include=\"Resources\\Fonts\\*\" />\n\n        <!-- Raw Assets (also remove the \"Resources\\Raw\" prefix) -->\n        <MauiAsset Include=\"Resources\\Raw\\**\" LogicalName=\"%(RecursiveDir)%(Filename)%(Extension)\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <PackageReference Include=\"Microsoft.AspNetCore.Components.WebView.Maui\" Version=\"9.0.30\" />\n        <PackageReference Include=\"Microsoft.Maui.Controls\" Version=\"9.0.30\" />\n        <PackageReference Include=\"Microsoft.Maui.Controls.Compatibility\" Version=\"9.0.30\" />\n        <PackageReference Include=\"Microsoft.Extensions.Http\" Version=\"9.0.0\" />\n        <PackageReference Include=\"Microsoft.Extensions.Logging.Debug\" Version=\"9.0.0\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <ProjectReference Include=\"..\\..\\src\\WebAppComponents\\WebAppComponents.csproj\" />\n    </ItemGroup>\n\n    <!-- Build Properties must be defined within these property groups to ensure successful publishing\n         to the Mac App Store. See: https://aka.ms/maui-publish-app-store#define-build-properties-in-your-project-file -->\n    <PropertyGroup Condition=\"$(TargetFramework.Contains('-maccatalyst')) and '$(Configuration)' == 'Debug'\">\n        <CodesignEntitlements>Platforms/MacCatalyst/Entitlements.Debug.plist</CodesignEntitlements>\n    </PropertyGroup>\n\n    <PropertyGroup Condition=\"$(TargetFramework.Contains('-maccatalyst')) and '$(Configuration)' == 'Release'\">\n        <CodesignEntitlements>Platforms/MacCatalyst/Entitlements.Release.plist</CodesignEntitlements>\n        <UseHardenedRuntime>true</UseHardenedRuntime>\n    </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "src/HybridApp/MainPage.xaml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<ContentPage xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n             xmlns:local=\"clr-namespace:eShop.HybridApp.Components\"\n             x:Class=\"eShop.HybridApp.MainPage\"\n             BackgroundColor=\"{DynamicResource PageBackgroundColor}\">\n\n    <BlazorWebView x:Name=\"blazorWebView\" HostPage=\"wwwroot/index.html\">\n        <BlazorWebView.RootComponents>\n            <RootComponent Selector=\"#app\" ComponentType=\"{x:Type local:Routes}\" />\n        </BlazorWebView.RootComponents>\n    </BlazorWebView>\n\n</ContentPage>\n"
  },
  {
    "path": "src/HybridApp/MainPage.xaml.cs",
    "content": "﻿namespace eShop.HybridApp;\n\npublic partial class MainPage : ContentPage\n{\n\tpublic MainPage()\n\t{\n\t\tInitializeComponent();\n\t}\n}\n"
  },
  {
    "path": "src/HybridApp/MauiProgram.cs",
    "content": "﻿using eShop.HybridApp.Services;\nusing Microsoft.Extensions.Logging;\n\nnamespace eShop.HybridApp;\n\npublic static class MauiProgram\n{\n    // NOTE: Must have a trailing slash on base URLs to ensure the full BaseAddress URL is used to resolve relative URLs\n    internal static string MobileBffHost = DeviceInfo.Platform == DevicePlatform.Android ? \"http://10.0.2.2:11632/\" : \"http://localhost:11632/\";\n\n\n    public static MauiApp CreateMauiApp()\n    {\n        var builder = MauiApp.CreateBuilder();\n        builder\n            .UseMauiApp<App>()\n            .ConfigureFonts(fonts =>\n            {\n                fonts.AddFont(\"OpenSans-Regular.ttf\", \"OpenSansRegular\");\n            });\n\n        builder.Services.AddMauiBlazorWebView();\n\n#if DEBUG\n\t\tbuilder.Services.AddBlazorWebViewDeveloperTools();\n\t\tbuilder.Logging.AddDebug();\n#endif\n\n\n        builder.Services.AddHttpClient<CatalogService>(o => o.BaseAddress = new(MobileBffHost));\n        builder.Services.AddSingleton<WebAppComponents.Services.IProductImageUrlProvider, ProductImageUrlProvider>();\n\n        return builder.Build();\n    }\n}\n"
  },
  {
    "path": "src/HybridApp/Platforms/Android/AndroidManifest.xml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <application android:allowBackup=\"true\" android:icon=\"@mipmap/appicon\" android:roundIcon=\"@mipmap/appicon_round\" android:supportsRtl=\"true\" android:usesCleartextTraffic=\"true\" android:networkSecurityConfig=\"@xml/network_security_config\">\n        \n    </application>\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n</manifest>"
  },
  {
    "path": "src/HybridApp/Platforms/Android/MainActivity.cs",
    "content": "﻿using Android.App;\nusing Android.Content.PM;\nusing Android.OS;\n\nnamespace eShop.HybridApp;\n\n[Activity(Theme = \"@style/Maui.SplashTheme\", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]\npublic class MainActivity : MauiAppCompatActivity\n{\n}\n"
  },
  {
    "path": "src/HybridApp/Platforms/Android/MainApplication.cs",
    "content": "﻿using Android.App;\nusing Android.Runtime;\n\nnamespace eShop.HybridApp;\n\n[Application]\npublic class MainApplication : MauiApplication\n{\n    public MainApplication(IntPtr handle, JniHandleOwnership ownership)\n        : base(handle, ownership)\n    {\n    }\n\n    protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();\n}\n"
  },
  {
    "path": "src/HybridApp/Platforms/Android/Resources/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#512BD4</color>\n    <color name=\"colorPrimaryDark\">#2B0B98</color>\n    <color name=\"colorAccent\">#2B0B98</color>\n</resources>"
  },
  {
    "path": "src/HybridApp/Platforms/Android/Resources/xml/network_security_config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<network-security-config>\n    <domain-config cleartextTrafficPermitted=\"true\">\n        <domain includeSubdomains=\"true\">10.0.2.2</domain>\n    </domain-config>\n</network-security-config>"
  },
  {
    "path": "src/HybridApp/Platforms/MacCatalyst/AppDelegate.cs",
    "content": "﻿using Foundation;\n\nnamespace eShop.HybridApp;\n\n[Register(\"AppDelegate\")]\npublic class AppDelegate : MauiUIApplicationDelegate\n{\n    protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();\n}\n"
  },
  {
    "path": "src/HybridApp/Platforms/MacCatalyst/Entitlements.Debug.plist",
    "content": "﻿<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <!-- Enable this value to use browser developer tools while debugging.-->\n        <!-- See https://aka.ms/blazor-hybrid-developer-tools -->\n        <key>com.apple.security.get-task-allow</key>\n        <true/>\n    </dict>\n</plist>\n"
  },
  {
    "path": "src/HybridApp/Platforms/MacCatalyst/Entitlements.Release.plist",
    "content": "﻿<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <!-- See https://aka.ms/maui-publish-app-store#add-entitlements for more information about adding entitlements.-->\n    <dict>\n        <key>com.apple.security.app-sandbox</key>\n        <true/>\n        <key>com.apple.security.network.client</key>\n        <true/>\n    </dict>\n</plist>\n\n"
  },
  {
    "path": "src/HybridApp/Platforms/MacCatalyst/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <!-- The Mac App Store requires you specify if the app uses encryption. -->\n    <!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption -->\n    <!-- <key>ITSAppUsesNonExemptEncryption</key> -->\n    <!-- Please indicate <true/> or <false/> here. -->\n\n    <!-- Specify the category for your app here. -->\n    <!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/lsapplicationcategorytype -->\n    <!-- <key>LSApplicationCategoryType</key> -->\n    <!-- <string>public.app-category.YOUR-CATEGORY-HERE</string> -->\n    <key>UIDeviceFamily</key>\n    <array>\n        <integer>2</integer>\n    </array>\n    <key>UIRequiredDeviceCapabilities</key>\n    <array>\n        <string>arm64</string>\n    </array>\n    <key>UISupportedInterfaceOrientations</key>\n    <array>\n        <string>UIInterfaceOrientationPortrait</string>\n        <string>UIInterfaceOrientationLandscapeLeft</string>\n        <string>UIInterfaceOrientationLandscapeRight</string>\n    </array>\n    <key>UISupportedInterfaceOrientations~ipad</key>\n    <array>\n        <string>UIInterfaceOrientationPortrait</string>\n        <string>UIInterfaceOrientationPortraitUpsideDown</string>\n        <string>UIInterfaceOrientationLandscapeLeft</string>\n        <string>UIInterfaceOrientationLandscapeRight</string>\n    </array>\n    <key>NSAppTransportSecurity</key>\n    <dict>\n        <key>NSAllowsArbitraryLoads</key>\n        <true/>\n    </dict>\n    <key>XSAppIconAssets</key>\n    <string>Assets.xcassets/appicon.appiconset</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "src/HybridApp/Platforms/MacCatalyst/Program.cs",
    "content": "﻿using ObjCRuntime;\nusing UIKit;\n\nnamespace eShop.HybridApp;\n\npublic class Program\n{\n    // This is the main entry point of the application.\n    static void Main(string[] args)\n    {\n        // if you want to use a different Application Delegate class from \"AppDelegate\"\n        // you can specify it here.\n        UIApplication.Main(args, null, typeof(AppDelegate));\n    }\n}\n"
  },
  {
    "path": "src/HybridApp/Platforms/Tizen/Main.cs",
    "content": "﻿using System;\nusing Microsoft.Maui;\nusing Microsoft.Maui.Hosting;\n\nnamespace eShop.HybridApp;\n\ninternal class Program : MauiApplication\n{\n    protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();\n\n    static void Main(string[] args)\n    {\n        var app = new Program();\n        app.Run(args);\n    }\n}\n"
  },
  {
    "path": "src/HybridApp/Platforms/Tizen/tizen-manifest.xml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest package=\"maui-application-id-placeholder\" version=\"0.0.0\" api-version=\"7\" xmlns=\"http://tizen.org/ns/packages\">\n  <profile name=\"common\" />\n  <ui-application appid=\"maui-application-id-placeholder\" exec=\"HybridApp.dll\" multiple=\"false\" nodisplay=\"false\" taskmanage=\"true\" type=\"dotnet\" launch_mode=\"single\">\n    <label>maui-application-title-placeholder</label>\n    <icon>maui-appicon-placeholder</icon>\n    <metadata key=\"http://tizen.org/metadata/prefer_dotnet_aot\" value=\"true\" />\n  </ui-application>\n  <shortcut-list />\n  <privileges>\n    <privilege>http://tizen.org/privilege/internet</privilege>\n  </privileges> \n  <dependencies />\n  <provides-appdefined-privileges />\n</manifest>"
  },
  {
    "path": "src/HybridApp/Platforms/Windows/App.xaml",
    "content": "﻿<maui:MauiWinUIApplication\n    x:Class=\"eShop.HybridApp.WinUI.App\"\n    xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n    xmlns:maui=\"using:Microsoft.Maui\"\n    xmlns:local=\"using:eShop.HybridApp.WinUI\">\n\n</maui:MauiWinUIApplication>\n"
  },
  {
    "path": "src/HybridApp/Platforms/Windows/App.xaml.cs",
    "content": "﻿using Microsoft.UI.Xaml;\n\n// To learn more about WinUI, the WinUI project structure,\n// and more about our project templates, see: http://aka.ms/winui-project-info.\n\nnamespace eShop.HybridApp.WinUI;\n\n/// <summary>\n/// Provides application-specific behavior to supplement the default Application class.\n/// </summary>\npublic partial class App : MauiWinUIApplication\n{\n    /// <summary>\n    /// Initializes the singleton application object.  This is the first line of authored code\n    /// executed, and as such is the logical equivalent of main() or WinMain().\n    /// </summary>\n    public App()\n    {\n        this.InitializeComponent();\n    }\n\n    protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();\n}\n"
  },
  {
    "path": "src/HybridApp/Platforms/Windows/Package.appxmanifest",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Package\n  xmlns=\"http://schemas.microsoft.com/appx/manifest/foundation/windows10\"\n  xmlns:uap=\"http://schemas.microsoft.com/appx/manifest/uap/windows10\"\n  xmlns:mp=\"http://schemas.microsoft.com/appx/2014/phone/manifest\"\n  xmlns:rescap=\"http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities\"\n  IgnorableNamespaces=\"uap rescap\">\n\n  <Identity Name=\"maui-package-name-placeholder\" Publisher=\"CN=User Name\" Version=\"0.0.0.0\" />\n\n  <mp:PhoneIdentity PhoneProductId=\"302B88B5-6577-44E2-9434-0398A25FCA65\" PhonePublisherId=\"00000000-0000-0000-0000-000000000000\"/>\n\n  <Properties>\n    <DisplayName>$placeholder$</DisplayName>\n    <PublisherDisplayName>User Name</PublisherDisplayName>\n    <Logo>$placeholder$.png</Logo>\n  </Properties>\n\n  <Dependencies>\n    <TargetDeviceFamily Name=\"Windows.Universal\" MinVersion=\"10.0.17763.0\" MaxVersionTested=\"10.0.19041.0\" />\n    <TargetDeviceFamily Name=\"Windows.Desktop\" MinVersion=\"10.0.17763.0\" MaxVersionTested=\"10.0.19041.0\" />\n  </Dependencies>\n\n  <Resources>\n    <Resource Language=\"x-generate\" />\n  </Resources>\n\n  <Applications>\n    <Application Id=\"App\" Executable=\"$targetnametoken$.exe\" EntryPoint=\"$targetentrypoint$\">\n      <uap:VisualElements\n        DisplayName=\"$placeholder$\"\n        Description=\"$placeholder$\"\n        Square150x150Logo=\"$placeholder$.png\"\n        Square44x44Logo=\"$placeholder$.png\"\n        BackgroundColor=\"transparent\">\n        <uap:DefaultTile Square71x71Logo=\"$placeholder$.png\" Wide310x150Logo=\"$placeholder$.png\" Square310x310Logo=\"$placeholder$.png\" />\n        <uap:SplashScreen Image=\"$placeholder$.png\" />\n      </uap:VisualElements>\n    </Application>\n  </Applications>\n\n  <Capabilities>\n    <rescap:Capability Name=\"runFullTrust\" />\n  </Capabilities>\n\n</Package>\n"
  },
  {
    "path": "src/HybridApp/Platforms/Windows/app.manifest",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<assembly manifestVersion=\"1.0\" xmlns=\"urn:schemas-microsoft-com:asm.v1\">\n  <assemblyIdentity version=\"1.0.0.0\" name=\"HybridApp.WinUI.app\"/>\n\n  <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <windowsSettings>\n      <!-- The combination of below two tags have the following effect:\n           1) Per-Monitor for >= Windows 10 Anniversary Update\n           2) System < Windows 10 Anniversary Update\n      -->\n      <dpiAware xmlns=\"http://schemas.microsoft.com/SMI/2005/WindowsSettings\">true/PM</dpiAware>\n      <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2, PerMonitor</dpiAwareness>\n    </windowsSettings>\n  </application>\n</assembly>\n"
  },
  {
    "path": "src/HybridApp/Platforms/iOS/AppDelegate.cs",
    "content": "﻿using Foundation;\n\nnamespace eShop.HybridApp;\n\n[Register(\"AppDelegate\")]\npublic class AppDelegate : MauiUIApplicationDelegate\n{\n    protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();\n}\n"
  },
  {
    "path": "src/HybridApp/Platforms/iOS/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>LSRequiresIPhoneOS</key>\n    <true/>\n    <key>UIDeviceFamily</key>\n    <array>\n        <integer>1</integer>\n        <integer>2</integer>\n    </array>\n    <key>UIRequiredDeviceCapabilities</key>\n    <array>\n        <string>arm64</string>\n    </array>\n    <key>UISupportedInterfaceOrientations</key>\n    <array>\n        <string>UIInterfaceOrientationPortrait</string>\n        <string>UIInterfaceOrientationLandscapeLeft</string>\n        <string>UIInterfaceOrientationLandscapeRight</string>\n    </array>\n    <key>UISupportedInterfaceOrientations~ipad</key>\n    <array>\n        <string>UIInterfaceOrientationPortrait</string>\n        <string>UIInterfaceOrientationPortraitUpsideDown</string>\n        <string>UIInterfaceOrientationLandscapeLeft</string>\n        <string>UIInterfaceOrientationLandscapeRight</string>\n    </array>\n    <key>XSAppIconAssets</key>\n    <string>Assets.xcassets/appicon.appiconset</string>\n    <key>NSAppTransportSecurity</key>\n    <dict>\n        <key>NSAllowsLocalNetworking</key>\n        <true/>\n    </dict>\n</dict>\n</plist>\n"
  },
  {
    "path": "src/HybridApp/Platforms/iOS/Program.cs",
    "content": "﻿using ObjCRuntime;\nusing UIKit;\n\nnamespace eShop.HybridApp;\n\npublic class Program\n{\n    // This is the main entry point of the application.\n    static void Main(string[] args)\n    {\n        // if you want to use a different Application Delegate class from \"AppDelegate\"\n        // you can specify it here.\n        UIApplication.Main(args, null, typeof(AppDelegate));\n    }\n}\n"
  },
  {
    "path": "src/HybridApp/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"Windows Machine\": {\n      \"commandName\": \"MsixPackage\",\n      \"nativeDebugging\": false\n    }\n  }\n}"
  },
  {
    "path": "src/HybridApp/Resources/Raw/AboutAssets.txt",
    "content": "﻿Any raw assets you want to be deployed with your application can be placed in\nthis directory (and child directories). Deployment of the asset to your application\nis automatically handled by the following `MauiAsset` Build Action within your `.csproj`.\n\n    <MauiAsset Include=\"Resources\\Raw\\**\" LogicalName=\"%(RecursiveDir)%(Filename)%(Extension)\" />\n\nThese files will be deployed with you package and will be accessible using Essentials:\n\n    async Task LoadMauiAsset()\n    {\n        using var stream = await FileSystem.OpenAppPackageFileAsync(\"AboutAssets.txt\");\n        using var reader = new StreamReader(stream);\n\n        var contents = reader.ReadToEnd();\n    }\n"
  },
  {
    "path": "src/HybridApp/Services/CatalogJsonContext.cs",
    "content": "using System.Text.Json.Serialization;\nusing eShop.WebAppComponents.Catalog;\n\nnamespace eShop.HybridApp.Services;\n\n[JsonSerializable(typeof(CatalogItem))]\n[JsonSerializable(typeof(CatalogResult))]\n[JsonSerializable(typeof(List<CatalogItem>))]\n[JsonSerializable(typeof(CatalogBrand[]))]\n[JsonSerializable(typeof(CatalogItemType[]))]\n[JsonSerializable(typeof(CatalogBrand))]\n[JsonSerializable(typeof(CatalogItemType))]\n[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]\npublic partial class CatalogJsonContext : JsonSerializerContext\n{\n}"
  },
  {
    "path": "src/HybridApp/Services/CatalogService.cs",
    "content": "﻿using System.Net.Http.Json;\nusing System.Web;\nusing eShop.WebAppComponents.Catalog;\nusing eShop.WebAppComponents.Services;\n\nnamespace eShop.HybridApp.Services;\n\npublic class CatalogService(HttpClient httpClient) : ICatalogService\n{\n    private readonly string remoteServiceBaseUrl = \"api/catalog/\";\n\n\n    public Task<CatalogItem?> GetCatalogItem(int id)\n    {\n        var uri = $\"{remoteServiceBaseUrl}items/{id}?api-version=2.0\";\n        return httpClient.GetFromJsonAsync(uri, CatalogJsonContext.Default.CatalogItem);\n    }\n\n    public async Task<CatalogResult> GetCatalogItems(int pageIndex, int pageSize, int? brand, int? type)\n    {\n        var uri = GetAllCatalogItemsUri(remoteServiceBaseUrl, pageIndex, pageSize, brand, type);\n        var result = await httpClient.GetFromJsonAsync($\"{uri}&api-version=2.0\", CatalogJsonContext.Default.CatalogResult);\n        return result!;\n    }\n\n    public async Task<List<CatalogItem>> GetCatalogItems(IEnumerable<int> ids)\n    {\n        var uri = $\"{remoteServiceBaseUrl}items/by?ids={string.Join(\"&ids=\", ids)}&api-version=2.0\";\n        var result = await httpClient.GetFromJsonAsync(uri, CatalogJsonContext.Default.ListCatalogItem);\n        return result!;\n    }\n\n    public Task<CatalogResult> GetCatalogItemsWithSemanticRelevance(int page, int take, string text)\n    {\n        var url = $\"{remoteServiceBaseUrl}items/withsemanticrelevance?text={HttpUtility.UrlEncode(text)}&pageIndex={page}&pageSize={take}&api-version=2.0\";\n        var result = httpClient.GetFromJsonAsync(url, CatalogJsonContext.Default.CatalogResult);\n        return result!;\n    }\n\n    public async Task<IEnumerable<CatalogBrand>> GetBrands()\n    {\n        var uri = $\"{remoteServiceBaseUrl}catalogBrands?api-version=2.0\";\n        var result = await httpClient.GetFromJsonAsync(uri, CatalogJsonContext.Default.CatalogBrandArray);\n        return result!;\n    }\n\n    public async Task<IEnumerable<CatalogItemType>> GetTypes()\n    {\n        var uri = $\"{remoteServiceBaseUrl}catalogTypes?api-version=2.0\";\n        var result = await httpClient.GetFromJsonAsync(uri, CatalogJsonContext.Default.CatalogItemTypeArray);\n        return result!;\n    }\n\n    private static string GetAllCatalogItemsUri(string baseUri, int pageIndex, int pageSize, int? brand, int? type)\n    {\n        string filterQs = string.Empty;\n\n        if (type.HasValue)\n        {\n            filterQs += $\"type={type.Value}&\";\n        }\n        if (brand.HasValue)\n        {\n            filterQs += $\"brand={brand.Value}&\";\n        }\n\n        return $\"{baseUri}items?{filterQs}pageIndex={pageIndex}&pageSize={pageSize}&api-version=2.0\";\n    }\n}\n"
  },
  {
    "path": "src/HybridApp/Services/ProductImageUrlProvider.cs",
    "content": "﻿using eShop.WebAppComponents.Services;\n\nnamespace eShop.HybridApp.Services;\n\npublic class ProductImageUrlProvider : IProductImageUrlProvider\n{\n    public string GetProductImageUrl(int productId)\n        => $\"{MauiProgram.MobileBffHost}api/catalog/items/{productId}/pic?api-version=2.0\";\n}\n"
  },
  {
    "path": "src/HybridApp/wwwroot/css/app.css",
    "content": "/* plus-jakarta-sans-200 - latin */\n@font-face {\n    font-display: swap;\n    font-family: 'Plus Jakarta Sans';\n    font-style: normal;\n    font-weight: 200;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-200.woff2') format('woff2');\n}\n\n/* plus-jakarta-sans-200italic - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: italic;\n    font-weight: 200;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-200italic.woff2') format('woff2');\n}\n\n/* plus-jakarta-sans-300 - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: normal;\n    font-weight: 300;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-300.woff2') format('woff2');\n}\n\n/* plus-jakarta-sans-300italic - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: italic;\n    font-weight: 300;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-300italic.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-regular - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: normal;\n    font-weight: 400;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-regular.woff2') format('woff2'); \n}\n  \n/* plus-jakarta-sans-italic - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: italic;\n    font-weight: 400;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-italic.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-500 - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: normal;\n    font-weight: 500;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-500.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-500italic - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: italic;\n    font-weight: 500;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-500italic.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-600 - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: normal;\n    font-weight: 600;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-600.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-600italic - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: italic;\n    font-weight: 600;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-600italic.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-700 - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: normal;\n    font-weight: 700;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-700.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-700italic - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: italic;\n    font-weight: 700;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-700italic.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-800 - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: normal;\n    font-weight: 800;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-800.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-800italic - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: italic;\n    font-weight: 800;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-800italic.woff2') format('woff2'); \n}\n\n\nbody {\n    font-family: 'Plus Jakarta Sans';\n}\n\n.container {\n    position: relative;\n    max-width: 120rem;\n    margin: auto;\n}\n\n.button {\n    display: flex;\n    padding: 1rem 0.75rem;\n    justify-content: center;\n    align-items: center;\n    gap: 0.25rem;\n    align-self: stretch;\n    border: none;\n    text-decoration: none;\n}\n\n.button.button-primary {\n    background: #000;\n    color: #FFF;\n}\n\n.button.button.button-secondary {\n    border: 1px solid #444;\n    background: #FFF;\n    color: #000;\n}\n\nh1:focus {\n    outline: none;\n}\n\n.valid.modified:not([type=checkbox]) {\n    outline: 1px solid #26b050;\n}\n\n.invalid {\n    outline: 1px solid red;\n}\n\n.validation-message {\n    color: red;\n}\n\n.blazor-error-boundary {\n    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;\n    padding: 1rem 1rem 1rem 3.7rem;\n    color: white;\n}\n\n    .blazor-error-boundary::after {\n        content: \"An error has occurred.\"\n    }"
  },
  {
    "path": "src/HybridApp/wwwroot/css/normalize.css",
    "content": "/**\n * 1. Correct the line height in all browsers.\n * 2. Prevent adjustments of font size after orientation changes in iOS.\n */\n\n html {\n    line-height: 1.15; /* 1 */\n    -webkit-text-size-adjust: 100%; /* 2 */\n  }\n  \n  /* Sections\n     ========================================================================== */\n  \n  /**\n   * Remove the margin in all browsers.\n   */\n  \n  body {\n    margin: 0;\n  }\n  \n  /**\n   * Render the `main` element consistently in IE.\n   */\n  \n  main {\n    display: block;\n  }\n  \n  /**\n   * Correct the font size and margin on `h1` elements within `section` and\n   * `article` contexts in Chrome, Firefox, and Safari.\n   */\n  \n  h1 {\n    font-size: 2em;\n    margin: 0.67em 0;\n  }\n  \n  /* Grouping content\n     ========================================================================== */\n  \n  /**\n   * 1. Add the correct box sizing in Firefox.\n   * 2. Show the overflow in Edge and IE.\n   */\n  \n  hr {\n    box-sizing: content-box; /* 1 */\n    height: 0; /* 1 */\n    overflow: visible; /* 2 */\n  }\n  \n  /**\n   * 1. Correct the inheritance and scaling of font size in all browsers.\n   * 2. Correct the odd `em` font sizing in all browsers.\n   */\n  \n  pre {\n    font-family: monospace, monospace; /* 1 */\n    font-size: 1em; /* 2 */\n  }\n  \n  /* Text-level semantics\n     ========================================================================== */\n  \n  /**\n   * Remove the gray background on active links in IE 10.\n   */\n  \n  a {\n    background-color: transparent;\n  }\n  \n  /**\n   * 1. Remove the bottom border in Chrome 57-\n   * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n   */\n  \n  abbr[title] {\n    border-bottom: none; /* 1 */\n    text-decoration: underline; /* 2 */\n    text-decoration: underline dotted; /* 2 */\n  }\n  \n  /**\n   * Add the correct font weight in Chrome, Edge, and Safari.\n   */\n  \n  b,\n  strong {\n    font-weight: bolder;\n  }\n  \n  /**\n   * 1. Correct the inheritance and scaling of font size in all browsers.\n   * 2. Correct the odd `em` font sizing in all browsers.\n   */\n  \n  code,\n  kbd,\n  samp {\n    font-family: monospace, monospace; /* 1 */\n    font-size: 1em; /* 2 */\n  }\n  \n  /**\n   * Add the correct font size in all browsers.\n   */\n  \n  small {\n    font-size: 80%;\n  }\n  \n  /**\n   * Prevent `sub` and `sup` elements from affecting the line height in\n   * all browsers.\n   */\n  \n  sub,\n  sup {\n    font-size: 75%;\n    line-height: 0;\n    position: relative;\n    vertical-align: baseline;\n  }\n  \n  sub {\n    bottom: -0.25em;\n  }\n  \n  sup {\n    top: -0.5em;\n  }\n  \n  /* Embedded content\n     ========================================================================== */\n  \n  /**\n   * Remove the border on images inside links in IE 10.\n   */\n  \n  img {\n    border-style: none;\n  }\n  \n  /* Forms\n     ========================================================================== */\n  \n  /**\n   * 1. Change the font styles in all browsers.\n   * 2. Remove the margin in Firefox and Safari.\n   */\n  \n  button,\n  input,\n  optgroup,\n  select,\n  textarea {\n    font-family: inherit; /* 1 */\n    font-size: 100%; /* 1 */\n    line-height: 1.15; /* 1 */\n    margin: 0; /* 2 */\n  }\n  \n  /**\n   * Show the overflow in IE.\n   * 1. Show the overflow in Edge.\n   */\n  \n  button,\n  input { /* 1 */\n    overflow: visible;\n  }\n  \n  /**\n   * Remove the inheritance of text transform in Edge, Firefox, and IE.\n   * 1. Remove the inheritance of text transform in Firefox.\n   */\n  \n  button,\n  select { /* 1 */\n    text-transform: none;\n  }\n  \n  /**\n   * Correct the inability to style clickable types in iOS and Safari.\n   */\n  \n  button,\n  [type=\"button\"],\n  [type=\"reset\"],\n  [type=\"submit\"] {\n    -webkit-appearance: button;\n  }\n  \n  /**\n   * Remove the inner border and padding in Firefox.\n   */\n  \n  button::-moz-focus-inner,\n  [type=\"button\"]::-moz-focus-inner,\n  [type=\"reset\"]::-moz-focus-inner,\n  [type=\"submit\"]::-moz-focus-inner {\n    border-style: none;\n    padding: 0;\n  }\n  \n  /**\n   * Restore the focus styles unset by the previous rule.\n   */\n  \n  button:-moz-focusring,\n  [type=\"button\"]:-moz-focusring,\n  [type=\"reset\"]:-moz-focusring,\n  [type=\"submit\"]:-moz-focusring {\n    outline: 1px dotted ButtonText;\n  }\n  \n  /**\n   * Correct the padding in Firefox.\n   */\n  \n  fieldset {\n    padding: 0.35em 0.75em 0.625em;\n  }\n  \n  /**\n   * 1. Correct the text wrapping in Edge and IE.\n   * 2. Correct the color inheritance from `fieldset` elements in IE.\n   * 3. Remove the padding so developers are not caught out when they zero out\n   *    `fieldset` elements in all browsers.\n   */\n  \n  legend {\n    box-sizing: border-box; /* 1 */\n    color: inherit; /* 2 */\n    display: table; /* 1 */\n    max-width: 100%; /* 1 */\n    padding: 0; /* 3 */\n    white-space: normal; /* 1 */\n  }\n  \n  /**\n   * Add the correct vertical alignment in Chrome, Firefox, and Opera.\n   */\n  \n  progress {\n    vertical-align: baseline;\n  }\n  \n  /**\n   * Remove the default vertical scrollbar in IE 10+.\n   */\n  \n  textarea {\n    overflow: auto;\n  }\n  \n  /**\n   * 1. Add the correct box sizing in IE 10.\n   * 2. Remove the padding in IE 10.\n   */\n  \n  [type=\"checkbox\"],\n  [type=\"radio\"] {\n    box-sizing: border-box; /* 1 */\n    padding: 0; /* 2 */\n  }\n  \n  /**\n   * Correct the cursor style of increment and decrement buttons in Chrome.\n   */\n  \n  [type=\"number\"]::-webkit-inner-spin-button,\n  [type=\"number\"]::-webkit-outer-spin-button {\n    height: auto;\n  }\n  \n  /**\n   * 1. Correct the odd appearance in Chrome and Safari.\n   * 2. Correct the outline style in Safari.\n   */\n  \n  [type=\"search\"] {\n    -webkit-appearance: textfield; /* 1 */\n    outline-offset: -2px; /* 2 */\n  }\n  \n  /**\n   * Remove the inner padding in Chrome and Safari on macOS.\n   */\n  \n  [type=\"search\"]::-webkit-search-decoration {\n    -webkit-appearance: none;\n  }\n  \n  /**\n   * 1. Correct the inability to style clickable types in iOS and Safari.\n   * 2. Change font properties to `inherit` in Safari.\n   */\n  \n  ::-webkit-file-upload-button {\n    -webkit-appearance: button; /* 1 */\n    font: inherit; /* 2 */\n  }\n  \n  /* Interactive\n     ========================================================================== */\n  \n  /*\n   * Add the correct display in Edge, IE 10+, and Firefox.\n   */\n  \n  details {\n    display: block;\n  }\n  \n  /*\n   * Add the correct display in all browsers.\n   */\n  \n  summary {\n    display: list-item;\n  }\n  \n  /* Misc\n     ========================================================================== */\n  \n  /**\n   * Add the correct display in IE 10+.\n   */\n  \n  template {\n    display: none;\n  }\n  \n  /**\n   * Add the correct display in IE 10.\n   */\n  \n  [hidden] {\n    display: none;\n  }"
  },
  {
    "path": "src/HybridApp/wwwroot/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover\" />\n    <title>eShop eShop.HybridApp</title>\n    <base href=\"/\" />\n    <link rel=\"stylesheet\" href=\"css/normalize.css\" />\n    <link rel=\"stylesheet\" href=\"css/app.css\" />\n    <link rel=\"stylesheet\" href=\"HybridApp.styles.css\" />\n    <link rel=\"icon\" type=\"image/png\" href=\"favicon.png\" />\n</head>\n\n<body>\n\n    <div class=\"status-bar-safe-area\"></div>\n\n    <div id=\"app\">Loading...</div>\n\n    <script src=\"_framework/blazor.webview.js\" autostart=\"false\"></script>\n\n</body>\n\n</html>"
  },
  {
    "path": "src/Identity.API/.gitignore",
    "content": "wwwroot/lib/\nkeys/\n"
  },
  {
    "path": "src/Identity.API/Configuration/Config.cs",
    "content": "﻿namespace eShop.Identity.API.Configuration\n{\n    public class Config\n    {\n        // ApiResources define the apis in your system\n        public static IEnumerable<ApiResource> GetApis()\n        {\n            return new List<ApiResource>\n            {\n                new ApiResource(\"orders\", \"Orders Service\"),\n                new ApiResource(\"basket\", \"Basket Service\"),\n                new ApiResource(\"webhooks\", \"Webhooks registration Service\"),\n            };\n        }\n\n        // ApiScope is used to protect the API \n        //The effect is the same as that of API resources in IdentityServer 3.x\n        public static IEnumerable<ApiScope> GetApiScopes()\n        {\n            return new List<ApiScope>\n            {\n                new ApiScope(\"orders\", \"Orders Service\"),\n                new ApiScope(\"basket\", \"Basket Service\"),\n                new ApiScope(\"webhooks\", \"Webhooks registration Service\"),\n            };\n        }\n\n        // Identity resources are data like user ID, name, or email address of a user\n        // see: http://docs.identityserver.io/en/release/configuration/resources.html\n        public static IEnumerable<IdentityResource> GetResources()\n        {\n            return new List<IdentityResource>\n            {\n                new IdentityResources.OpenId(),\n                new IdentityResources.Profile()\n            };\n        }\n\n        // client want to access resources (aka scopes)\n        public static IEnumerable<Client> GetClients(IConfiguration configuration)\n        {\n            return new List<Client>\n            {\n                new Client\n                {\n                    ClientId = \"maui\",\n                    ClientName = \"eShop MAUI OpenId Client\",\n                    AllowedGrantTypes = GrantTypes.Code,                    \n                    //Used to retrieve the access token on the back channel.\n                    ClientSecrets =\n                    {\n                        new Secret(\"secret\".Sha256())\n                    },\n                    RedirectUris = { configuration[\"MauiCallback\"] },\n                    RequireConsent = false,\n                    RequirePkce = true,\n                    PostLogoutRedirectUris = { $\"{configuration[\"MauiCallback\"]}/Account/Redirecting\" },\n                    //AllowedCorsOrigins = { \"http://eshopxamarin\" },\n                    AllowedScopes = new List<string>\n                    {\n                        IdentityServerConstants.StandardScopes.OpenId,\n                        IdentityServerConstants.StandardScopes.Profile,\n                        IdentityServerConstants.StandardScopes.OfflineAccess,\n                        \"orders\",\n                        \"basket\",\n                        \"mobileshoppingagg\",\n                        \"webhooks\"\n                    },\n                    //Allow requesting refresh tokens for long lived API access\n                    AllowOfflineAccess = true,\n                    AllowAccessTokensViaBrowser = true,\n                    AlwaysIncludeUserClaimsInIdToken = true,\n                    AccessTokenLifetime = 60*60*2, // 2 hours\n                    IdentityTokenLifetime= 60*60*2 // 2 hours\n                },\n                new Client\n                {\n                    ClientId = \"webapp\",\n                    ClientName = \"WebApp Client\",\n                    ClientSecrets = new List<Secret>\n                    {\n                        new Secret(\"secret\".Sha256())\n                    },\n                    ClientUri = $\"{configuration[\"WebAppClient\"]}\",                             // public uri of the client\n                    AllowedGrantTypes = GrantTypes.Code,\n                    AllowAccessTokensViaBrowser = false,\n                    RequireConsent = false,\n                    AllowOfflineAccess = true,\n                    AlwaysIncludeUserClaimsInIdToken = true,\n                    RequirePkce = false,\n                    RedirectUris = new List<string>\n                    {\n                        $\"{configuration[\"WebAppClient\"]}/signin-oidc\"\n                    },\n                    PostLogoutRedirectUris = new List<string>\n                    {\n                        $\"{configuration[\"WebAppClient\"]}/signout-callback-oidc\"\n                    },\n                    AllowedScopes = new List<string>\n                    {\n                        IdentityServerConstants.StandardScopes.OpenId,\n                        IdentityServerConstants.StandardScopes.Profile,\n                        IdentityServerConstants.StandardScopes.OfflineAccess,\n                        \"orders\",\n                        \"basket\",\n                        \"webshoppingagg\",\n                        \"webhooks\"\n                    },\n                    AccessTokenLifetime = 60*60*2, // 2 hours\n                    IdentityTokenLifetime= 60*60*2 // 2 hours\n                },\n                new Client\n                {\n                    ClientId = \"webhooksclient\",\n                    ClientName = \"Webhooks Client\",\n                    ClientSecrets = new List<Secret>\n                    {\n                        new Secret(\"secret\".Sha256())\n                    },\n                    ClientUri = $\"{configuration[\"WebhooksWebClient\"]}\",                             // public uri of the client\n                    AllowedGrantTypes = GrantTypes.Code,\n                    AllowAccessTokensViaBrowser = false,\n                    RequireConsent = false,\n                    AllowOfflineAccess = true,\n                    AlwaysIncludeUserClaimsInIdToken = true,\n                    RedirectUris = new List<string>\n                    {\n                        $\"{configuration[\"WebhooksWebClient\"]}/signin-oidc\"\n                    },\n                    PostLogoutRedirectUris = new List<string>\n                    {\n                        $\"{configuration[\"WebhooksWebClient\"]}/signout-callback-oidc\"\n                    },\n                    AllowedScopes = new List<string>\n                    {\n                        IdentityServerConstants.StandardScopes.OpenId,\n                        IdentityServerConstants.StandardScopes.Profile,\n                        IdentityServerConstants.StandardScopes.OfflineAccess,\n                        \"webhooks\"\n                    },\n                    AccessTokenLifetime = 60*60*2, // 2 hours\n                    IdentityTokenLifetime= 60*60*2 // 2 hours\n                },\n                new Client\n                {\n                    ClientId = \"basketswaggerui\",\n                    ClientName = \"Basket Swagger UI\",\n                    AllowedGrantTypes = GrantTypes.Implicit,\n                    AllowAccessTokensViaBrowser = true,\n\n                    RedirectUris = { $\"{configuration[\"BasketApiClient\"]}/swagger/oauth2-redirect.html\" },\n                    PostLogoutRedirectUris = { $\"{configuration[\"BasketApiClient\"]}/swagger/\" },\n\n                    AllowedScopes =\n                    {\n                        \"basket\"\n                    }\n                },\n                new Client\n                {\n                    ClientId = \"orderingswaggerui\",\n                    ClientName = \"Ordering Swagger UI\",\n                    AllowedGrantTypes = GrantTypes.Implicit,\n                    AllowAccessTokensViaBrowser = true,\n\n                    RedirectUris = { $\"{configuration[\"OrderingApiClient\"]}/swagger/oauth2-redirect.html\" },\n                    PostLogoutRedirectUris = { $\"{configuration[\"OrderingApiClient\"]}/swagger/\" },\n\n                    AllowedScopes =\n                    {\n                        \"orders\"\n                    }\n                },\n                new Client\n                {\n                    ClientId = \"webhooksswaggerui\",\n                    ClientName = \"WebHooks Service Swagger UI\",\n                    AllowedGrantTypes = GrantTypes.Implicit,\n                    AllowAccessTokensViaBrowser = true,\n\n                    RedirectUris = { $\"{configuration[\"WebhooksApiClient\"]}/swagger/oauth2-redirect.html\" },\n                    PostLogoutRedirectUris = { $\"{configuration[\"WebhooksApiClient\"]}/swagger/\" },\n\n                    AllowedScopes =\n                    {\n                        \"webhooks\"\n                    }\n                }\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Data/ApplicationDbContext.cs",
    "content": "﻿namespace eShop.Identity.API.Data;\n\n/// <remarks>\n/// Add migrations using the following command inside the 'Identity.API' project directory:\n///\n/// dotnet ef migrations add [migration-name]\n/// </remarks>\npublic class ApplicationDbContext : IdentityDbContext<ApplicationUser>\n{\n    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)\n        : base(options)\n    {\n    }\n\n    protected override void OnModelCreating(ModelBuilder builder)\n    {\n        base.OnModelCreating(builder);\n        // Customize the ASP.NET Identity model and override the defaults if needed.\n        // For example, you can rename the ASP.NET Identity table names and more.\n        // Add your customizations after calling base.OnModelCreating(builder);\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Data/Migrations/20230925223402_InitialMigration.Designer.cs",
    "content": "﻿// <auto-generated />\nusing System;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Infrastructure;\nusing Microsoft.EntityFrameworkCore.Migrations;\nusing Microsoft.EntityFrameworkCore.Storage.ValueConversion;\nusing eShop.Identity.API.Data;\nusing Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;\n\n#nullable disable\n\nnamespace eShop.Identity.API.Data.Migrations\n{\n    [DbContext(typeof(ApplicationDbContext))]\n    [Migration(\"20230925223402_InitialMigration\")]\n    partial class InitialMigration\n    {\n        /// <inheritdoc />\n        protected override void BuildTargetModel(ModelBuilder modelBuilder)\n        {\n#pragma warning disable 612, 618\n            modelBuilder\n                .HasAnnotation(\"ProductVersion\", \"8.0.0-rc.1.23419.6\")\n                .HasAnnotation(\"Relational:MaxIdentifierLength\", 63);\n\n            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityRole\", b =>\n                {\n                    b.Property<string>(\"Id\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"ConcurrencyStamp\")\n                        .IsConcurrencyToken()\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"Name\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"character varying(256)\");\n\n                    b.Property<string>(\"NormalizedName\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"character varying(256)\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"NormalizedName\")\n                        .IsUnique()\n                        .HasDatabaseName(\"RoleNameIndex\");\n\n                    b.ToTable(\"AspNetRoles\", (string)null);\n                });\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>(\"Id\"));\n\n                    b.Property<string>(\"ClaimType\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"ClaimValue\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"RoleId\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"RoleId\");\n\n                    b.ToTable(\"AspNetRoleClaims\", (string)null);\n                });\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityUserClaim<string>\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>(\"Id\"));\n\n                    b.Property<string>(\"ClaimType\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"ClaimValue\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"UserId\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"UserId\");\n\n                    b.ToTable(\"AspNetUserClaims\", (string)null);\n                });\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityUserLogin<string>\", b =>\n                {\n                    b.Property<string>(\"LoginProvider\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"ProviderKey\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"ProviderDisplayName\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"UserId\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.HasKey(\"LoginProvider\", \"ProviderKey\");\n\n                    b.HasIndex(\"UserId\");\n\n                    b.ToTable(\"AspNetUserLogins\", (string)null);\n                });\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityUserRole<string>\", b =>\n                {\n                    b.Property<string>(\"UserId\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"RoleId\")\n                        .HasColumnType(\"text\");\n\n                    b.HasKey(\"UserId\", \"RoleId\");\n\n                    b.HasIndex(\"RoleId\");\n\n                    b.ToTable(\"AspNetUserRoles\", (string)null);\n                });\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityUserToken<string>\", b =>\n                {\n                    b.Property<string>(\"UserId\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"LoginProvider\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"Name\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"Value\")\n                        .HasColumnType(\"text\");\n\n                    b.HasKey(\"UserId\", \"LoginProvider\", \"Name\");\n\n                    b.ToTable(\"AspNetUserTokens\", (string)null);\n                });\n\n            modelBuilder.Entity(\"eShop.Identity.API.Models.ApplicationUser\", b =>\n                {\n                    b.Property<string>(\"Id\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<int>(\"AccessFailedCount\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"CardHolderName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"CardNumber\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<int>(\"CardType\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"City\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"ConcurrencyStamp\")\n                        .IsConcurrencyToken()\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"Country\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"Email\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"character varying(256)\");\n\n                    b.Property<bool>(\"EmailConfirmed\")\n                        .HasColumnType(\"boolean\");\n\n                    b.Property<string>(\"Expiration\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"LastName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<bool>(\"LockoutEnabled\")\n                        .HasColumnType(\"boolean\");\n\n                    b.Property<DateTimeOffset?>(\"LockoutEnd\")\n                        .HasColumnType(\"timestamp with time zone\");\n\n                    b.Property<string>(\"Name\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"NormalizedEmail\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"character varying(256)\");\n\n                    b.Property<string>(\"NormalizedUserName\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"character varying(256)\");\n\n                    b.Property<string>(\"PasswordHash\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"PhoneNumber\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<bool>(\"PhoneNumberConfirmed\")\n                        .HasColumnType(\"boolean\");\n\n                    b.Property<string>(\"SecurityNumber\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"SecurityStamp\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"State\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"Street\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<bool>(\"TwoFactorEnabled\")\n                        .HasColumnType(\"boolean\");\n\n                    b.Property<string>(\"UserName\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"character varying(256)\");\n\n                    b.Property<string>(\"ZipCode\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"NormalizedEmail\")\n                        .HasDatabaseName(\"EmailIndex\");\n\n                    b.HasIndex(\"NormalizedUserName\")\n                        .IsUnique()\n                        .HasDatabaseName(\"UserNameIndex\");\n\n                    b.ToTable(\"AspNetUsers\", (string)null);\n                });\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>\", b =>\n                {\n                    b.HasOne(\"Microsoft.AspNetCore.Identity.IdentityRole\", null)\n                        .WithMany()\n                        .HasForeignKey(\"RoleId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n                });\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityUserClaim<string>\", b =>\n                {\n                    b.HasOne(\"eShop.Identity.API.Models.ApplicationUser\", null)\n                        .WithMany()\n                        .HasForeignKey(\"UserId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n                });\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityUserLogin<string>\", b =>\n                {\n                    b.HasOne(\"eShop.Identity.API.Models.ApplicationUser\", null)\n                        .WithMany()\n                        .HasForeignKey(\"UserId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n                });\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityUserRole<string>\", b =>\n                {\n                    b.HasOne(\"Microsoft.AspNetCore.Identity.IdentityRole\", null)\n                        .WithMany()\n                        .HasForeignKey(\"RoleId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.HasOne(\"eShop.Identity.API.Models.ApplicationUser\", null)\n                        .WithMany()\n                        .HasForeignKey(\"UserId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n                });\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityUserToken<string>\", b =>\n                {\n                    b.HasOne(\"eShop.Identity.API.Models.ApplicationUser\", null)\n                        .WithMany()\n                        .HasForeignKey(\"UserId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n                });\n#pragma warning restore 612, 618\n        }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Data/Migrations/20230925223402_InitialMigration.cs",
    "content": "﻿using System;\nusing Microsoft.EntityFrameworkCore.Migrations;\nusing Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;\n\n#nullable disable\n\nnamespace eShop.Identity.API.Data.Migrations\n{\n    /// <inheritdoc />\n    public partial class InitialMigration : Migration\n    {\n        /// <inheritdoc />\n        protected override void Up(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.CreateTable(\n                name: \"AspNetRoles\",\n                columns: table => new\n                {\n                    Id = table.Column<string>(type: \"text\", nullable: false),\n                    Name = table.Column<string>(type: \"character varying(256)\", maxLength: 256, nullable: true),\n                    NormalizedName = table.Column<string>(type: \"character varying(256)\", maxLength: 256, nullable: true),\n                    ConcurrencyStamp = table.Column<string>(type: \"text\", nullable: true)\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_AspNetRoles\", x => x.Id);\n                });\n\n            migrationBuilder.CreateTable(\n                name: \"AspNetUsers\",\n                columns: table => new\n                {\n                    Id = table.Column<string>(type: \"text\", nullable: false),\n                    CardNumber = table.Column<string>(type: \"text\", nullable: false),\n                    SecurityNumber = table.Column<string>(type: \"text\", nullable: false),\n                    Expiration = table.Column<string>(type: \"text\", nullable: false),\n                    CardHolderName = table.Column<string>(type: \"text\", nullable: false),\n                    CardType = table.Column<int>(type: \"integer\", nullable: false),\n                    Street = table.Column<string>(type: \"text\", nullable: false),\n                    City = table.Column<string>(type: \"text\", nullable: false),\n                    State = table.Column<string>(type: \"text\", nullable: false),\n                    Country = table.Column<string>(type: \"text\", nullable: false),\n                    ZipCode = table.Column<string>(type: \"text\", nullable: false),\n                    Name = table.Column<string>(type: \"text\", nullable: false),\n                    LastName = table.Column<string>(type: \"text\", nullable: false),\n                    UserName = table.Column<string>(type: \"character varying(256)\", maxLength: 256, nullable: true),\n                    NormalizedUserName = table.Column<string>(type: \"character varying(256)\", maxLength: 256, nullable: true),\n                    Email = table.Column<string>(type: \"character varying(256)\", maxLength: 256, nullable: true),\n                    NormalizedEmail = table.Column<string>(type: \"character varying(256)\", maxLength: 256, nullable: true),\n                    EmailConfirmed = table.Column<bool>(type: \"boolean\", nullable: false),\n                    PasswordHash = table.Column<string>(type: \"text\", nullable: true),\n                    SecurityStamp = table.Column<string>(type: \"text\", nullable: true),\n                    ConcurrencyStamp = table.Column<string>(type: \"text\", nullable: true),\n                    PhoneNumber = table.Column<string>(type: \"text\", nullable: true),\n                    PhoneNumberConfirmed = table.Column<bool>(type: \"boolean\", nullable: false),\n                    TwoFactorEnabled = table.Column<bool>(type: \"boolean\", nullable: false),\n                    LockoutEnd = table.Column<DateTimeOffset>(type: \"timestamp with time zone\", nullable: true),\n                    LockoutEnabled = table.Column<bool>(type: \"boolean\", nullable: false),\n                    AccessFailedCount = table.Column<int>(type: \"integer\", nullable: false)\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_AspNetUsers\", x => x.Id);\n                });\n\n            migrationBuilder.CreateTable(\n                name: \"AspNetRoleClaims\",\n                columns: table => new\n                {\n                    Id = table.Column<int>(type: \"integer\", nullable: false)\n                        .Annotation(\"Npgsql:ValueGenerationStrategy\", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),\n                    RoleId = table.Column<string>(type: \"text\", nullable: false),\n                    ClaimType = table.Column<string>(type: \"text\", nullable: true),\n                    ClaimValue = table.Column<string>(type: \"text\", nullable: true)\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_AspNetRoleClaims\", x => x.Id);\n                    table.ForeignKey(\n                        name: \"FK_AspNetRoleClaims_AspNetRoles_RoleId\",\n                        column: x => x.RoleId,\n                        principalTable: \"AspNetRoles\",\n                        principalColumn: \"Id\",\n                        onDelete: ReferentialAction.Cascade);\n                });\n\n            migrationBuilder.CreateTable(\n                name: \"AspNetUserClaims\",\n                columns: table => new\n                {\n                    Id = table.Column<int>(type: \"integer\", nullable: false)\n                        .Annotation(\"Npgsql:ValueGenerationStrategy\", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),\n                    UserId = table.Column<string>(type: \"text\", nullable: false),\n                    ClaimType = table.Column<string>(type: \"text\", nullable: true),\n                    ClaimValue = table.Column<string>(type: \"text\", nullable: true)\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_AspNetUserClaims\", x => x.Id);\n                    table.ForeignKey(\n                        name: \"FK_AspNetUserClaims_AspNetUsers_UserId\",\n                        column: x => x.UserId,\n                        principalTable: \"AspNetUsers\",\n                        principalColumn: \"Id\",\n                        onDelete: ReferentialAction.Cascade);\n                });\n\n            migrationBuilder.CreateTable(\n                name: \"AspNetUserLogins\",\n                columns: table => new\n                {\n                    LoginProvider = table.Column<string>(type: \"text\", nullable: false),\n                    ProviderKey = table.Column<string>(type: \"text\", nullable: false),\n                    ProviderDisplayName = table.Column<string>(type: \"text\", nullable: true),\n                    UserId = table.Column<string>(type: \"text\", nullable: false)\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_AspNetUserLogins\", x => new { x.LoginProvider, x.ProviderKey });\n                    table.ForeignKey(\n                        name: \"FK_AspNetUserLogins_AspNetUsers_UserId\",\n                        column: x => x.UserId,\n                        principalTable: \"AspNetUsers\",\n                        principalColumn: \"Id\",\n                        onDelete: ReferentialAction.Cascade);\n                });\n\n            migrationBuilder.CreateTable(\n                name: \"AspNetUserRoles\",\n                columns: table => new\n                {\n                    UserId = table.Column<string>(type: \"text\", nullable: false),\n                    RoleId = table.Column<string>(type: \"text\", nullable: false)\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_AspNetUserRoles\", x => new { x.UserId, x.RoleId });\n                    table.ForeignKey(\n                        name: \"FK_AspNetUserRoles_AspNetRoles_RoleId\",\n                        column: x => x.RoleId,\n                        principalTable: \"AspNetRoles\",\n                        principalColumn: \"Id\",\n                        onDelete: ReferentialAction.Cascade);\n                    table.ForeignKey(\n                        name: \"FK_AspNetUserRoles_AspNetUsers_UserId\",\n                        column: x => x.UserId,\n                        principalTable: \"AspNetUsers\",\n                        principalColumn: \"Id\",\n                        onDelete: ReferentialAction.Cascade);\n                });\n\n            migrationBuilder.CreateTable(\n                name: \"AspNetUserTokens\",\n                columns: table => new\n                {\n                    UserId = table.Column<string>(type: \"text\", nullable: false),\n                    LoginProvider = table.Column<string>(type: \"text\", nullable: false),\n                    Name = table.Column<string>(type: \"text\", nullable: false),\n                    Value = table.Column<string>(type: \"text\", nullable: true)\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_AspNetUserTokens\", x => new { x.UserId, x.LoginProvider, x.Name });\n                    table.ForeignKey(\n                        name: \"FK_AspNetUserTokens_AspNetUsers_UserId\",\n                        column: x => x.UserId,\n                        principalTable: \"AspNetUsers\",\n                        principalColumn: \"Id\",\n                        onDelete: ReferentialAction.Cascade);\n                });\n\n            migrationBuilder.CreateIndex(\n                name: \"IX_AspNetRoleClaims_RoleId\",\n                table: \"AspNetRoleClaims\",\n                column: \"RoleId\");\n\n            migrationBuilder.CreateIndex(\n                name: \"RoleNameIndex\",\n                table: \"AspNetRoles\",\n                column: \"NormalizedName\",\n                unique: true);\n\n            migrationBuilder.CreateIndex(\n                name: \"IX_AspNetUserClaims_UserId\",\n                table: \"AspNetUserClaims\",\n                column: \"UserId\");\n\n            migrationBuilder.CreateIndex(\n                name: \"IX_AspNetUserLogins_UserId\",\n                table: \"AspNetUserLogins\",\n                column: \"UserId\");\n\n            migrationBuilder.CreateIndex(\n                name: \"IX_AspNetUserRoles_RoleId\",\n                table: \"AspNetUserRoles\",\n                column: \"RoleId\");\n\n            migrationBuilder.CreateIndex(\n                name: \"EmailIndex\",\n                table: \"AspNetUsers\",\n                column: \"NormalizedEmail\");\n\n            migrationBuilder.CreateIndex(\n                name: \"UserNameIndex\",\n                table: \"AspNetUsers\",\n                column: \"NormalizedUserName\",\n                unique: true);\n        }\n\n        /// <inheritdoc />\n        protected override void Down(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.DropTable(\n                name: \"AspNetRoleClaims\");\n\n            migrationBuilder.DropTable(\n                name: \"AspNetUserClaims\");\n\n            migrationBuilder.DropTable(\n                name: \"AspNetUserLogins\");\n\n            migrationBuilder.DropTable(\n                name: \"AspNetUserRoles\");\n\n            migrationBuilder.DropTable(\n                name: \"AspNetUserTokens\");\n\n            migrationBuilder.DropTable(\n                name: \"AspNetRoles\");\n\n            migrationBuilder.DropTable(\n                name: \"AspNetUsers\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Data/Migrations/ApplicationDbContextModelSnapshot.cs",
    "content": "﻿// <auto-generated />\nusing System;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Infrastructure;\nusing Microsoft.EntityFrameworkCore.Storage.ValueConversion;\nusing eShop.Identity.API.Data;\nusing Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;\n\n#nullable disable\n\nnamespace eShop.Identity.API.Data.Migrations\n{\n    [DbContext(typeof(ApplicationDbContext))]\n    partial class ApplicationDbContextModelSnapshot : ModelSnapshot\n    {\n        protected override void BuildModel(ModelBuilder modelBuilder)\n        {\n#pragma warning disable 612, 618\n            modelBuilder\n                .HasAnnotation(\"ProductVersion\", \"8.0.0-rc.1.23419.6\")\n                .HasAnnotation(\"Relational:MaxIdentifierLength\", 63);\n\n            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityRole\", b =>\n                {\n                    b.Property<string>(\"Id\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"ConcurrencyStamp\")\n                        .IsConcurrencyToken()\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"Name\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"character varying(256)\");\n\n                    b.Property<string>(\"NormalizedName\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"character varying(256)\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"NormalizedName\")\n                        .IsUnique()\n                        .HasDatabaseName(\"RoleNameIndex\");\n\n                    b.ToTable(\"AspNetRoles\", (string)null);\n                });\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>(\"Id\"));\n\n                    b.Property<string>(\"ClaimType\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"ClaimValue\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"RoleId\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"RoleId\");\n\n                    b.ToTable(\"AspNetRoleClaims\", (string)null);\n                });\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityUserClaim<string>\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>(\"Id\"));\n\n                    b.Property<string>(\"ClaimType\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"ClaimValue\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"UserId\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"UserId\");\n\n                    b.ToTable(\"AspNetUserClaims\", (string)null);\n                });\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityUserLogin<string>\", b =>\n                {\n                    b.Property<string>(\"LoginProvider\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"ProviderKey\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"ProviderDisplayName\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"UserId\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.HasKey(\"LoginProvider\", \"ProviderKey\");\n\n                    b.HasIndex(\"UserId\");\n\n                    b.ToTable(\"AspNetUserLogins\", (string)null);\n                });\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityUserRole<string>\", b =>\n                {\n                    b.Property<string>(\"UserId\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"RoleId\")\n                        .HasColumnType(\"text\");\n\n                    b.HasKey(\"UserId\", \"RoleId\");\n\n                    b.HasIndex(\"RoleId\");\n\n                    b.ToTable(\"AspNetUserRoles\", (string)null);\n                });\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityUserToken<string>\", b =>\n                {\n                    b.Property<string>(\"UserId\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"LoginProvider\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"Name\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"Value\")\n                        .HasColumnType(\"text\");\n\n                    b.HasKey(\"UserId\", \"LoginProvider\", \"Name\");\n\n                    b.ToTable(\"AspNetUserTokens\", (string)null);\n                });\n\n            modelBuilder.Entity(\"eShop.Identity.API.Models.ApplicationUser\", b =>\n                {\n                    b.Property<string>(\"Id\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<int>(\"AccessFailedCount\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"CardHolderName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"CardNumber\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<int>(\"CardType\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"City\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"ConcurrencyStamp\")\n                        .IsConcurrencyToken()\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"Country\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"Email\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"character varying(256)\");\n\n                    b.Property<bool>(\"EmailConfirmed\")\n                        .HasColumnType(\"boolean\");\n\n                    b.Property<string>(\"Expiration\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"LastName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<bool>(\"LockoutEnabled\")\n                        .HasColumnType(\"boolean\");\n\n                    b.Property<DateTimeOffset?>(\"LockoutEnd\")\n                        .HasColumnType(\"timestamp with time zone\");\n\n                    b.Property<string>(\"Name\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"NormalizedEmail\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"character varying(256)\");\n\n                    b.Property<string>(\"NormalizedUserName\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"character varying(256)\");\n\n                    b.Property<string>(\"PasswordHash\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"PhoneNumber\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<bool>(\"PhoneNumberConfirmed\")\n                        .HasColumnType(\"boolean\");\n\n                    b.Property<string>(\"SecurityNumber\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"SecurityStamp\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"State\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"Street\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<bool>(\"TwoFactorEnabled\")\n                        .HasColumnType(\"boolean\");\n\n                    b.Property<string>(\"UserName\")\n                        .HasMaxLength(256)\n                        .HasColumnType(\"character varying(256)\");\n\n                    b.Property<string>(\"ZipCode\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"NormalizedEmail\")\n                        .HasDatabaseName(\"EmailIndex\");\n\n                    b.HasIndex(\"NormalizedUserName\")\n                        .IsUnique()\n                        .HasDatabaseName(\"UserNameIndex\");\n\n                    b.ToTable(\"AspNetUsers\", (string)null);\n                });\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>\", b =>\n                {\n                    b.HasOne(\"Microsoft.AspNetCore.Identity.IdentityRole\", null)\n                        .WithMany()\n                        .HasForeignKey(\"RoleId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n                });\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityUserClaim<string>\", b =>\n                {\n                    b.HasOne(\"eShop.Identity.API.Models.ApplicationUser\", null)\n                        .WithMany()\n                        .HasForeignKey(\"UserId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n                });\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityUserLogin<string>\", b =>\n                {\n                    b.HasOne(\"eShop.Identity.API.Models.ApplicationUser\", null)\n                        .WithMany()\n                        .HasForeignKey(\"UserId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n                });\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityUserRole<string>\", b =>\n                {\n                    b.HasOne(\"Microsoft.AspNetCore.Identity.IdentityRole\", null)\n                        .WithMany()\n                        .HasForeignKey(\"RoleId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.HasOne(\"eShop.Identity.API.Models.ApplicationUser\", null)\n                        .WithMany()\n                        .HasForeignKey(\"UserId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n                });\n\n            modelBuilder.Entity(\"Microsoft.AspNetCore.Identity.IdentityUserToken<string>\", b =>\n                {\n                    b.HasOne(\"eShop.Identity.API.Models.ApplicationUser\", null)\n                        .WithMany()\n                        .HasForeignKey(\"UserId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n                });\n#pragma warning restore 612, 618\n        }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/GlobalUsings.cs",
    "content": "﻿global using System;\nglobal using System.Collections.Generic;\nglobal using System.ComponentModel.DataAnnotations;\nglobal using System.IdentityModel.Tokens.Jwt;\nglobal using System.Linq;\nglobal using System.Security.Claims;\nglobal using System.Text.RegularExpressions;\nglobal using System.Threading.Tasks;\nglobal using Duende.IdentityModel;\nglobal using Duende.IdentityServer;\nglobal using Duende.IdentityServer.Configuration;\nglobal using Duende.IdentityServer.Events;\nglobal using Duende.IdentityServer.Extensions;\nglobal using Duende.IdentityServer.Models;\nglobal using Duende.IdentityServer.Services;\nglobal using Duende.IdentityServer.Stores;\nglobal using Duende.IdentityServer.Validation;\nglobal using Microsoft.AspNetCore.Authentication;\nglobal using Microsoft.AspNetCore.Authorization;\nglobal using Microsoft.AspNetCore.Builder;\nglobal using Microsoft.AspNetCore.Hosting;\nglobal using Microsoft.AspNetCore.Http;\nglobal using Microsoft.AspNetCore.Identity;\nglobal using Microsoft.AspNetCore.Identity.EntityFrameworkCore;\nglobal using Microsoft.AspNetCore.Mvc;\nglobal using Microsoft.AspNetCore.Mvc.Filters;\nglobal using Microsoft.AspNetCore.Mvc.Rendering;\nglobal using Microsoft.EntityFrameworkCore;\nglobal using Microsoft.EntityFrameworkCore.Infrastructure;\nglobal using Microsoft.EntityFrameworkCore.Metadata;\nglobal using Microsoft.EntityFrameworkCore.Migrations;\nglobal using eShop.Identity.API;\nglobal using eShop.Identity.API.Configuration;\nglobal using eShop.Identity.API.Data;\nglobal using eShop.Identity.API.Models;\nglobal using eShop.Identity.API.Services;\nglobal using Microsoft.Extensions.Configuration;\nglobal using Microsoft.Extensions.DependencyInjection;\nglobal using Microsoft.Extensions.Hosting;\nglobal using Microsoft.Extensions.Logging;\nglobal using Microsoft.Extensions.Options;\nglobal using Polly;\nglobal using eShop.ServiceDefaults;\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "src/Identity.API/Identity.API.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <UserSecretsId>aspnet-eShopOnContainers.Identity-90487118-103c-4ff0-b9da-e5e26f7ab0c5</UserSecretsId>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Duende.IdentityServer.AspNetIdentity\" />\n    <PackageReference Include=\"Duende.IdentityServer.EntityFramework\" />\n    <PackageReference Include=\"Duende.IdentityServer.Storage\" />\n    <PackageReference Include=\"Duende.IdentityServer\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Identity.EntityFrameworkCore\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Identity.UI\" />\n    <PackageReference Include=\"Microsoft.EntityFrameworkCore.Tools\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <PackageReference Include=\"Aspire.Npgsql.EntityFrameworkCore.PostgreSQL\" />\n    <PackageReference Include=\"Microsoft.Web.LibraryManager.Build\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <None Include=\"Views\\Account\\AccessDenied.cshtml\" />\n    <None Include=\"Views\\Account\\LoggedOut.cshtml\" />\n    <None Include=\"Views\\Account\\Login.cshtml\" />\n    <None Include=\"Views\\Account\\Logout.cshtml\" />\n    <None Include=\"Views\\Consent\\Index.cshtml\" />\n    <None Include=\"Views\\Device\\Success.cshtml\" />\n    <None Include=\"Views\\Device\\UserCodeCapture.cshtml\" />\n    <None Include=\"Views\\Device\\UserCodeConfirmation.cshtml\" />\n    <None Include=\"Views\\Diagnostics\\Index.cshtml\" />\n    <None Include=\"Views\\Grants\\Index.cshtml\" />\n    <None Include=\"Views\\Home\\Index.cshtml\" />\n    <None Include=\"Views\\Shared\\Error.cshtml\" />\n    <None Include=\"Views\\Shared\\Redirect.cshtml\" />\n    <None Include=\"Views\\Shared\\_Layout.cshtml\" />\n    <None Include=\"Views\\Shared\\_ScopeListItem.cshtml\" />\n    <None Include=\"Views\\Shared\\_ValidationSummary.cshtml\" />\n    <None Include=\"Views\\_ViewImports.cshtml\" />\n    <None Include=\"Views\\_ViewStart.cshtml\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\eShop.ServiceDefaults\\eShop.ServiceDefaults.csproj\" />\n  </ItemGroup>\n  \n  <ItemGroup>\n    <!-- Don't use this in production -->\n    <Content Include=\"tempkey.jwk\" CopyToOutputDirectory=\"PreserveNewest\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Compile Include=\"..\\Shared\\MigrateDbContextExtensions.cs\" Link=\"Extensions\\MigrateDbContextExtensions.cs\" />\n    <Compile Include=\"..\\Shared\\ActivityExtensions.cs\" Link=\"Extensions\\ActivityExtensions.cs\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Identity.API/Models/AccountViewModels/ForgotPasswordViewModel.cs",
    "content": "﻿namespace eShop.Identity.API.Models.AccountViewModels\n{\n    public record ForgotPasswordViewModel\n    {\n        [Required]\n        [EmailAddress]\n        public string Email { get; init; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/AccountViewModels/LoggedOutViewModel.cs",
    "content": "﻿namespace eShop.Identity.API.Models.AccountViewModels\n{\n    public record LoggedOutViewModel\n    {\n        public string PostLogoutRedirectUri { get; init; }\n        public string ClientName { get; init; }\n        public string SignOutIframeUrl { get; init; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/AccountViewModels/LoginViewModel.cs",
    "content": "﻿namespace eShop.Identity.API.Models.AccountViewModels\n{\n    public record LoginViewModel\n    {\n        [Required]\n        [EmailAddress]\n        public string Email { get; set; }\n\n        [Required]\n        [DataType(DataType.Password)]\n        public string Password { get; set; }\n\n        [Display(Name = \"Remember me?\")]\n        public bool RememberMe { get; set; }\n        public string ReturnUrl { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/AccountViewModels/LogoutViewModel.cs",
    "content": "﻿namespace eShop.Identity.API.Models.AccountViewModels\n{\n    public record LogoutViewModel\n    {\n        public string LogoutId { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/AccountViewModels/RedirectViewModel.cs",
    "content": "﻿namespace eShop.Identity.API.Models.AccountViewModels\n{\n    public class RedirectViewModel\n    {\n        public string RedirectUrl { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/AccountViewModels/RegisterViewModel.cs",
    "content": "﻿namespace eShop.Identity.API.Models.AccountViewModels\n{\n    public record RegisterViewModel\n    {\n        [Required]\n        [EmailAddress]\n        [Display(Name = \"Email\")]\n        public string Email { get; init; }\n\n        [Required]\n        [StringLength(100, ErrorMessage = \"The {0} must be at least {2} and at max {1} characters long.\", MinimumLength = 6)]\n        [DataType(DataType.Password)]\n        [Display(Name = \"Password\")]\n        public string Password { get; init; }\n\n        [DataType(DataType.Password)]\n        [Display(Name = \"Confirm password\")]\n        [Compare(\"Password\", ErrorMessage = \"The password and confirmation password do not match.\")]\n        public string ConfirmPassword { get; init; }\n\n        public ApplicationUser User { get; init; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/AccountViewModels/ResetPasswordViewModel.cs",
    "content": "﻿namespace eShop.Identity.API.Models.AccountViewModels\n{\n    public record ResetPasswordViewModel\n    {\n        [Required]\n        [EmailAddress]\n        public string Email { get; init; }\n\n        [Required]\n        [StringLength(100, ErrorMessage = \"The {0} must be at least {2} and at max {1} characters long.\", MinimumLength = 6)]\n        [DataType(DataType.Password)]\n        public string Password { get; init; }\n\n        [DataType(DataType.Password)]\n        [Display(Name = \"Confirm password\")]\n        [Compare(\"Password\", ErrorMessage = \"The password and confirmation password do not match.\")]\n        public string ConfirmPassword { get; init; }\n\n        public string Code { get; init; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/AccountViewModels/SendCodeViewModel.cs",
    "content": "﻿namespace eShop.Identity.API.Models.AccountViewModels\n{\n    public record SendCodeViewModel\n    {\n        public string SelectedProvider { get; init; }\n\n        public ICollection<SelectListItem> Providers { get; init; }\n\n        public string ReturnUrl { get; init; }\n\n        public bool RememberMe { get; init; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/AccountViewModels/VerifyCodeViewModel.cs",
    "content": "﻿namespace eShop.Identity.API.Models.AccountViewModels\n{\n    public record VerifyCodeViewModel\n    {\n        [Required]\n        public string Provider { get; init; }\n\n        [Required]\n        public string Code { get; init; }\n\n        public string ReturnUrl { get; init; }\n\n        [Display(Name = \"Remember this browser?\")]\n        public bool RememberBrowser { get; init; }\n\n        [Display(Name = \"Remember me?\")]\n        public bool RememberMe { get; init; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/ApplicationUser.cs",
    "content": "﻿namespace eShop.Identity.API.Models\n{\n    // Add profile data for application users by adding properties to the ApplicationUser class\n    public class ApplicationUser : IdentityUser\n    {\n        [Required]\n        public string CardNumber { get; set; }\n        [Required]\n        public string SecurityNumber { get; set; }\n        [Required]\n        [RegularExpression(@\"(0[1-9]|1[0-2])\\/[0-9]{2}\", ErrorMessage = \"Expiration should match a valid MM/YY value\")]\n        public string Expiration { get; set; }\n        [Required]\n        public string CardHolderName { get; set; }\n        public int CardType { get; set; }\n        [Required]\n        public string Street { get; set; }\n        [Required]\n        public string City { get; set; }\n        [Required]\n        public string State { get; set; }\n        [Required]\n        public string Country { get; set; }\n        [Required]\n        public string ZipCode { get; set; }\n        [Required]\n        public string Name { get; set; }\n        [Required]\n        public string LastName { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/ConsentViewModels/ConsentInputModel.cs",
    "content": "﻿namespace eShop.Identity.API.Models.ConsentViewModels\n{\n    public class ConsentInputModel\n    {\n        public string Button { get; set; }\n        public IEnumerable<string> ScopesConsented { get; set; }\n        public bool RememberConsent { get; set; }\n        public string ReturnUrl { get; set; }\n        public string Description { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/ConsentViewModels/ConsentOptions.cs",
    "content": "﻿namespace eShop.Identity.API.Models.ConsentViewModels\n{\n    public class ConsentOptions\n    {\n        public static bool EnableOfflineAccess = true;\n        public static string OfflineAccessDisplayName = \"Offline Access\";\n        public static string OfflineAccessDescription = \"Access to your applications and resources, even when you are offline\";\n\n        public static readonly string MustChooseOneErrorMessage = \"You must pick at least one permission\";\n        public static readonly string InvalidSelectionErrorMessage = \"Invalid selection\";\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/ConsentViewModels/ConsentViewModel.cs",
    "content": "﻿namespace eShop.Identity.API.Models.ConsentViewModels\n{\n    public class ConsentViewModel : ConsentInputModel\n    {\n        public string ClientName { get; set; }\n        public string ClientUrl { get; set; }\n        public string ClientLogoUrl { get; set; }\n        public bool AllowRememberConsent { get; set; }\n\n        public IEnumerable<ScopeViewModel> IdentityScopes { get; set; }\n        public IEnumerable<ScopeViewModel> ApiScopes { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/ConsentViewModels/ProcessConsentResult.cs",
    "content": "﻿namespace eShop.Identity.API.Models.ConsentViewModels\n{\n    public class ProcessConsentResult\n    {\n        public bool IsRedirect => RedirectUri != null;\n        public string RedirectUri { get; set; }\n        public Client Client { get; set; }\n\n        public bool ShowView => ViewModel != null;\n        public ConsentViewModel ViewModel { get; set; }\n\n        public bool HasValidationError => ValidationError != null;\n        public string ValidationError { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/ConsentViewModels/ScopeViewModel.cs",
    "content": "﻿namespace eShop.Identity.API.Models.ConsentViewModels\n{\n    public class ScopeViewModel\n    {\n        public string Value { get; set; }\n        public string DisplayName { get; set; }\n        public string Description { get; set; }\n        public bool Emphasize { get; set; }\n        public bool Required { get; set; }\n        public bool Checked { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/ErrorViewModel.cs",
    "content": "﻿// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\nnamespace eShop.Identity.API.Models\n{\n    public record ErrorViewModel\n    {\n        public ErrorMessage Error { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/ManageViewModels/AddPhoneNumberViewModel.cs",
    "content": "﻿namespace eShop.Identity.API.Models.ManageViewModels\n{\n    public record AddPhoneNumberViewModel\n    {\n        [Required]\n        [Phone]\n        [Display(Name = \"Phone number\")]\n        public string PhoneNumber { get; init; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/ManageViewModels/ChangePasswordViewModel.cs",
    "content": "﻿namespace eShop.Identity.API.Models.ManageViewModels\n{\n    public record ChangePasswordViewModel\n    {\n        [Required]\n        [DataType(DataType.Password)]\n        [Display(Name = \"Current password\")]\n        public string OldPassword { get; init; }\n\n        [Required]\n        [StringLength(100, ErrorMessage = \"The {0} must be at least {2} and at max {1} characters long.\", MinimumLength = 6)]\n        [DataType(DataType.Password)]\n        [Display(Name = \"New password\")]\n        public string NewPassword { get; init; }\n\n        [DataType(DataType.Password)]\n        [Display(Name = \"Confirm new password\")]\n        [Compare(\"NewPassword\", ErrorMessage = \"The new password and confirmation password do not match.\")]\n        public string ConfirmPassword { get; init; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/ManageViewModels/ConfigureTwoFactorViewModel.cs",
    "content": "﻿namespace eShop.Identity.API.Models.ManageViewModels\n{\n    public record ConfigureTwoFactorViewModel\n    {\n        public string SelectedProvider { get; init; }\n\n        public ICollection<SelectListItem> Providers { get; init; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/ManageViewModels/FactorViewModel.cs",
    "content": "﻿namespace eShop.Identity.API.Models.ManageViewModels\n{\n    public record FactorViewModel\n    {\n        public string Purpose { get; init; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/ManageViewModels/IndexViewModel.cs",
    "content": "﻿namespace eShop.Identity.API.Models.ManageViewModels\n{\n    public record IndexViewModel\n    {\n        public bool HasPassword { get; init; }\n\n        public IList<UserLoginInfo> Logins { get; init; }\n\n        public string PhoneNumber { get; init; }\n\n        public bool TwoFactor { get; init; }\n\n        public bool BrowserRemembered { get; init; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/ManageViewModels/SetPasswordViewModel.cs",
    "content": "﻿namespace eShop.Identity.API.Models.ManageViewModels\n{\n    public record SetPasswordViewModel\n    {\n        [Required]\n        [StringLength(100, ErrorMessage = \"The {0} must be at least {2} and at max {1} characters long.\", MinimumLength = 6)]\n        [DataType(DataType.Password)]\n        [Display(Name = \"New password\")]\n        public string NewPassword { get; init; }\n\n        [DataType(DataType.Password)]\n        [Display(Name = \"Confirm new password\")]\n        [Compare(\"NewPassword\", ErrorMessage = \"The new password and confirmation password do not match.\")]\n        public string ConfirmPassword { get; init; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Models/ManageViewModels/VerifyPhoneNumberViewModel.cs",
    "content": "﻿namespace eShop.Identity.API.Models.ManageViewModels\n{\n    public record VerifyPhoneNumberViewModel\n    {\n        [Required]\n        public string Code { get; init; }\n\n        [Required]\n        [Phone]\n        [Display(Name = \"Phone number\")]\n        public string PhoneNumber { get; init; }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Program.cs",
    "content": "﻿var builder = WebApplication.CreateBuilder(args);\n\nbuilder.AddServiceDefaults();\n\nbuilder.Services.AddControllersWithViews();\n\nbuilder.AddNpgsqlDbContext<ApplicationDbContext>(\"identitydb\");\n\n// Apply database migration automatically. Note that this approach is not\n// recommended for production scenarios. Consider generating SQL scripts from\n// migrations instead.\nbuilder.Services.AddMigration<ApplicationDbContext, UsersSeed>();\n\nbuilder.Services.AddIdentity<ApplicationUser, IdentityRole>()\n        .AddEntityFrameworkStores<ApplicationDbContext>()\n        .AddDefaultTokenProviders();\n\nbuilder.Services.AddIdentityServer(options =>\n{\n    //options.IssuerUri = \"null\";\n    options.Authentication.CookieLifetime = TimeSpan.FromHours(2);\n\n    options.Events.RaiseErrorEvents = true;\n    options.Events.RaiseInformationEvents = true;\n    options.Events.RaiseFailureEvents = true;\n    options.Events.RaiseSuccessEvents = true;\n\n    // TODO: Remove this line in production.\n    options.KeyManagement.Enabled = false;\n})\n.AddInMemoryIdentityResources(Config.GetResources())\n.AddInMemoryApiScopes(Config.GetApiScopes())\n.AddInMemoryApiResources(Config.GetApis())\n.AddInMemoryClients(Config.GetClients(builder.Configuration))\n.AddAspNetIdentity<ApplicationUser>()\n// TODO: Not recommended for production - you need to store your key material somewhere secure\n.AddDeveloperSigningCredential();\n\nbuilder.Services.AddTransient<IProfileService, ProfileService>();\nbuilder.Services.AddTransient<ILoginService<ApplicationUser>, EFLoginService>();\nbuilder.Services.AddTransient<IRedirectService, RedirectService>();\n\nvar app = builder.Build();\n\napp.MapDefaultEndpoints();\n\napp.UseStaticFiles();\n\n// This cookie policy fixes login issues with Chrome 80+ using HTTP\napp.UseCookiePolicy(new CookiePolicyOptions { MinimumSameSitePolicy = SameSiteMode.Lax });\napp.UseRouting();\napp.UseIdentityServer();\napp.UseAuthorization();\n\napp.MapDefaultControllerRoute();\n\napp.Run();\n"
  },
  {
    "path": "src/Identity.API/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"http\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"applicationUrl\": \"http://localhost:5223\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"https\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"applicationUrl\": \"https://localhost:5243;http://localhost:5223\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "src/Identity.API/Quickstart/Account/AccountController.cs",
    "content": "﻿// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\nnamespace IdentityServerHost.Quickstart.UI\n{\n    [SecurityHeaders]\n    [AllowAnonymous]\n    public class AccountController : Controller\n    {\n        private readonly UserManager<ApplicationUser> _userManager;\n        private readonly SignInManager<ApplicationUser> _signInManager;\n        private readonly IIdentityServerInteractionService _interaction;\n        private readonly IClientStore _clientStore;\n        private readonly IAuthenticationSchemeProvider _schemeProvider;\n        private readonly IAuthenticationHandlerProvider _handlerProvider;\n        private readonly IEventService _events;\n\n        public AccountController(\n            UserManager<ApplicationUser> userManager,\n            SignInManager<ApplicationUser> signInManager,\n            IIdentityServerInteractionService interaction,\n            IClientStore clientStore,\n            IAuthenticationSchemeProvider schemeProvider,\n            IAuthenticationHandlerProvider handlerProvider,\n            IEventService events)\n        {\n            _userManager = userManager;\n            _signInManager = signInManager;\n            _interaction = interaction;\n            _clientStore = clientStore;\n            _schemeProvider = schemeProvider;\n            _handlerProvider = handlerProvider;\n            _events = events;\n        }\n\n        /// <summary>\n        /// Entry point into the login workflow\n        /// </summary>\n        [HttpGet]\n        public async Task<IActionResult> Login(string returnUrl)\n        {\n            // build a model so we know what to show on the login page\n            var vm = await BuildLoginViewModelAsync(returnUrl);\n\n            ViewData[\"ReturnUrl\"] = returnUrl;\n\n            if (vm.IsExternalLoginOnly)\n            {\n                // we only have one option for logging in and it's an external provider\n                return RedirectToAction(\"Challenge\", \"External\", new { scheme = vm.ExternalLoginScheme, returnUrl });\n            }\n\n            return View(vm);\n        }\n\n        /// <summary>\n        /// Handle postback from username/password login\n        /// </summary>\n        [HttpPost]\n        [ValidateAntiForgeryToken]\n        public async Task<IActionResult> Login(LoginInputModel model, string button)\n        {\n            // check if we are in the context of an authorization request\n            var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);\n\n            // the user clicked the \"cancel\" button\n            if (button != \"login\")\n            {\n                if (context != null)\n                {\n                    // if the user cancels, send a result back into IdentityServer as if they \n                    // denied the consent (even if this client does not require consent).\n                    // this will send back an access denied OIDC error response to the client.\n                    await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied);\n\n                    // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null\n                    if (context.IsNativeClient())\n                    {\n                        // The client is native, so this change in how to\n                        // return the response is for better UX for the end user.\n                        return this.LoadingPage(\"Redirect\", model.ReturnUrl);\n                    }\n\n                    return Redirect(model.ReturnUrl);\n                }\n                else\n                {\n                    // since we don't have a valid context, then we just go back to the home page\n                    return Redirect(\"~/\");\n                }\n            }\n\n            if (ModelState.IsValid)\n            {\n                var result = await _signInManager.PasswordSignInAsync(model.Username, model.Password, model.RememberLogin, lockoutOnFailure: true);\n                if (result.Succeeded)\n                {\n                    var user = await _userManager.FindByNameAsync(model.Username);\n                    await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName, clientId: context?.Client.ClientId));\n\n                    if (context != null)\n                    {\n                        if (context.IsNativeClient())\n                        {\n                            // The client is native, so this change in how to\n                            // return the response is for better UX for the end user.\n                            return this.LoadingPage(\"Redirect\", model.ReturnUrl);\n                        }\n\n                        // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null\n                        return Redirect(model.ReturnUrl);\n                    }\n\n                    // request for a local page\n                    if (Url.IsLocalUrl(model.ReturnUrl))\n                    {\n                        return Redirect(model.ReturnUrl);\n                    }\n                    else if (string.IsNullOrEmpty(model.ReturnUrl))\n                    {\n                        return Redirect(\"~/\");\n                    }\n                    else\n                    {\n                        // user might have clicked on a malicious link - should be logged\n                        throw new Exception(\"invalid return URL\");\n                    }\n                }\n\n                await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, \"invalid credentials\", clientId: context?.Client.ClientId));\n                ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);\n            }\n\n            // something went wrong, show form with error\n            var vm = await BuildLoginViewModelAsync(model);\n\n            ViewData[\"ReturnUrl\"] = model.ReturnUrl;\n\n            return View(vm);\n        }\n\n\n        /// <summary>\n        /// Show logout page\n        /// </summary>\n        [HttpGet]\n        public async Task<IActionResult> Logout(string logoutId)\n        {\n            // build a model so the logout page knows what to display\n            var vm = await BuildLogoutViewModelAsync(logoutId);\n\n            if (vm.ShowLogoutPrompt == false)\n            {\n                // if the request for logout was properly authenticated from IdentityServer, then\n                // we don't need to show the prompt and can just log the user out directly.\n                return await Logout(vm);\n            }\n\n            return View(vm);\n        }\n\n        /// <summary>\n        /// Handle logout page postback\n        /// </summary>\n        [HttpPost]\n        [ValidateAntiForgeryToken]\n        public async Task<IActionResult> Logout(LogoutInputModel model)\n        {\n            // build a model so the logged out page knows what to display\n            var vm = await BuildLoggedOutViewModelAsync(model.LogoutId);\n\n            if (User?.Identity.IsAuthenticated == true)\n            {\n                // delete local authentication cookie\n                await _signInManager.SignOutAsync();\n\n                // raise the logout event\n                await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));\n            }\n\n            // check if we need to trigger sign-out at an upstream identity provider\n            if (vm.TriggerExternalSignout)\n            {\n                // build a return URL so the upstream provider will redirect back\n                // to us after the user has logged out. this allows us to then\n                // complete our single sign-out processing.\n                string url = Url.Action(\"Logout\", new { logoutId = vm.LogoutId });\n\n                // this triggers a redirect to the external provider for sign-out\n                return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme);\n            }\n\n            return View(\"LoggedOut\", vm);\n        }\n\n        [HttpGet]\n        public IActionResult AccessDenied()\n        {\n            return View();\n        }\n\n\n        /*****************************************/\n        /* helper APIs for the AccountController */\n        /*****************************************/\n        private async Task<LoginViewModel> BuildLoginViewModelAsync(string returnUrl)\n        {\n            var context = await _interaction.GetAuthorizationContextAsync(returnUrl);\n            if (context?.IdP != null && await _schemeProvider.GetSchemeAsync(context.IdP) != null)\n            {\n                var local = context.IdP == IdentityServerConstants.LocalIdentityProvider;\n\n                // this is meant to short circuit the UI and only trigger the one external IdP\n                var vm = new LoginViewModel\n                {\n                    EnableLocalLogin = local,\n                    ReturnUrl = returnUrl,\n                    Username = context?.LoginHint,\n                };\n\n                if (!local)\n                {\n                    vm.ExternalProviders = new[] { new ExternalProvider { AuthenticationScheme = context.IdP } };\n                }\n\n                return vm;\n            }\n\n            var schemes = await _schemeProvider.GetAllSchemesAsync();\n\n            var providers = schemes\n                .Where(x => x.DisplayName != null)\n                .Select(x => new ExternalProvider\n                {\n                    DisplayName = x.DisplayName ?? x.Name,\n                    AuthenticationScheme = x.Name\n                }).ToList();\n\n            var allowLocal = true;\n            if (context?.Client.ClientId != null)\n            {\n                var client = await _clientStore.FindEnabledClientByIdAsync(context.Client.ClientId);\n                if (client != null)\n                {\n                    allowLocal = client.EnableLocalLogin;\n\n                    if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any())\n                    {\n                        providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList();\n                    }\n                }\n            }\n\n            return new LoginViewModel\n            {\n                AllowRememberLogin = AccountOptions.AllowRememberLogin,\n                EnableLocalLogin = allowLocal && AccountOptions.AllowLocalLogin,\n                ReturnUrl = returnUrl,\n                Username = context?.LoginHint,\n                ExternalProviders = providers.ToArray()\n            };\n        }\n\n        private async Task<LoginViewModel> BuildLoginViewModelAsync(LoginInputModel model)\n        {\n            var vm = await BuildLoginViewModelAsync(model.ReturnUrl);\n            vm.Username = model.Username;\n            vm.RememberLogin = model.RememberLogin;\n            return vm;\n        }\n\n        private async Task<LogoutViewModel> BuildLogoutViewModelAsync(string logoutId)\n        {\n            var vm = new LogoutViewModel { LogoutId = logoutId, ShowLogoutPrompt = AccountOptions.ShowLogoutPrompt };\n\n            if (User?.Identity.IsAuthenticated != true)\n            {\n                // if the user is not authenticated, then just show logged out page\n                vm.ShowLogoutPrompt = false;\n                return vm;\n            }\n\n            var context = await _interaction.GetLogoutContextAsync(logoutId);\n            if (context?.ShowSignoutPrompt == false)\n            {\n                // it's safe to automatically sign-out\n                vm.ShowLogoutPrompt = false;\n                return vm;\n            }\n\n            // show the logout prompt. this prevents attacks where the user\n            // is automatically signed out by another malicious web page.\n            return vm;\n        }\n\n        private async Task<LoggedOutViewModel> BuildLoggedOutViewModelAsync(string logoutId)\n        {\n            // get context information (client name, post logout redirect URI and iframe for federated signout)\n            var logout = await _interaction.GetLogoutContextAsync(logoutId);\n\n            var vm = new LoggedOutViewModel\n            {\n                AutomaticRedirectAfterSignOut = AccountOptions.AutomaticRedirectAfterSignOut,\n                PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,\n                ClientName = string.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName,\n                SignOutIframeUrl = logout?.SignOutIFrameUrl,\n                LogoutId = logoutId\n            };\n\n            if (User?.Identity.IsAuthenticated == true)\n            {\n                var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;\n                if (idp != null && idp != IdentityServerConstants.LocalIdentityProvider)\n                {\n                    var handler = await _handlerProvider.GetHandlerAsync(HttpContext, idp);\n                    if (handler is IAuthenticationSignOutHandler)\n                    {\n                        if (vm.LogoutId == null)\n                        {\n                            // if there's no current logout context, we need to create one\n                            // this captures necessary info from the current logged in user\n                            // before we signout and redirect away to the external IdP for signout\n                            vm.LogoutId = await _interaction.CreateLogoutContextAsync();\n                        }\n\n                        vm.ExternalAuthenticationScheme = idp;\n                    }\n                }\n            }\n\n            return vm;\n        }\n    }\n}"
  },
  {
    "path": "src/Identity.API/Quickstart/Account/AccountOptions.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\nnamespace IdentityServerHost.Quickstart.UI;\n\npublic class AccountOptions\n{\n    public static bool AllowLocalLogin = true;\n    public static bool AllowRememberLogin = true;\n    public static TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30);\n\n    public static bool ShowLogoutPrompt = false;\n    public static bool AutomaticRedirectAfterSignOut = true;\n\n    public static string InvalidCredentialsErrorMessage = \"Invalid username or password\";\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Account/ExternalController.cs",
    "content": "namespace IdentityServerHost.Quickstart.UI;\n\n[SecurityHeaders]\n[AllowAnonymous]\npublic class ExternalController : Controller\n{\n    private readonly UserManager<ApplicationUser> _userManager;\n    private readonly SignInManager<ApplicationUser> _signInManager;\n    private readonly IIdentityServerInteractionService _interaction;\n    private readonly IClientStore _clientStore;\n    private readonly IEventService _events;\n    private readonly ILogger<ExternalController> _logger;\n\n    public ExternalController(\n        UserManager<ApplicationUser> userManager,\n        SignInManager<ApplicationUser> signInManager,\n        IIdentityServerInteractionService interaction,\n        IClientStore clientStore,\n        IEventService events,\n        ILogger<ExternalController> logger)\n    {\n        _userManager = userManager;\n        _signInManager = signInManager;\n        _interaction = interaction;\n        _clientStore = clientStore;\n        _events = events;\n        _logger = logger;\n    }\n\n    /// <summary>\n    /// initiate roundtrip to external authentication provider\n    /// </summary>\n    [HttpGet]\n    public IActionResult Challenge(string scheme, string returnUrl)\n    {\n        if (string.IsNullOrEmpty(returnUrl)) returnUrl = \"~/\";\n\n        // validate returnUrl - either it is a valid OIDC URL or back to a local page\n        if (Url.IsLocalUrl(returnUrl) == false && _interaction.IsValidReturnUrl(returnUrl) == false)\n        {\n            // user might have clicked on a malicious link - should be logged\n            throw new Exception(\"invalid return URL\");\n        }\n\n        // start challenge and roundtrip the return URL and scheme \n        var props = new AuthenticationProperties\n        {\n            RedirectUri = Url.Action(nameof(Callback)),\n            Items =\n                {\n                    { \"returnUrl\", returnUrl },\n                    { \"scheme\", scheme },\n                }\n        };\n\n        return Challenge(props, scheme);\n\n    }\n\n    /// <summary>\n    /// Post processing of external authentication\n    /// </summary>\n    [HttpGet]\n    public async Task<IActionResult> Callback()\n    {\n        // read external identity from the temporary cookie\n        var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);\n        if (result?.Succeeded != true)\n        {\n            throw new Exception(\"External authentication error\");\n        }\n\n        if (_logger.IsEnabled(LogLevel.Debug))\n        {\n            var externalClaims = result.Principal.Claims.Select(c => $\"{c.Type}: {c.Value}\");\n            _logger.LogDebug(\"External claims: {@claims}\", externalClaims);\n        }\n\n        // lookup our user and external provider info\n        var (user, provider, providerUserId, claims) = await FindUserFromExternalProviderAsync(result);\n        if (user == null)\n        {\n            // this might be where you might initiate a custom workflow for user registration\n            // in this sample we don't show how that would be done, as our sample implementation\n            // simply auto-provisions new external user\n            user = await AutoProvisionUserAsync(provider, providerUserId, claims);\n        }\n\n        // this allows us to collect any additional claims or properties\n        // for the specific protocols used and store them in the local auth cookie.\n        // this is typically used to store data needed for signout from those protocols.\n        var additionalLocalClaims = new List<Claim>();\n        var localSignInProps = new AuthenticationProperties();\n        ProcessLoginCallback(result, additionalLocalClaims, localSignInProps);\n\n        // issue authentication cookie for user\n        // we must issue the cookie maually, and can't use the SignInManager because\n        // it doesn't expose an API to issue additional claims from the login workflow\n        var principal = await _signInManager.CreateUserPrincipalAsync(user);\n        additionalLocalClaims.AddRange(principal.Claims);\n        var name = principal.FindFirst(JwtClaimTypes.Name)?.Value ?? user.Id;\n\n        var isuser = new IdentityServerUser(user.Id)\n        {\n            DisplayName = name,\n            IdentityProvider = provider,\n            AdditionalClaims = additionalLocalClaims\n        };\n\n        await HttpContext.SignInAsync(isuser, localSignInProps);\n\n        // delete temporary cookie used during external authentication\n        await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);\n\n        // retrieve return URL\n        var returnUrl = result.Properties.Items[\"returnUrl\"] ?? \"~/\";\n\n        // check if external login is in the context of an OIDC request\n        var context = await _interaction.GetAuthorizationContextAsync(returnUrl);\n        await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.Id, name, true, context?.Client.ClientId));\n\n        if (context != null)\n        {\n            if (context.IsNativeClient())\n            {\n                // The client is native, so this change in how to\n                // return the response is for better UX for the end user.\n                return this.LoadingPage(\"Redirect\", returnUrl);\n            }\n        }\n\n        return Redirect(returnUrl);\n    }\n\n    private async Task<(ApplicationUser user, string provider, string providerUserId, IEnumerable<Claim> claims)>\n        FindUserFromExternalProviderAsync(AuthenticateResult result)\n    {\n        var externalUser = result.Principal;\n\n        // try to determine the unique id of the external user (issued by the provider)\n        // the most common claim type for that are the sub claim and the NameIdentifier\n        // depending on the external provider, some other claim type might be used\n        var userIdClaim = externalUser.FindFirst(JwtClaimTypes.Subject) ??\n                          externalUser.FindFirst(ClaimTypes.NameIdentifier) ??\n                          throw new Exception(\"Unknown userid\");\n\n        // remove the user id claim so we don't include it as an extra claim if/when we provision the user\n        var claims = externalUser.Claims.ToList();\n        claims.Remove(userIdClaim);\n\n        var provider = result.Properties.Items[\"scheme\"];\n        var providerUserId = userIdClaim.Value;\n\n        // find external user\n        var user = await _userManager.FindByLoginAsync(provider, providerUserId);\n\n        return (user, provider, providerUserId, claims);\n    }\n\n    private async Task<ApplicationUser> AutoProvisionUserAsync(string provider, string providerUserId, IEnumerable<Claim> claims)\n    {\n        // create a list of claims that we want to transfer into our store\n        var filtered = new List<Claim>();\n\n        // user's display name\n        var name = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Name)?.Value ??\n            claims.FirstOrDefault(x => x.Type == ClaimTypes.Name)?.Value;\n        if (name != null)\n        {\n            filtered.Add(new Claim(JwtClaimTypes.Name, name));\n        }\n        else\n        {\n            var first = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.GivenName)?.Value ??\n                claims.FirstOrDefault(x => x.Type == ClaimTypes.GivenName)?.Value;\n            var last = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.FamilyName)?.Value ??\n                claims.FirstOrDefault(x => x.Type == ClaimTypes.Surname)?.Value;\n            if (first != null && last != null)\n            {\n                filtered.Add(new Claim(JwtClaimTypes.Name, first + \" \" + last));\n            }\n            else if (first != null)\n            {\n                filtered.Add(new Claim(JwtClaimTypes.Name, first));\n            }\n            else if (last != null)\n            {\n                filtered.Add(new Claim(JwtClaimTypes.Name, last));\n            }\n        }\n\n        // email\n        var email = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Email)?.Value ??\n           claims.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value;\n        if (email != null)\n        {\n            filtered.Add(new Claim(JwtClaimTypes.Email, email));\n        }\n\n        var user = new ApplicationUser\n        {\n            UserName = Guid.NewGuid().ToString(),\n        };\n        var identityResult = await _userManager.CreateAsync(user);\n        if (!identityResult.Succeeded) throw new Exception(identityResult.Errors.First().Description);\n\n        if (filtered.Any())\n        {\n            identityResult = await _userManager.AddClaimsAsync(user, filtered);\n            if (!identityResult.Succeeded) throw new Exception(identityResult.Errors.First().Description);\n        }\n\n        identityResult = await _userManager.AddLoginAsync(user, new UserLoginInfo(provider, providerUserId, provider));\n        if (!identityResult.Succeeded) throw new Exception(identityResult.Errors.First().Description);\n\n        return user;\n    }\n\n    // if the external login is OIDC-based, there are certain things we need to preserve to make logout work\n    // this will be different for WS-Fed, SAML2p or other protocols\n    private void ProcessLoginCallback(AuthenticateResult externalResult, List<Claim> localClaims, AuthenticationProperties localSignInProps)\n    {\n        // if the external system sent a session id claim, copy it over\n        // so we can use it for single sign-out\n        var sid = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId);\n        if (sid != null)\n        {\n            localClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value));\n        }\n\n        // if the external provider issued an id_token, we'll keep it for signout\n        var idToken = externalResult.Properties.GetTokenValue(\"id_token\");\n        if (idToken != null)\n        {\n            localSignInProps.StoreTokens(new[] { new AuthenticationToken { Name = \"id_token\", Value = idToken } });\n        }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Account/ExternalProvider.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\nnamespace IdentityServerHost.Quickstart.UI;\n\npublic class ExternalProvider\n{\n    public string DisplayName { get; set; }\n    public string AuthenticationScheme { get; set; }\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Account/LoggedOutViewModel.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\n\nnamespace IdentityServerHost.Quickstart.UI;\n\npublic class LoggedOutViewModel\n{\n    public string PostLogoutRedirectUri { get; set; }\n    public string ClientName { get; set; }\n    public string SignOutIframeUrl { get; set; }\n\n    public bool AutomaticRedirectAfterSignOut { get; set; }\n\n    public string LogoutId { get; set; }\n    public bool TriggerExternalSignout => ExternalAuthenticationScheme != null;\n    public string ExternalAuthenticationScheme { get; set; }\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Account/LoginInputModel.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\n\nnamespace IdentityServerHost.Quickstart.UI;\n\npublic class LoginInputModel\n{\n    [Required]\n    public string Username { get; set; }\n    [Required]\n    public string Password { get; set; }\n    public bool RememberLogin { get; set; }\n    public string ReturnUrl { get; set; }\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Account/LoginViewModel.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\n\nnamespace IdentityServerHost.Quickstart.UI;\n\npublic class LoginViewModel : LoginInputModel\n{\n    public bool AllowRememberLogin { get; set; } = true;\n    public bool EnableLocalLogin { get; set; } = true;\n\n    public IEnumerable<ExternalProvider> ExternalProviders { get; set; } = Enumerable.Empty<ExternalProvider>();\n    public IEnumerable<ExternalProvider> VisibleExternalProviders => ExternalProviders.Where(x => !string.IsNullOrWhiteSpace(x.DisplayName));\n\n    public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1;\n    public string ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null;\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Account/LogoutInputModel.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\n\nnamespace IdentityServerHost.Quickstart.UI;\n\npublic class LogoutInputModel\n{\n    public string LogoutId { get; set; }\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Account/LogoutViewModel.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\n\nnamespace IdentityServerHost.Quickstart.UI;\n\npublic class LogoutViewModel : LogoutInputModel\n{\n    public bool ShowLogoutPrompt { get; set; } = true;\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Account/RedirectViewModel.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\n\n\nnamespace IdentityServerHost.Quickstart.UI;\n\npublic class RedirectViewModel\n{\n    public string RedirectUrl { get; set; }\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Consent/ConsentController.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\nnamespace IdentityServerHost.Quickstart.UI;\n\n/// <summary>\n/// This controller processes the consent UI\n/// </summary>\n[SecurityHeaders]\n[Authorize]\npublic class ConsentController : Controller\n{\n    private readonly IIdentityServerInteractionService _interaction;\n    private readonly IEventService _events;\n    private readonly ILogger<ConsentController> _logger;\n\n    public ConsentController(\n        IIdentityServerInteractionService interaction,\n        IEventService events,\n        ILogger<ConsentController> logger)\n    {\n        _interaction = interaction;\n        _events = events;\n        _logger = logger;\n    }\n\n    /// <summary>\n    /// Shows the consent screen\n    /// </summary>\n    /// <param name=\"returnUrl\"></param>\n    /// <returns></returns>\n    [HttpGet]\n    public async Task<IActionResult> Index(string returnUrl)\n    {\n        var vm = await BuildViewModelAsync(returnUrl);\n        if (vm != null)\n        {\n            return View(\"Index\", vm);\n        }\n\n        return View(\"Error\");\n    }\n\n    /// <summary>\n    /// Handles the consent screen postback\n    /// </summary>\n    [HttpPost]\n    [ValidateAntiForgeryToken]\n    public async Task<IActionResult> Index(ConsentInputModel model)\n    {\n        var result = await ProcessConsent(model);\n\n        if (result.IsRedirect)\n        {\n            var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);\n            if (context?.IsNativeClient() == true)\n            {\n                // The client is native, so this change in how to\n                // return the response is for better UX for the end user.\n                return this.LoadingPage(\"Redirect\", result.RedirectUri);\n            }\n\n            return Redirect(result.RedirectUri);\n        }\n\n        if (result.HasValidationError)\n        {\n            ModelState.AddModelError(string.Empty, result.ValidationError);\n        }\n\n        if (result.ShowView)\n        {\n            return View(\"Index\", result.ViewModel);\n        }\n\n        return View(\"Error\");\n    }\n\n    /*****************************************/\n    /* helper APIs for the ConsentController */\n    /*****************************************/\n    private async Task<ProcessConsentResult> ProcessConsent(ConsentInputModel model)\n    {\n        var result = new ProcessConsentResult();\n\n        // validate return url is still valid\n        var request = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);\n        if (request == null) return result;\n\n        ConsentResponse grantedConsent = null;\n\n        // user clicked 'no' - send back the standard 'access_denied' response\n        if (model?.Button == \"no\")\n        {\n            grantedConsent = new ConsentResponse { Error = AuthorizationError.AccessDenied };\n\n            // emit event\n            await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues));\n        }\n        // user clicked 'yes' - validate the data\n        else if (model?.Button == \"yes\")\n        {\n            // if the user consented to some scope, build the response model\n            if (model.ScopesConsented != null && model.ScopesConsented.Any())\n            {\n                var scopes = model.ScopesConsented;\n                if (ConsentOptions.EnableOfflineAccess == false)\n                {\n                    scopes = scopes.Where(x => x != IdentityServerConstants.StandardScopes.OfflineAccess);\n                }\n\n                grantedConsent = new ConsentResponse\n                {\n                    RememberConsent = model.RememberConsent,\n                    ScopesValuesConsented = scopes.ToArray(),\n                    Description = model.Description\n                };\n\n                // emit event\n                await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent));\n            }\n            else\n            {\n                result.ValidationError = ConsentOptions.MustChooseOneErrorMessage;\n            }\n        }\n        else\n        {\n            result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage;\n        }\n\n        if (grantedConsent != null)\n        {\n            // communicate outcome of consent back to identityserver\n            await _interaction.GrantConsentAsync(request, grantedConsent);\n\n            // indicate that's it ok to redirect back to authorization endpoint\n            result.RedirectUri = model.ReturnUrl;\n            result.Client = request.Client;\n        }\n        else\n        {\n            // we need to redisplay the consent UI\n            result.ViewModel = await BuildViewModelAsync(model.ReturnUrl, model);\n        }\n\n        return result;\n    }\n\n    private async Task<ConsentViewModel> BuildViewModelAsync(string returnUrl, ConsentInputModel model = null)\n    {\n        var request = await _interaction.GetAuthorizationContextAsync(returnUrl);\n        if (request != null)\n        {\n            return CreateConsentViewModel(model, returnUrl, request);\n        }\n        else\n        {\n            _logger.LogError(\"No consent request matching request: {0}\", returnUrl);\n        }\n\n        return null;\n    }\n\n    private ConsentViewModel CreateConsentViewModel(\n        ConsentInputModel model, string returnUrl,\n        AuthorizationRequest request)\n    {\n        var vm = new ConsentViewModel\n        {\n            RememberConsent = model?.RememberConsent ?? true,\n            ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty<string>(),\n            Description = model?.Description,\n\n            ReturnUrl = returnUrl,\n\n            ClientName = request.Client.ClientName ?? request.Client.ClientId,\n            ClientUrl = request.Client.ClientUri,\n            ClientLogoUrl = request.Client.LogoUri,\n            AllowRememberConsent = request.Client.AllowRememberConsent\n        };\n\n        vm.IdentityScopes = request.ValidatedResources.Resources.IdentityResources.Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray();\n\n        var apiScopes = new List<ScopeViewModel>();\n        foreach (var parsedScope in request.ValidatedResources.ParsedScopes)\n        {\n            var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName);\n            if (apiScope != null)\n            {\n                var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.RawValue) || model == null);\n                apiScopes.Add(scopeVm);\n            }\n        }\n        if (ConsentOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess)\n        {\n            apiScopes.Add(GetOfflineAccessScope(vm.ScopesConsented.Contains(IdentityServerConstants.StandardScopes.OfflineAccess) || model == null));\n        }\n        vm.ApiScopes = apiScopes;\n\n        return vm;\n    }\n\n    private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)\n    {\n        return new ScopeViewModel\n        {\n            Value = identity.Name,\n            DisplayName = identity.DisplayName ?? identity.Name,\n            Description = identity.Description,\n            Emphasize = identity.Emphasize,\n            Required = identity.Required,\n            Checked = check || identity.Required\n        };\n    }\n\n    public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check)\n    {\n        var displayName = apiScope.DisplayName ?? apiScope.Name;\n        if (!string.IsNullOrWhiteSpace(parsedScopeValue.ParsedParameter))\n        {\n            displayName += \":\" + parsedScopeValue.ParsedParameter;\n        }\n\n        return new ScopeViewModel\n        {\n            Value = parsedScopeValue.RawValue,\n            DisplayName = displayName,\n            Description = apiScope.Description,\n            Emphasize = apiScope.Emphasize,\n            Required = apiScope.Required,\n            Checked = check || apiScope.Required\n        };\n    }\n\n    private ScopeViewModel GetOfflineAccessScope(bool check)\n    {\n        return new ScopeViewModel\n        {\n            Value = IdentityServerConstants.StandardScopes.OfflineAccess,\n            DisplayName = ConsentOptions.OfflineAccessDisplayName,\n            Description = ConsentOptions.OfflineAccessDescription,\n            Emphasize = true,\n            Checked = check\n        };\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Consent/ConsentInputModel.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\n\nnamespace IdentityServerHost.Quickstart.UI;\n\npublic class ConsentInputModel\n{\n    public string Button { get; set; }\n    public IEnumerable<string> ScopesConsented { get; set; }\n    public bool RememberConsent { get; set; }\n    public string ReturnUrl { get; set; }\n    public string Description { get; set; }\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Consent/ConsentOptions.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\n\nnamespace IdentityServerHost.Quickstart.UI;\n\npublic class ConsentOptions\n{\n    public static bool EnableOfflineAccess = true;\n    public static string OfflineAccessDisplayName = \"Offline Access\";\n    public static string OfflineAccessDescription = \"Access to your applications and resources, even when you are offline\";\n\n    public static readonly string MustChooseOneErrorMessage = \"You must pick at least one permission\";\n    public static readonly string InvalidSelectionErrorMessage = \"Invalid selection\";\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Consent/ConsentViewModel.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\n\nnamespace IdentityServerHost.Quickstart.UI;\n\npublic class ConsentViewModel : ConsentInputModel\n{\n    public string ClientName { get; set; }\n    public string ClientUrl { get; set; }\n    public string ClientLogoUrl { get; set; }\n    public bool AllowRememberConsent { get; set; }\n\n    public IEnumerable<ScopeViewModel> IdentityScopes { get; set; }\n    public IEnumerable<ScopeViewModel> ApiScopes { get; set; }\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Consent/ProcessConsentResult.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\nnamespace IdentityServerHost.Quickstart.UI;\n\npublic class ProcessConsentResult\n{\n    public bool IsRedirect => RedirectUri != null;\n    public string RedirectUri { get; set; }\n    public Client Client { get; set; }\n\n    public bool ShowView => ViewModel != null;\n    public ConsentViewModel ViewModel { get; set; }\n\n    public bool HasValidationError => ValidationError != null;\n    public string ValidationError { get; set; }\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Consent/ScopeViewModel.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\n\nnamespace IdentityServerHost.Quickstart.UI;\n\npublic class ScopeViewModel\n{\n    public string Value { get; set; }\n    public string DisplayName { get; set; }\n    public string Description { get; set; }\n    public bool Emphasize { get; set; }\n    public bool Required { get; set; }\n    public bool Checked { get; set; }\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Device/DeviceAuthorizationInputModel.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\n\nnamespace IdentityServerHost.Quickstart.UI;\n\npublic class DeviceAuthorizationInputModel : ConsentInputModel\n{\n    public string UserCode { get; set; }\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Device/DeviceAuthorizationViewModel.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\n\nnamespace IdentityServerHost.Quickstart.UI;\n\npublic class DeviceAuthorizationViewModel : ConsentViewModel\n{\n    public string UserCode { get; set; }\n    public bool ConfirmUserCode { get; set; }\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Device/DeviceController.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\nnamespace IdentityServerHost.Quickstart.UI;\n\n[Authorize]\n[SecurityHeaders]\npublic class DeviceController : Controller\n{\n    private readonly IDeviceFlowInteractionService _interaction;\n    private readonly IEventService _events;\n    private readonly IOptions<IdentityServerOptions> _options;\n    private readonly ILogger<DeviceController> _logger;\n\n    public DeviceController(\n        IDeviceFlowInteractionService interaction,\n        IEventService eventService,\n        IOptions<IdentityServerOptions> options,\n        ILogger<DeviceController> logger)\n    {\n        _interaction = interaction;\n        _events = eventService;\n        _options = options;\n        _logger = logger;\n    }\n\n    [HttpGet]\n    public async Task<IActionResult> Index()\n    {\n        string userCodeParamName = _options.Value.UserInteraction.DeviceVerificationUserCodeParameter;\n        string userCode = Request.Query[userCodeParamName];\n        if (string.IsNullOrWhiteSpace(userCode)) return View(\"UserCodeCapture\");\n\n        var vm = await BuildViewModelAsync(userCode);\n        if (vm == null) return View(\"Error\");\n\n        vm.ConfirmUserCode = true;\n        return View(\"UserCodeConfirmation\", vm);\n    }\n\n    [HttpPost]\n    [ValidateAntiForgeryToken]\n    public async Task<IActionResult> UserCodeCapture(string userCode)\n    {\n        var vm = await BuildViewModelAsync(userCode);\n        if (vm == null) return View(\"Error\");\n\n        return View(\"UserCodeConfirmation\", vm);\n    }\n\n    [HttpPost]\n    [ValidateAntiForgeryToken]\n    public async Task<IActionResult> Callback(DeviceAuthorizationInputModel model)\n    {\n        if (model == null) throw new ArgumentNullException(nameof(model));\n\n        var result = await ProcessConsent(model);\n        if (result.HasValidationError) return View(\"Error\");\n\n        return View(\"Success\");\n    }\n\n    private async Task<ProcessConsentResult> ProcessConsent(DeviceAuthorizationInputModel model)\n    {\n        var result = new ProcessConsentResult();\n\n        var request = await _interaction.GetAuthorizationContextAsync(model.UserCode);\n        if (request == null) return result;\n\n        ConsentResponse grantedConsent = null;\n\n        // user clicked 'no' - send back the standard 'access_denied' response\n        if (model.Button == \"no\")\n        {\n            grantedConsent = new ConsentResponse { Error = AuthorizationError.AccessDenied };\n\n            // emit event\n            await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues));\n        }\n        // user clicked 'yes' - validate the data\n        else if (model.Button == \"yes\")\n        {\n            // if the user consented to some scope, build the response model\n            if (model.ScopesConsented != null && model.ScopesConsented.Any())\n            {\n                var scopes = model.ScopesConsented;\n                if (ConsentOptions.EnableOfflineAccess == false)\n                {\n                    scopes = scopes.Where(x => x != IdentityServerConstants.StandardScopes.OfflineAccess);\n                }\n\n                grantedConsent = new ConsentResponse\n                {\n                    RememberConsent = model.RememberConsent,\n                    ScopesValuesConsented = scopes.ToArray(),\n                    Description = model.Description\n                };\n\n                // emit event\n                await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent));\n            }\n            else\n            {\n                result.ValidationError = ConsentOptions.MustChooseOneErrorMessage;\n            }\n        }\n        else\n        {\n            result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage;\n        }\n\n        if (grantedConsent != null)\n        {\n            // communicate outcome of consent back to identityserver\n            await _interaction.HandleRequestAsync(model.UserCode, grantedConsent);\n\n            // indicate that's it ok to redirect back to authorization endpoint\n            result.RedirectUri = model.ReturnUrl;\n            result.Client = request.Client;\n        }\n        else\n        {\n            // we need to redisplay the consent UI\n            result.ViewModel = await BuildViewModelAsync(model.UserCode, model);\n        }\n\n        return result;\n    }\n\n    private async Task<DeviceAuthorizationViewModel> BuildViewModelAsync(string userCode, DeviceAuthorizationInputModel model = null)\n    {\n        var request = await _interaction.GetAuthorizationContextAsync(userCode);\n        if (request != null)\n        {\n            return CreateConsentViewModel(userCode, model, request);\n        }\n\n        return null;\n    }\n\n    private DeviceAuthorizationViewModel CreateConsentViewModel(string userCode, DeviceAuthorizationInputModel model, DeviceFlowAuthorizationRequest request)\n    {\n        var vm = new DeviceAuthorizationViewModel\n        {\n            UserCode = userCode,\n            Description = model?.Description,\n\n            RememberConsent = model?.RememberConsent ?? true,\n            ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty<string>(),\n\n            ClientName = request.Client.ClientName ?? request.Client.ClientId,\n            ClientUrl = request.Client.ClientUri,\n            ClientLogoUrl = request.Client.LogoUri,\n            AllowRememberConsent = request.Client.AllowRememberConsent\n        };\n\n        vm.IdentityScopes = request.ValidatedResources.Resources.IdentityResources.Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray();\n\n        var apiScopes = new List<ScopeViewModel>();\n        foreach (var parsedScope in request.ValidatedResources.ParsedScopes)\n        {\n            var apiScope = request.ValidatedResources.Resources.FindApiScope(parsedScope.ParsedName);\n            if (apiScope != null)\n            {\n                var scopeVm = CreateScopeViewModel(parsedScope, apiScope, vm.ScopesConsented.Contains(parsedScope.RawValue) || model == null);\n                apiScopes.Add(scopeVm);\n            }\n        }\n        if (ConsentOptions.EnableOfflineAccess && request.ValidatedResources.Resources.OfflineAccess)\n        {\n            apiScopes.Add(GetOfflineAccessScope(vm.ScopesConsented.Contains(IdentityServerConstants.StandardScopes.OfflineAccess) || model == null));\n        }\n        vm.ApiScopes = apiScopes;\n\n        return vm;\n    }\n\n    private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check)\n    {\n        return new ScopeViewModel\n        {\n            Value = identity.Name,\n            DisplayName = identity.DisplayName ?? identity.Name,\n            Description = identity.Description,\n            Emphasize = identity.Emphasize,\n            Required = identity.Required,\n            Checked = check || identity.Required\n        };\n    }\n\n    public ScopeViewModel CreateScopeViewModel(ParsedScopeValue parsedScopeValue, ApiScope apiScope, bool check)\n    {\n        return new ScopeViewModel\n        {\n            Value = parsedScopeValue.RawValue,\n            DisplayName = apiScope.DisplayName ?? apiScope.Name,\n            Description = apiScope.Description,\n            Emphasize = apiScope.Emphasize,\n            Required = apiScope.Required,\n            Checked = check || apiScope.Required\n        };\n    }\n    private ScopeViewModel GetOfflineAccessScope(bool check)\n    {\n        return new ScopeViewModel\n        {\n            Value = IdentityServerConstants.StandardScopes.OfflineAccess,\n            DisplayName = ConsentOptions.OfflineAccessDisplayName,\n            Description = ConsentOptions.OfflineAccessDescription,\n            Emphasize = true,\n            Checked = check\n        };\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Diagnostics/DiagnosticsController.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\n\nnamespace IdentityServerHost.Quickstart.UI;\n\n[SecurityHeaders]\n[Authorize]\npublic class DiagnosticsController : Controller\n{\n    public async Task<IActionResult> Index()\n    {\n        var localAddresses = new string[] { \"127.0.0.1\", \"::1\", HttpContext.Connection.LocalIpAddress.ToString() };\n        if (!localAddresses.Contains(HttpContext.Connection.RemoteIpAddress.ToString()))\n        {\n            return NotFound();\n        }\n\n        var model = new DiagnosticsViewModel(await HttpContext.AuthenticateAsync());\n        return View(model);\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Diagnostics/DiagnosticsViewModel.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\n\nusing System.Text;\nusing System.Text.Json;\n\nnamespace IdentityServerHost.Quickstart.UI;\n\npublic class DiagnosticsViewModel\n{\n    public DiagnosticsViewModel(AuthenticateResult result)\n    {\n        AuthenticateResult = result;\n\n        if (result.Properties.Items.ContainsKey(\"client_list\"))\n        {\n            var encoded = result.Properties.Items[\"client_list\"];\n            var bytes = Base64Url.Decode(encoded);\n            var value = Encoding.UTF8.GetString(bytes);\n\n            Clients = JsonSerializer.Deserialize<string[]>(value);\n        }\n    }\n\n    public AuthenticateResult AuthenticateResult { get; }\n    public IEnumerable<string> Clients { get; } = new List<string>();\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Extensions.cs",
    "content": "namespace IdentityServerHost.Quickstart.UI;\n\npublic static class Extensions\n{\n    /// <summary>\n    /// Checks if the redirect URI is for a native client.\n    /// </summary>\n    /// <returns></returns>\n    public static bool IsNativeClient(this AuthorizationRequest context)\n    {\n        return !context.RedirectUri.StartsWith(\"https\", StringComparison.Ordinal)\n           && !context.RedirectUri.StartsWith(\"http\", StringComparison.Ordinal);\n    }\n\n    public static IActionResult LoadingPage(this Controller controller, string viewName, string redirectUri)\n    {\n        controller.HttpContext.Response.StatusCode = 200;\n        controller.HttpContext.Response.Headers[\"Location\"] = \"\";\n\n        return controller.View(viewName, new RedirectViewModel { RedirectUrl = redirectUri });\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Grants/GrantsController.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\nnamespace IdentityServerHost.Quickstart.UI;\n\n/// <summary>\n/// This sample controller allows a user to revoke grants given to clients\n/// </summary>\n[SecurityHeaders]\n[Authorize]\npublic class GrantsController : Controller\n{\n    private readonly IIdentityServerInteractionService _interaction;\n    private readonly IClientStore _clients;\n    private readonly IResourceStore _resources;\n    private readonly IEventService _events;\n\n    public GrantsController(IIdentityServerInteractionService interaction,\n        IClientStore clients,\n        IResourceStore resources,\n        IEventService events)\n    {\n        _interaction = interaction;\n        _clients = clients;\n        _resources = resources;\n        _events = events;\n    }\n\n    /// <summary>\n    /// Show list of grants\n    /// </summary>\n    [HttpGet]\n    public async Task<IActionResult> Index()\n    {\n        return View(\"Index\", await BuildViewModelAsync());\n    }\n\n    /// <summary>\n    /// Handle postback to revoke a client\n    /// </summary>\n    [HttpPost]\n    [ValidateAntiForgeryToken]\n    public async Task<IActionResult> Revoke(string clientId)\n    {\n        await _interaction.RevokeUserConsentAsync(clientId);\n        await _events.RaiseAsync(new GrantsRevokedEvent(User.GetSubjectId(), clientId));\n\n        return RedirectToAction(\"Index\");\n    }\n\n    private async Task<GrantsViewModel> BuildViewModelAsync()\n    {\n        var grants = await _interaction.GetAllUserGrantsAsync();\n\n        var list = new List<GrantViewModel>();\n        foreach (var grant in grants)\n        {\n            var client = await _clients.FindClientByIdAsync(grant.ClientId);\n            if (client != null)\n            {\n                var resources = await _resources.FindResourcesByScopeAsync(grant.Scopes);\n\n                var item = new GrantViewModel()\n                {\n                    ClientId = client.ClientId,\n                    ClientName = client.ClientName ?? client.ClientId,\n                    ClientLogoUrl = client.LogoUri,\n                    ClientUrl = client.ClientUri,\n                    Description = grant.Description,\n                    Created = grant.CreationTime,\n                    Expires = grant.Expiration,\n                    IdentityGrantNames = resources.IdentityResources.Select(x => x.DisplayName ?? x.Name).ToArray(),\n                    ApiGrantNames = resources.ApiScopes.Select(x => x.DisplayName ?? x.Name).ToArray()\n                };\n\n                list.Add(item);\n            }\n        }\n\n        return new GrantsViewModel\n        {\n            Grants = list\n        };\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Grants/GrantsViewModel.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\n\nnamespace IdentityServerHost.Quickstart.UI;\n\npublic class GrantsViewModel\n{\n    public IEnumerable<GrantViewModel> Grants { get; set; }\n}\n\npublic class GrantViewModel\n{\n    public string ClientId { get; set; }\n    public string ClientName { get; set; }\n    public string ClientUrl { get; set; }\n    public string ClientLogoUrl { get; set; }\n    public string Description { get; set; }\n    public DateTime Created { get; set; }\n    public DateTime? Expires { get; set; }\n    public IEnumerable<string> IdentityGrantNames { get; set; }\n    public IEnumerable<string> ApiGrantNames { get; set; }\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Home/ErrorViewModel.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\nnamespace IdentityServerHost.Quickstart.UI;\n\npublic class ErrorViewModel\n{\n    public ErrorViewModel()\n    {\n    }\n\n    public ErrorViewModel(string error)\n    {\n        Error = new ErrorMessage { Error = error };\n    }\n\n    public ErrorMessage Error { get; set; }\n}\n"
  },
  {
    "path": "src/Identity.API/Quickstart/Home/HomeController.cs",
    "content": "// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\nnamespace IdentityServerHost.Quickstart.UI\n{\n    [SecurityHeaders]\n    [AllowAnonymous]\n    public class HomeController : Controller\n    {\n        private readonly IIdentityServerInteractionService _interaction;\n        private readonly IWebHostEnvironment _environment;\n        private readonly ILogger _logger;\n\n        public HomeController(\n            IIdentityServerInteractionService interaction,\n            IWebHostEnvironment environment,\n            ILogger<HomeController> logger)\n        {\n            _interaction = interaction;\n            _environment = environment;\n            _logger = logger;\n        }\n\n        public IActionResult Index()\n        {\n            if (_environment.IsDevelopment())\n            {\n                // only show in development\n                return View();\n            }\n\n            _logger.LogInformation(\"Homepage is disabled in production. Returning 404.\");\n            return NotFound();\n        }\n\n        /// <summary>\n        /// Shows the error page\n        /// </summary>\n        public async Task<IActionResult> Error(string errorId)\n        {\n            var vm = new ErrorViewModel();\n\n            // retrieve error details from identityserver\n            var message = await _interaction.GetErrorContextAsync(errorId);\n            if (message != null)\n            {\n                vm.Error = message;\n\n                if (!_environment.IsDevelopment())\n                {\n                    // only show in development\n                    message.ErrorDescription = null;\n                }\n            }\n\n            return View(\"Error\", vm);\n        }\n    }\n}"
  },
  {
    "path": "src/Identity.API/Quickstart/SecurityHeadersAttribute.cs",
    "content": "﻿// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.\n// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.\n\n\nnamespace IdentityServerHost.Quickstart.UI;\n\npublic class SecurityHeadersAttribute : ActionFilterAttribute\n{\n    public override void OnResultExecuting(ResultExecutingContext context)\n    {\n        var result = context.Result;\n        if (result is ViewResult)\n        {\n            // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options\n            if (!context.HttpContext.Response.Headers.ContainsKey(\"X-Content-Type-Options\"))\n            {\n                context.HttpContext.Response.Headers.Append(\"X-Content-Type-Options\", \"nosniff\");\n            }\n\n            // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options\n            if (!context.HttpContext.Response.Headers.ContainsKey(\"X-Frame-Options\"))\n            {\n                context.HttpContext.Response.Headers.Append(\"X-Frame-Options\", \"SAMEORIGIN\");\n            }\n\n            // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy\n            var csp = \"default-src 'self'; object-src 'none'; frame-ancestors 'none'; sandbox allow-forms allow-same-origin allow-scripts; base-uri 'self';\";\n            // also consider adding upgrade-insecure-requests once you have HTTPS in place for production\n            //csp += \"upgrade-insecure-requests;\";\n            // also an example if you need client images to be displayed from twitter\n            // csp += \"img-src 'self' https://pbs.twimg.com;\";\n\n            // once for standards compliant browsers\n            if (!context.HttpContext.Response.Headers.ContainsKey(\"Content-Security-Policy\"))\n            {\n                context.HttpContext.Response.Headers.Append(\"Content-Security-Policy\", csp);\n            }\n            // and once again for IE\n            if (!context.HttpContext.Response.Headers.ContainsKey(\"X-Content-Security-Policy\"))\n            {\n                context.HttpContext.Response.Headers.Append(\"X-Content-Security-Policy\", csp);\n            }\n\n            // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy\n            var referrer_policy = \"no-referrer\";\n            if (!context.HttpContext.Response.Headers.ContainsKey(\"Referrer-Policy\"))\n            {\n                context.HttpContext.Response.Headers.Append(\"Referrer-Policy\", referrer_policy);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Services/EFLoginService.cs",
    "content": "﻿namespace eShop.Identity.API.Services\n{\n    public class EFLoginService : ILoginService<ApplicationUser>\n    {\n        private UserManager<ApplicationUser> _userManager;\n        private SignInManager<ApplicationUser> _signInManager;\n\n        public EFLoginService(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)\n        {\n            _userManager = userManager;\n            _signInManager = signInManager;\n        }\n\n        public async Task<ApplicationUser> FindByUsername(string user)\n        {\n            return await _userManager.FindByEmailAsync(user);\n        }\n\n        public async Task<bool> ValidateCredentials(ApplicationUser user, string password)\n        {\n            return await _userManager.CheckPasswordAsync(user, password);\n        }\n\n        public Task SignIn(ApplicationUser user)\n        {\n            return _signInManager.SignInAsync(user, true);\n        }\n\n        public Task SignInAsync(ApplicationUser user, AuthenticationProperties properties, string authenticationMethod = null)\n        {\n            return _signInManager.SignInAsync(user, properties, authenticationMethod);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Services/ILoginService.cs",
    "content": "﻿namespace eShop.Identity.API.Services\n{\n    public interface ILoginService<T>\n    {\n        Task<bool> ValidateCredentials(T user, string password);\n\n        Task<T> FindByUsername(string user);\n\n        Task SignIn(T user);\n\n        Task SignInAsync(T user, AuthenticationProperties properties, string authenticationMethod = null);\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Services/IRedirectService.cs",
    "content": "﻿namespace eShop.Identity.API.Services\n{\n    public interface IRedirectService\n    {\n        string ExtractRedirectUriFromReturnUrl(string url);\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Services/ProfileService.cs",
    "content": "﻿namespace eShop.Identity.API.Services\n{\n    public class ProfileService : IProfileService\n    {\n        private readonly UserManager<ApplicationUser> _userManager;\n\n        public ProfileService(UserManager<ApplicationUser> userManager)\n        {\n            _userManager = userManager;\n        }\n\n        public async Task GetProfileDataAsync(ProfileDataRequestContext context)\n        {\n            var subject = context.Subject ?? throw new ArgumentNullException(nameof(context.Subject));\n\n            var subjectId = subject.Claims.Where(x => x.Type == \"sub\").FirstOrDefault()?.Value;\n\n            var user = await _userManager.FindByIdAsync(subjectId);\n            if (user == null)\n                throw new ArgumentException(\"Invalid subject identifier\");\n\n            var claims = GetClaimsFromUser(user);\n            context.IssuedClaims = claims.ToList();\n        }\n\n        public async Task IsActiveAsync(IsActiveContext context)\n        {\n            var subject = context.Subject ?? throw new ArgumentNullException(nameof(context.Subject));\n\n            var subjectId = subject.Claims.Where(x => x.Type == \"sub\").FirstOrDefault()?.Value;\n            var user = await _userManager.FindByIdAsync(subjectId);\n\n            context.IsActive = false;\n\n            if (user != null)\n            {\n                if (_userManager.SupportsUserSecurityStamp)\n                {\n                    var security_stamp = subject.Claims.Where(c => c.Type == \"security_stamp\").Select(c => c.Value).SingleOrDefault();\n                    if (security_stamp != null)\n                    {\n                        var db_security_stamp = await _userManager.GetSecurityStampAsync(user);\n                        if (db_security_stamp != security_stamp)\n                            return;\n                    }\n                }\n\n                context.IsActive =\n                    !user.LockoutEnabled ||\n                    !user.LockoutEnd.HasValue ||\n                    user.LockoutEnd <= DateTime.UtcNow;\n            }\n        }\n\n        private IEnumerable<Claim> GetClaimsFromUser(ApplicationUser user)\n        {\n            var claims = new List<Claim>\n            {\n                new Claim(JwtClaimTypes.Subject, user.Id),\n                new Claim(JwtClaimTypes.PreferredUserName, user.UserName),\n                new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName)\n            };\n\n            if (!string.IsNullOrWhiteSpace(user.Name))\n                claims.Add(new Claim(\"name\", user.Name));\n\n            if (!string.IsNullOrWhiteSpace(user.LastName))\n                claims.Add(new Claim(\"last_name\", user.LastName));\n\n            if (!string.IsNullOrWhiteSpace(user.CardNumber))\n                claims.Add(new Claim(\"card_number\", user.CardNumber));\n\n            if (!string.IsNullOrWhiteSpace(user.CardHolderName))\n                claims.Add(new Claim(\"card_holder\", user.CardHolderName));\n\n            if (!string.IsNullOrWhiteSpace(user.SecurityNumber))\n                claims.Add(new Claim(\"card_security_number\", user.SecurityNumber));\n\n            if (!string.IsNullOrWhiteSpace(user.Expiration))\n                claims.Add(new Claim(\"card_expiration\", user.Expiration));\n\n            if (!string.IsNullOrWhiteSpace(user.City))\n                claims.Add(new Claim(\"address_city\", user.City));\n\n            if (!string.IsNullOrWhiteSpace(user.Country))\n                claims.Add(new Claim(\"address_country\", user.Country));\n\n            if (!string.IsNullOrWhiteSpace(user.State))\n                claims.Add(new Claim(\"address_state\", user.State));\n\n            if (!string.IsNullOrWhiteSpace(user.Street))\n                claims.Add(new Claim(\"address_street\", user.Street));\n\n            if (!string.IsNullOrWhiteSpace(user.ZipCode))\n                claims.Add(new Claim(\"address_zip_code\", user.ZipCode));\n\n            if (_userManager.SupportsUserEmail)\n            {\n                claims.AddRange(new[]\n                {\n                    new Claim(JwtClaimTypes.Email, user.Email),\n                    new Claim(JwtClaimTypes.EmailVerified, user.EmailConfirmed ? \"true\" : \"false\", ClaimValueTypes.Boolean)\n                });\n            }\n\n            if (_userManager.SupportsUserPhoneNumber && !string.IsNullOrWhiteSpace(user.PhoneNumber))\n            {\n                claims.AddRange(new[]\n                {\n                    new Claim(JwtClaimTypes.PhoneNumber, user.PhoneNumber),\n                    new Claim(JwtClaimTypes.PhoneNumberVerified, user.PhoneNumberConfirmed ? \"true\" : \"false\", ClaimValueTypes.Boolean)\n                });\n            }\n\n            return claims;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Services/RedirectService.cs",
    "content": "﻿namespace eShop.Identity.API.Services\n{\n    public class RedirectService : IRedirectService\n    {\n        public string ExtractRedirectUriFromReturnUrl(string url)\n        {\n            var decodedUrl = System.Net.WebUtility.HtmlDecode(url);\n            var results = Regex.Split(decodedUrl, \"redirect_uri=\");\n            if (results.Length < 2)\n                return \"\";\n\n            string result = results[1];\n\n            string splitKey;\n            if (result.Contains(\"signin-oidc\"))\n                splitKey = \"signin-oidc\";\n            else\n                splitKey = \"scope\";\n\n            results = Regex.Split(result, splitKey);\n            if (results.Length < 2)\n                return \"\";\n\n            result = results[0];\n\n            return result.Replace(\"%3A\", \":\").Replace(\"%2F\", \"/\").Replace(\"&\", \"\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/UsersSeed.cs",
    "content": "﻿\nnamespace eShop.Identity.API;\n\npublic class UsersSeed(ILogger<UsersSeed> logger, UserManager<ApplicationUser> userManager) : IDbSeeder<ApplicationDbContext>\n{\n    public async Task SeedAsync(ApplicationDbContext context)\n    {\n        var alice = await userManager.FindByNameAsync(\"alice\");\n\n        if (alice == null)\n        {\n            alice = new ApplicationUser\n            {\n                UserName = \"alice\",\n                Email = \"AliceSmith@email.com\",\n                EmailConfirmed = true,\n                CardHolderName = \"Alice Smith\",\n                CardNumber = \"XXXXXXXXXXXX1881\",\n                CardType = 1,\n                City = \"Redmond\",\n                Country = \"U.S.\",\n                Expiration = \"12/24\",\n                Id = Guid.NewGuid().ToString(),\n                LastName = \"Smith\",\n                Name = \"Alice\",\n                PhoneNumber = \"1234567890\",\n                ZipCode = \"98052\",\n                State = \"WA\",\n                Street = \"15703 NE 61st Ct\",\n                SecurityNumber = \"123\"\n            };\n\n            var result = await userManager.CreateAsync(alice, \"Pass123$\");\n\n            if (!result.Succeeded)\n            {\n                throw new Exception(result.Errors.First().Description);\n            }\n\n            if (logger.IsEnabled(LogLevel.Debug))\n            {\n                logger.LogDebug(\"alice created\");\n            }\n        }\n        else\n        {\n            if (logger.IsEnabled(LogLevel.Debug))\n            {\n                logger.LogDebug(\"alice already exists\");\n            }\n        }\n\n        var bob = await userManager.FindByNameAsync(\"bob\");\n\n        if (bob == null)\n        {\n            bob = new ApplicationUser\n            {\n                UserName = \"bob\",\n                Email = \"BobSmith@email.com\",\n                EmailConfirmed = true,\n                CardHolderName = \"Bob Smith\",\n                CardNumber = \"XXXXXXXXXXXX1881\",\n                CardType = 1,\n                City = \"Redmond\",\n                Country = \"U.S.\",\n                Expiration = \"12/24\",\n                Id = Guid.NewGuid().ToString(),\n                LastName = \"Smith\",\n                Name = \"Bob\",\n                PhoneNumber = \"1234567890\",\n                ZipCode = \"98052\",\n                State = \"WA\",\n                Street = \"15703 NE 61st Ct\",\n                SecurityNumber = \"456\"\n            };\n\n            var result = await userManager.CreateAsync(bob, \"Pass123$\");\n\n            if (!result.Succeeded)\n            {\n                throw new Exception(result.Errors.First().Description);\n            }\n\n            if (logger.IsEnabled(LogLevel.Debug))\n            {\n                logger.LogDebug(\"bob created\");\n            }\n        }\n        else\n        {\n            if (logger.IsEnabled(LogLevel.Debug))\n            {\n                logger.LogDebug(\"bob already exists\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Views/Account/AccessDenied.cshtml",
    "content": "\n<div class=\"container\">\n    <div class=\"lead\">\n        <h1>Access Denied</h1>\n        <p>You do not have access to that resource.</p>\n    </div>\n</div>"
  },
  {
    "path": "src/Identity.API/Views/Account/LoggedOut.cshtml",
    "content": "@model LoggedOutViewModel\n\n@{ \n    // set this so the layout rendering sees an anonymous user\n    ViewData[\"signed-out\"] = true;\n}\n\n<div class=\"logged-out-page\">\n    <h1>\n        Logout\n        <small>You are now logged out</small>\n    </h1>\n\n    @if (Model.PostLogoutRedirectUri != null)\n    {\n        <div>\n            Click <a class=\"PostLogoutRedirectUri\" href=\"@Model.PostLogoutRedirectUri\">here</a> to return to the\n            <span>@Model.ClientName</span> application.\n        </div>\n    }\n\n    @if (Model.SignOutIframeUrl != null)\n    {\n        <iframe width=\"0\" height=\"0\" class=\"signout\" src=\"@Model.SignOutIframeUrl\"></iframe>\n    }\n</div>\n\n@section scripts\n{\n    @if (Model.AutomaticRedirectAfterSignOut)\n    {\n        <script src=\"~/js/signout-redirect.js\"></script>\n    }\n}\n"
  },
  {
    "path": "src/Identity.API/Views/Account/Login.cshtml",
    "content": "@model LoginViewModel\n\n<div class=\"login-page\">\n    <div class=\"container\">\n        <div class=\"row top-buffer\">\n\n            @if (Model.EnableLocalLogin)\n            {\n                <div class=\"col\"></div>\n                <div class=\"col-sm-6\">\n                    <form asp-route=\"Login\">\n                        <input type=\"hidden\" asp-for=\"ReturnUrl\" />\n                        <div class=\"login-container\">\n                            <h2>Login</h2>\n                            <partial name=\"_ValidationSummary\" />\n                            <div class=\"form-group\">\n                                <label asp-for=\"Username\"></label>\n                                <input class=\"form-control\" placeholder=\"Username\" asp-for=\"Username\" autofocus>\n                            </div> \n                            <div class=\"form-group\">\n                                <label asp-for=\"Password\"></label>\n                                <input type=\"password\" class=\"form-control\" placeholder=\"Password\" asp-for=\"Password\" autocomplete=\"off\">\n                            </div>\n                            @if (Model.AllowRememberLogin)\n                            {\n                                <div class=\"form-group\">\n                                    <div class=\"form-check\">\n                                        <input class=\"form-check-input\" asp-for=\"RememberLogin\">\n                                        <label class=\"form-check-label\" asp-for=\"RememberLogin\">\n                                            Remember me\n                                        </label>\n                                    </div>\n                                </div>\n                            }\n\n                            <div>\n                                <p>The default users are alice/bob, password: Pass123$</p>\n                            </div>\n                            <button class=\"btn btn-primary\" name=\"button\" value=\"login\">Login</button>\n                        </div>\n                    </form>\n                </div>\n                <div class=\"col\"></div>\n            }\n        </div>\n    </div>\n</div>"
  },
  {
    "path": "src/Identity.API/Views/Account/Logout.cshtml",
    "content": "@model LogoutViewModel\n\n<div class=\"logout-page\">\n    <div class=\"lead\">\n        <h1>Logout</h1>\n        <p>Would you like to logut of IdentityServer?</p>\n    </div>\n\n    <form asp-action=\"Logout\">\n        <input type=\"hidden\" name=\"logoutId\" value=\"@Model.LogoutId\"  />\n        <div class=\"form-group\">\n            <button class=\"btn btn-primary\">Yes</button>\n        </div>\n    </form>\n</div>\n"
  },
  {
    "path": "src/Identity.API/Views/Consent/Index.cshtml",
    "content": "@model ConsentViewModel\n\n<div class=\"page-consent\">\n    <div class=\"lead\">\n        @if (Model.ClientLogoUrl != null)\n        {\n            <div class=\"client-logo\"><img src=\"@Model.ClientLogoUrl\"></div>\n        }\n        <h1>\n            @Model.ClientName\n            <small class=\"text-muted\">is requesting your permission</small>\n        </h1>\n        <p>Uncheck the permissions you do not wish to grant.</p>\n    </div>\n\n    <div class=\"row\">\n        <div class=\"col-sm-8\">\n            <partial name=\"_ValidationSummary\" />\n        </div>\n    </div>\n\n    <form asp-action=\"Index\">\n        <input type=\"hidden\" asp-for=\"ReturnUrl\" />\n        <div class=\"row\">\n            <div class=\"col-sm-8\">\n                @if (Model.IdentityScopes.Any())\n                {\n                    <div class=\"form-group\">\n                        <div class=\"card\">\n                            <div class=\"card-header\">\n                                <span class=\"glyphicon glyphicon-user\"></span>\n                                Personal Information\n                            </div>\n                            <ul class=\"list-group list-group-flush\">\n                                @foreach (var scope in Model.IdentityScopes)\n                                {\n                                    <partial name=\"_ScopeListItem\" model=\"@scope\" />\n                                }\n                            </ul>\n                        </div>\n                    </div>\n                }\n\n                @if (Model.ApiScopes.Any())\n                {\n                    <div class=\"form-group\">\n                        <div class=\"card\">\n                            <div class=\"card-header\">\n                                <span class=\"glyphicon glyphicon-tasks\"></span>\n                                Application Access\n                            </div>\n                            <ul class=\"list-group list-group-flush\">\n                                @foreach (var scope in Model.ApiScopes)\n                                {\n                                    <partial name=\"_ScopeListItem\" model=\"scope\" />\n                                }\n                            </ul>\n                        </div>\n                    </div>\n                }\n\n                <div class=\"form-group\">\n                    <div class=\"card\">\n                        <div class=\"card-header\">\n                            <span class=\"glyphicon glyphicon-tasks\"></span>\n                            Description\n                        </div>\n                        <div class=\"card-body\">\n                            <input class=\"form-control\" placeholder=\"Description or name of device\" asp-for=\"Description\" autofocus>\n                        </div>\n                    </div>\n                </div>\n\n                @if (Model.AllowRememberConsent)\n                {\n                    <div class=\"form-group\">\n                        <div class=\"form-check\">\n                            <input class=\"form-check-input\" asp-for=\"RememberConsent\">\n                            <label class=\"form-check-label\" asp-for=\"RememberConsent\">\n                                <strong>Remember My Decision</strong>\n                            </label>\n                        </div>\n                    </div>\n                }\n            </div>\n        </div>\n\n        <div class=\"row\">\n            <div class=\"col-sm-4\">\n                <button name=\"button\" value=\"yes\" class=\"btn btn-primary\" autofocus>Yes, Allow</button>\n                <button name=\"button\" value=\"no\" class=\"btn btn-secondary\">No, Do Not Allow</button>\n            </div>\n            <div class=\"col-sm-4 col-lg-auto\">\n                @if (Model.ClientUrl != null)\n                {\n                    <a class=\"btn btn-outline-info\" href=\"@Model.ClientUrl\">\n                        <span class=\"glyphicon glyphicon-info-sign\"></span>\n                        <strong>@Model.ClientName</strong>\n                    </a>\n                }\n            </div>\n        </div>\n    </form>\n</div>\n"
  },
  {
    "path": "src/Identity.API/Views/Device/Success.cshtml",
    "content": "\n<div class=\"page-device-success\">\n    <div class=\"lead\">\n        <h1>Success</h1>\n        <p>You have successfully authorized the device</p>\n    </div>\n</div>\n"
  },
  {
    "path": "src/Identity.API/Views/Device/UserCodeCapture.cshtml",
    "content": "@model string\n\n<div class=\"page-device-code\">\n    <div class=\"lead\">\n        <h1>User Code</h1>\n        <p>Please enter the code displayed on your device.</p>\n    </div>\n\n    <partial name=\"_ValidationSummary\" />\n\n    <div class=\"row\">\n        <div class=\"col-sm-6\">\n            <form asp-action=\"UserCodeCapture\">\n                <div class=\"form-group\">\n                    <label for=\"userCode\">User Code:</label>\n                    <input class=\"form-control\" for=\"userCode\" name=\"userCode\" autofocus />\n                </div>\n\n                <button class=\"btn btn-primary\" name=\"button\">Submit</button>\n            </form>\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "src/Identity.API/Views/Device/UserCodeConfirmation.cshtml",
    "content": "@model DeviceAuthorizationViewModel\n\n<div class=\"page-device-confirmation\">\n    <div class=\"lead\">\n        @if (Model.ClientLogoUrl != null)\n        {\n            <div class=\"client-logo\"><img src=\"@Model.ClientLogoUrl\"></div>\n        }\n        <h1>\n            @Model.ClientName\n            <small class=\"text-muted\">is requesting your permission</small>\n        </h1>\n        @if (Model.ConfirmUserCode)\n        {\n            <p>Please confirm that the authorization request quotes the code: <strong>@Model.UserCode</strong>.</p>\n        }\n        <p>Uncheck the permissions you do not wish to grant.</p>\n    </div>\n\n    <div class=\"row\">\n        <div class=\"col-sm-8\">\n            <partial name=\"_ValidationSummary\" />\n        </div>\n    </div>\n\n    <form asp-action=\"Callback\">\n        <input asp-for=\"UserCode\" type=\"hidden\" value=\"@Model.UserCode\" />\n        <div class=\"row\">\n            <div class=\"col-sm-8\">\n                @if (Model.IdentityScopes.Any())\n                {\n                    <div class=\"form-group\">\n                        <div class=\"card\">\n                            <div class=\"card-header\">\n                                <span class=\"glyphicon glyphicon-user\"></span>\n                                Personal Information\n                            </div>\n                            <ul class=\"list-group list-group-flush\">\n                                @foreach (var scope in Model.IdentityScopes)\n                                {\n                                    <partial name=\"_ScopeListItem\" model=\"@scope\" />\n                                }\n                            </ul>\n                        </div>\n                    </div>\n                }\n\n                @if (Model.ApiScopes.Any())\n                {\n                    <div class=\"form-group\">\n                        <div class=\"card\">\n                            <div class=\"card-header\">\n                                <span class=\"glyphicon glyphicon-tasks\"></span>\n                                Application Access\n                            </div>\n                            <ul class=\"list-group list-group-flush\">\n                                @foreach (var scope in Model.ApiScopes)\n                                {\n                                    <partial name=\"_ScopeListItem\" model=\"scope\" />\n                                }\n                            </ul>\n                        </div>\n                    </div>\n                }\n\n                <div class=\"form-group\">\n                    <div class=\"card\">\n                        <div class=\"card-header\">\n                            <span class=\"glyphicon glyphicon-tasks\"></span>\n                            Description\n                        </div>\n                        <div class=\"card-body\">\n                            <input class=\"form-control\" placeholder=\"Description or name of device\" asp-for=\"Description\" autofocus>\n                        </div>\n                    </div>\n                </div>\n\n                @if (Model.AllowRememberConsent)\n                {\n                    <div class=\"form-group\">\n                        <div class=\"form-check\">\n                            <input class=\"form-check-input\" asp-for=\"RememberConsent\">\n                            <label class=\"form-check-label\" asp-for=\"RememberConsent\">\n                                <strong>Remember My Decision</strong>\n                            </label>\n                        </div>\n                    </div>\n                }\n            </div>\n        </div>\n\n        <div class=\"row\">\n            <div class=\"col-sm-4\">\n                <button name=\"button\" value=\"yes\" class=\"btn btn-primary\" autofocus>Yes, Allow</button>\n                <button name=\"button\" value=\"no\" class=\"btn btn-secondary\">No, Do Not Allow</button>\n            </div>\n            <div class=\"col-sm-4 col-lg-auto\">\n                @if (Model.ClientUrl != null)\n                {\n                    <a class=\"btn btn-outline-info\" href=\"@Model.ClientUrl\">\n                        <span class=\"glyphicon glyphicon-info-sign\"></span>\n                        <strong>@Model.ClientName</strong>\n                    </a>\n                }\n            </div>\n        </div>\n    </form>\n</div>\n"
  },
  {
    "path": "src/Identity.API/Views/Diagnostics/Index.cshtml",
    "content": "@model DiagnosticsViewModel\n\n<div class=\"diagnostics-page\">\n    <div class=\"lead\">\n        <h1>Authentication Cookie</h1>\n    </div>\n\n    <div class=\"row\">\n        <div class=\"col\">\n            <div class=\"card\">\n                <div class=\"card-header\">\n                    <h2>Claims</h2>\n                </div>\n                <div class=\"card-body\">\n                    <dl>\n                        @foreach (var claim in Model.AuthenticateResult.Principal.Claims)\n                        {\n                            <dt>@claim.Type</dt>\n                            <dd>@claim.Value</dd>\n                        }\n                    </dl>\n                </div>\n            </div>\n        </div>\n        \n        <div class=\"col\">\n            <div class=\"card\">\n                <div class=\"card-header\">\n                    <h2>Properties</h2>\n                </div>\n                <div class=\"card-body\">\n                    <dl>\n                        @foreach (var prop in Model.AuthenticateResult.Properties.Items)\n                        {\n                            <dt>@prop.Key</dt>\n                            <dd>@prop.Value</dd>\n                        }\n                        @if (Model.Clients.Any())\n                        {\n                            <dt>Clients</dt>\n                            <dd>\n                            @{\n                                var clients = Model.Clients.ToArray();\n                                for(var i = 0; i < clients.Length; i++)\n                                {\n                                    <text>@clients[i]</text>\n                                    if (i < clients.Length - 1)\n                                    {\n                                        <text>, </text>\n                                    }\n                                }\n                            }\n                            </dd>\n                        }\n                    </dl>\n                </div>\n            </div>\n        </div>\n    </div>\n</div>\n\n\n\n\n"
  },
  {
    "path": "src/Identity.API/Views/Grants/Index.cshtml",
    "content": "@model GrantsViewModel\n\n<div class=\"grants-page\">\n    <div class=\"lead\">\n        <h1>Client Application Permissions</h1>\n        <p>Below is the list of applications you have given permission to and the resources they have access to.</p>\n    </div>\n\n    @if (Model.Grants.Any() == false)\n    {\n        <div class=\"row\">\n            <div class=\"col-sm-8\">\n                <div class=\"alert alert-info\">\n                    You have not given access to any applications\n                </div>\n            </div>\n        </div>\n    }\n    else\n    {\n        foreach (var grant in Model.Grants)\n        {\n            <div class=\"card\">\n                <div class=\"card-header\">\n                    <div class=\"row\">\n                        <div class=\"col-sm-8 card-title\">\n                            @if (grant.ClientLogoUrl != null)\n                            {\n                                <img src=\"@grant.ClientLogoUrl\">\n                            }\n                            <strong>@grant.ClientName</strong>\n                        </div>\n\n                        <div class=\"col-sm-2\">\n                            <form asp-action=\"Revoke\">\n                                <input type=\"hidden\" name=\"clientId\" value=\"@grant.ClientId\">\n                                <button class=\"btn btn-danger\">Revoke Access</button>\n                            </form>\n                        </div>\n                    </div>\n                </div>\n                \n                <ul class=\"list-group list-group-flush\">\n                    @if (grant.Description != null)\n                    {\n                        <li class=\"list-group-item\">\n                            <label>Description:</label> @grant.Description\n                        </li>   \n                    }\n                    <li class=\"list-group-item\">\n                        <label>Created:</label> @grant.Created.ToString(\"yyyy-MM-dd\")\n                    </li>\n                    @if (grant.Expires.HasValue)\n                    {\n                        <li class=\"list-group-item\">\n                            <label>Expires:</label> @grant.Expires.Value.ToString(\"yyyy-MM-dd\")\n                        </li>\n                    }\n                    @if (grant.IdentityGrantNames.Any())\n                    {\n                        <li class=\"list-group-item\">\n                            <label>Identity Grants</label>\n                            <ul>\n                                @foreach (var name in grant.IdentityGrantNames)\n                                {\n                                    <li>@name</li>\n                                }\n                            </ul>\n                        </li>\n                    }\n                    @if (grant.ApiGrantNames.Any())\n                    {\n                        <li class=\"list-group-item\">\n                            <label>API Grants</label>\n                            <ul>\n                                @foreach (var name in grant.ApiGrantNames)\n                                {\n                                    <li>@name</li>\n                                }\n                            </ul>\n                        </li>\n                    }\n                </ul>\n            </div>\n        }\n    }\n</div>"
  },
  {
    "path": "src/Identity.API/Views/Home/Index.cshtml",
    "content": "@using System.Diagnostics\n\n@{\n    var version = FileVersionInfo.GetVersionInfo(typeof(Duende.IdentityServer.Hosting.IdentityServerMiddleware).Assembly.Location).ProductVersion.Split('+').First();\n}\n\n<div class=\"welcome-page\">\n    <h1>\n        <img src=\"~/icon.jpg\">\n        Welcome to IdentityServer4\n        <small class=\"text-muted\">(version @version)</small>\n    </h1>\n\n    <ul>\n        <li>\n            IdentityServer publishes a\n            <a href=\"~/.well-known/openid-configuration\">discovery document</a>\n            where you can find metadata and links to all the endpoints, key material, etc.\n        </li>\n        <li>\n            Click <a href=\"~/diagnostics\">here</a> to see the claims for your current session.\n        </li>\n        <li>\n            Click <a href=\"~/grants\">here</a> to manage your stored grants.\n        </li>\n        <li>\n            Here are links to the\n            <a href=\"https://github.com/identityserver/IdentityServer4\">source code repository</a>,\n            and <a href=\"https://github.com/IdentityServer/IdentityServer4/tree/main/samples\">ready to use samples</a>.\n        </li>\n    </ul>\n</div>\n"
  },
  {
    "path": "src/Identity.API/Views/Shared/Error.cshtml",
    "content": "@model ErrorViewModel\n\n@{\n    var error = Model?.Error?.Error;\n    var errorDescription = Model?.Error?.ErrorDescription;\n    var request_id = Model?.Error?.RequestId;\n}\n\n<div class=\"error-page\">\n    <div class=\"lead\">\n        <h1>Error</h1>\n    </div>\n\n    <div class=\"row\">\n        <div class=\"col-sm-6\">\n            <div class=\"alert alert-danger\">\n                Sorry, there was an error\n\n                @if (error != null)\n                {\n                    <strong>\n                        <em>\n                            : @error\n                        </em>\n                    </strong>\n\n                    if (errorDescription != null)\n                    {\n                        <div>@errorDescription</div>\n                    }\n                }\n            </div>\n\n            @if (request_id != null)\n            {\n                <div class=\"request-id\">Request Id: @request_id</div>\n            }\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "src/Identity.API/Views/Shared/Redirect.cshtml",
    "content": "@model RedirectViewModel\n\n<div class=\"redirect-page\">\n    <div class=\"lead\">\n        <h1>You are now being returned to the application</h1>\n        <p>Once complete, you may close this tab.</p>\n    </div>\n</div>\n\n<meta http-equiv=\"refresh\" content=\"0;url=@Model.RedirectUrl\" data-url=\"@Model.RedirectUrl\">\n<script src=\"~/js/signin-redirect.js\"></script>\n"
  },
  {
    "path": "src/Identity.API/Views/Shared/_Layout.cshtml",
    "content": "﻿@using Microsoft.Extensions.Configuration\n@inject IConfiguration Configuration\n\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, shrink-to-fit=no\" />\n\n    <title>Identity | AdventureWorks</title>\n\n    <link rel=\"icon\" type=\"image/x-icon\" href=\"favicon.png\">\n    <link rel=\"shortcut icon\" type=\"image/x-icon\" href=\"~/favicon.png\" />\n\n    <link rel=\"stylesheet\" href=\"~/lib/bootstrap/dist/css/bootstrap.min.css\" />\n    <link rel=\"stylesheet\" href=\"~/css/site.css\" />\n</head>\n<body>\n    <div class='eshop-banner'>\n        <img role=\"presentation\" src=\"/images/banner.png\" />\n    </div>\n    <div class=\"eshop-header\">\n        <div class=\"eshop-header-container\">\n            <nav class=\"eshop-header-navbar\">\n                <a href=\"@Configuration[\"WebAppClient\"]\" class='logo logo-header'>\n                    <img alt=\"AdventureWorks\" src='/images/logo-header.svg' />\n                </a>\n            </nav>\n      </div>\n    </div>\n\n    <div class=\"content\">\n        @RenderBody()\n    </div>\n\n    <footer class='eshop-footer'>\n        <div class='eshop-footer-content'>\n            <div class='eshop-footer-row'>\n                <img role=\"presentation\" src='/images/logo-footer.svg' class='logo logo-footer' />\n                <p>© AdventureWorks</p>\n            </div>\n        </div>\n    </footer>\n\n    <script src=\"~/lib/jquery/dist/jquery.slim.min.js\"></script>\n    <script src=\"~/lib/bootstrap/dist/js/bootstrap.bundle.min.js\"></script>\n\n    @RenderSection(\"scripts\", required: false)\n</body>\n</html>\n"
  },
  {
    "path": "src/Identity.API/Views/Shared/_ScopeListItem.cshtml",
    "content": "@model ScopeViewModel\n\n<li class=\"list-group-item\">\n    <label>\n        <input class=\"consent-scopecheck\"\n               type=\"checkbox\"\n               name=\"ScopesConsented\"\n               id=\"scopes_@Model.Value\"\n               value=\"@Model.Value\"\n               checked=\"@Model.Checked\"\n               disabled=\"@Model.Required\" />\n        @if (Model.Required)\n        {\n            <input type=\"hidden\"\n                   name=\"ScopesConsented\"\n                   value=\"@Model.Value\" />\n        }\n        <strong>@Model.DisplayName</strong>\n        @if (Model.Emphasize)\n        {\n            <span class=\"glyphicon glyphicon-exclamation-sign\"></span>\n        }\n    </label>\n    @if (Model.Required)\n    {\n        <span><em>(required)</em></span>\n    }\n    @if (Model.Description != null)\n    {\n        <div class=\"consent-description\">\n            <label for=\"scopes_@Model.Value\">@Model.Description</label>\n        </div>\n    }\n</li>"
  },
  {
    "path": "src/Identity.API/Views/Shared/_ValidationSummary.cshtml",
    "content": "@if (ViewContext.ModelState.IsValid == false)\n{\n    <div class=\"alert alert-danger\">\n        <strong>Error</strong>\n        <div asp-validation-summary=\"All\" class=\"danger\"></div>\n    </div>\n}"
  },
  {
    "path": "src/Identity.API/Views/_ViewImports.cshtml",
    "content": "@using IdentityServerHost.Quickstart.UI\n@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers\n"
  },
  {
    "path": "src/Identity.API/Views/_ViewStart.cshtml",
    "content": "@{\n    Layout = \"_Layout\";\n}\n"
  },
  {
    "path": "src/Identity.API/appsettings.Development.json",
    "content": "{\n  \"ConnectionStrings\": {\n    \"IdentityDB\": \"Host=localhost;Database=IdentityDB;Username=postgres;Password=yourWeak(!)Password\"\n  }\n}"
  },
  {
    "path": "src/Identity.API/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  },\n  \"MauiCallback\": \"maui://authcallback\",\n  \"UseCustomizationData\": false,\n  \"TokenLifetimeMinutes\": 120,\n  \"PermanentTokenLifetimeDays\": 365\n}\n"
  },
  {
    "path": "src/Identity.API/bundleconfig.json",
    "content": "﻿// Configure bundling and minification for the project.\n// More info at https://go.microsoft.com/fwlink/?LinkId=808241\n[\n  {\n    \"outputFileName\": \"wwwroot/css/site.min.css\",\n    // An array of relative input file paths. Globbing patterns supported\n    \"inputFiles\": [\n      \"wwwroot/css/**/*.css\"\n    ]\n  },\n  {\n    \"outputFileName\": \"wwwroot/js/site.min.js\",\n    \"inputFiles\": [\n      \"wwwroot/js/site.js\"\n    ],\n    // Optionally specify minification options\n    \"minify\": {\n      \"enabled\": true,\n      \"renameLocals\": true\n    },\n    // Optinally generate .map file\n    \"sourceMap\": false\n  }\n]\n"
  },
  {
    "path": "src/Identity.API/libman.json",
    "content": "{\n  \"version\": \"1.0\",\n  \"defaultProvider\": \"cdnjs\",\n  \"libraries\": [\n    {\n      \"library\": \"jquery@3.6.3\",\n      \"destination\": \"wwwroot/lib/jquery/\"\n    },\n    {\n      \"library\": \"bootstrap@5.2.3\",\n      \"destination\": \"wwwroot/lib/bootstrap/\",\n      \"files\": [\n        \"css/bootstrap.css\",\n        \"css/bootstrap.css.map\",\n        \"css/bootstrap.min.css\",\n        \"css/bootstrap.min.css.map\",\n        \"js/bootstrap.bundle.js\",\n        \"js/bootstrap.bundle.min.js\"\n      ]\n    },\n    {\n      \"library\": \"jquery-validation-unobtrusive@4.0.0\",\n      \"destination\": \"wwwroot/lib/jquery-validation-unobtrusive/\"\n    },\n    {\n      \"library\": \"jquery-validate@1.19.5\",\n      \"destination\": \"wwwroot/lib/jquery-validate/\",\n      \"files\": [\n        \"jquery.validate.min.js\",\n        \"jquery.validate.js\"\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "src/Identity.API/tempkey.jwk",
    "content": "{\"AdditionalData\":{},\"Alg\":\"RS256\",\"Crv\":null,\"D\":\"Pf7_o1LMYwcXEWHkZ54kP4ItOYu0lhOOn4vc88XYg9XmDR8DcPxyvRLWGIuV-jSioMP59HIN5yHeDrQbG3tY2lTUynRYHdwOFVQAzQ3LpGSF4eKfMOSDU05HkWv-w2E1XWogVFAafv2F4FHeYtqWAzjjcjlYy_n0GkCcjBvIneuVqWNZQ9-eBt28cApruVfTeoE01Liz-ww4T0CQ9ujJQf9zUxhYb6WWMQ1U-A0qGK9hmh00Ymf-rxS2HuZ9o8Bps9YEvy7rCBfy6nSMB8T2I2SNCHDdlR8Oi-j_l0VQyidnfnRY0_Lio8uSKDTiUL_vBKyCREAfCEHM6TCpdixR-Q\",\"DP\":\"CR-Lyp15RyQV_TmxXq2EX8mntgdnT6ROePrkNlRCSXH-AAVJQ-24OlBH0qzehzMWI9Yk72bhqZdBIC1W8SGsTxf6T3xp-MwINJwHgYftX-D9RDQCYS5o6_gFW1775Of43Zv8tsjjwwkxUKHiwH8ImbEyqv2qVhdb_yMlgFwtXCc\",\"DQ\":\"ulVv9t13HB8K8jKLomSoINMaFtNGN0LS6wY93JKtwGCjcvV5lwXRiHhq0AbDOrJggg-zZVedlbXpWRokMI-hLTHc9TguYEfiJ11DvgldTB5MUfMxHau0L8ofS5YAuTSEgg4EPIrWgvVEF1ljHwLQtDSBB3CvI6UHF7rXXy9cwIk\",\"E\":\"AQAB\",\"K\":null,\"KeyId\":\"F7383A0ED7CD960EF473FD2851803938\",\"Kid\":\"F7383A0ED7CD960EF473FD2851803938\",\"Kty\":\"RSA\",\"N\":\"p3jTfCB0YsKpqJ6CNJi0tVmFBmoyI_D7QLLsbB-TCZ3-HIXDEr_k6zKb2GJ_QP7mncdSnYpJWSv7fWPfM0bL3A6NaMLF9MDjbfD5ti9irEW1dzBvIK0YjWmfks3eq6Mb2mM6PZtNEnoCqEzjgcRkDR1vtClEzUjs1E_i7TB-Y0J_aTYpLf-eN7yA1Obu8zMVRSSVBIwG5W5jljzA2nxk2u9qeDq8Sn0qgGwbX8cyYGQoVWBOPx7zap4cNcL6dHILjnlVrHqAUW9NVXtBWlVDP1Gvnm2zhCVJT_gW1twNhswyFULGVQH1ZWI0NukEqHG6LpN8Ti7Hx-K8MEEv_vQBYw\",\"Oth\":null,\"P\":\"1XZw-ufz_e5fWroi8naLluW5Ow1f-Ems8oG1WYUJkc1Q4vZWklLRqlEsDH48gqk0dEgXLE9tz0dDTo03tAPJmR1InFpusyLp2XPPJ4XLhWFCMDDb0xr4oayV1XDOi3U9eKERtcASo1IDo8zDEuY8NHFdJlNKpfJ7P0JSB0hEwoc\",\"Q\":\"yNg4tO9PVrwQ0CaouP0jniFouHAEfZMyJ0vVM1xPVim7jeKysOJi79lHO7OWNSABB3hY8dLcMSiKxS3BaU0Q2ns9Gw-NLR6eqT0BN9Nzh-a0sa0iuPUWCPgsWdrqjIf4FBX0Ra-6q7_LxuWCncehUAtoLFWMkcOrEj03Gwz1lUU\",\"QI\":\"UWdYA2qY622GL97_tPvrgk1VWSqoO2a0-TlGu0gK51T_Z1hqLwzG9QpTujoxnRU7Js_QUe_9cIEO72N0qliEm6A-MClM-5LUA9XleyAosb1cblLjAPqT9gfGbRRAbB3Aj3YwsgASGUiSpGVfaUuXn-OnuLoidZQXw7w9xcPuxMo\",\"Use\":null,\"X\":null,\"X5t\":null,\"X5tS256\":null,\"X5u\":null,\"Y\":null,\"KeySize\":2048,\"HasPrivateKey\":true,\"CryptoProviderFactory\":{\"CryptoProviderCache\":{},\"CustomCryptoProvider\":null,\"CacheSignatureProviders\":true,\"SignatureProviderObjectPoolCacheSize\":16}}"
  },
  {
    "path": "src/Identity.API/wwwroot/_references.js",
    "content": "﻿/// <autosync enabled=\"true\" />\n/// <reference path=\"js/site.js\" />\n/// <reference path=\"lib/bootstrap/dist/js/bootstrap.js\" />\n/// <reference path=\"lib/jquery/dist/jquery.js\" />\n/// <reference path=\"lib/jquery-validation/dist/jquery.validate.js\" />\n/// <reference path=\"lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js\" />\n"
  },
  {
    "path": "src/Identity.API/wwwroot/css/site.css",
    "content": "﻿/* plus-jakarta-sans-200 - latin */\n@font-face {\n    font-display: swap;\n    font-family: 'Plus Jakarta Sans';\n    font-style: normal;\n    font-weight: 200;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-200.woff2') format('woff2');\n}\n\n/* plus-jakarta-sans-200italic - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: italic;\n    font-weight: 200;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-200italic.woff2') format('woff2');\n}\n\n/* plus-jakarta-sans-300 - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: normal;\n    font-weight: 300;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-300.woff2') format('woff2');\n}\n\n/* plus-jakarta-sans-300italic - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: italic;\n    font-weight: 300;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-300italic.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-regular - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: normal;\n    font-weight: 400;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-regular.woff2') format('woff2'); \n}\n  \n/* plus-jakarta-sans-italic - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: italic;\n    font-weight: 400;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-italic.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-500 - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: normal;\n    font-weight: 500;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-500.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-500italic - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: italic;\n    font-weight: 500;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-500italic.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-600 - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: normal;\n    font-weight: 600;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-600.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-600italic - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: italic;\n    font-weight: 600;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-600italic.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-700 - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: normal;\n    font-weight: 700;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-700.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-700italic - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: italic;\n    font-weight: 700;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-700italic.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-800 - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: normal;\n    font-weight: 800;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-800.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-800italic - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: italic;\n    font-weight: 800;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-800italic.woff2') format('woff2'); \n}\n\nbody {\n    margin-top: 0;\n    font-family: 'Plus Jakarta Sans';\n    min-width:480px;\n}\n\n.content {\n    position: relative;\n}\n\n.welcome-page {\n    background: #fff;\n    max-width: 60rem;\n    margin: auto;\n    padding: 3rem;\n}\n\n.eshop-banner {\n    display: flex;\n    justify-content: center;\n}\n\n.eshop-banner img {\n    position: absolute;\n    width: 100%;\n    max-width: 1920px;\n}\n\n.eshop-header {\n    position: relative;\n    max-width: 120rem;\n    margin: auto;\n}\n\n.eshop-header.home .eshop-header-container {\n    height: 38rem;\n    margin-bottom: 0;\n}\n\n.eshop-header .eshop-header-container {\n    margin-bottom: 4rem;\n}\n\n.eshop-header-hero {\n    overflow: hidden;\n    position: absolute;\n    max-width: 100%;\n    left: 0;\n    top: 0;\n}\n\n.eshop-header-container {\n    position: relative;\n    margin: auto;\n    margin: 0 10rem;\n}\n\n.eshop-header-intro {\n    position: absolute;\n    max-width: 48rem;\n    bottom: 3rem;\n}\n\n.eshop-header-intro h1 {\n    color: #000;\n    font-size: 3.5rem;\n    font-style: normal;\n    font-weight: 700;\n    line-height: 100%;\n    margin: 0;\n}\n\n.eshop-header-intro p {\n    color: #000;\n    font-size: 2rem;\n    font-style: normal;\n    font-weight: 700;\n    line-height: 125%;\n    margin: 0;\n}\n\n.eshop-header .logo-header {\n    color: black;\n    margin-right: auto;\n}\n\n.eshop-header-navbar {\n    display: flex;\n    flex-direction: row;\n    justify-content: flex-end;\n    align-items: baseline;\n    margin-top: 1.25rem;\n    gap: 1.5rem;\n}\n\n.eshop-footer {\n    margin-top: 3.5rem;\n    background-color: #000;\n    width: 100%;\n}\n\n.eshop-footer-content {\n    max-width: 120rem;\n    margin: auto;\n}\n\n.eshop-footer-row {\n    padding: 3.5rem 10rem;\n    color: white;\n    display: flex;\n    justify-content: flex-end;\n    align-items: baseline;\n}\n\n.eshop-footer .logo-footer {\n    color: white;\n    margin-right: auto;\n}\n\n.login-container {\n    position: relative;\n    display: flex;\n    padding: 40px;\n    flex-direction: column;\n    justify-content: flex-end;\n    gap: 20px;\n    flex: 1 0 0;\n    align-self: stretch;\n    background: #FFF;\n    box-shadow: 0px 3.38082px 3.38082px 0px rgba(0, 0, 0, 0.01), 0px 27px 27px 0px rgba(0, 0, 0, 0.03);\n}\n\n.login-container h2 {\n    color: #000;\n    font-size: 36px;\n    font-style: normal;\n    font-weight: 700;\n    line-height: 48px; \n}\n\n.login-container label {\n    color: #444;\n    font-size: 14px;\n    font-style: normal;\n    font-weight: 400;\n    line-height: 20px;\n}\n\n.login-container input {\n    border: 1px solid #000;\n    background: #FFF;\n    max-width: 100%;\n    border-radius: 0;\n    color: #000;\n}\n\n.login-container input {\n    border: 1px solid #000;\n    background: #FFF;\n    max-width: 100%;\n    border-radius: 0;\n    color: #000;\n}\n\n.login-container .form-check {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n}\n\n.login-container .form-check-input[type=checkbox] {\n    border-radius: 0;\n    width: 20px;\n    height: 20px;\n}\n\n.login-container .form-check-input:checked[type=checkbox] {\n    background-image: url('../images/check.svg');\n    background-size: contain;\n}\n\n.login-container .btn.btn-primary {\n    border-radius: 0;\n    display: flex;\n    padding: 12px 24px;\n    justify-content: center;\n    align-items: flex-end;\n    gap: 8px;\n    align-self: stretch;\n    background: #000;\n}\n\n.icon {\n  position: relative;\n  top: -10px;\n}\n\n.page-consent .client-logo {\n  float: left;\n}\n.page-consent .client-logo img {\n  width: 80px;\n  height: 80px;\n}\n.page-consent .consent-buttons {\n  margin-top: 25px;\n}\n.page-consent .consent-form .consent-scopecheck {\n  display: inline-block;\n  margin-right: 5px;\n}\n.page-consent .consent-form .consent-description {\n  margin-left: 25px;\n}\n.page-consent .consent-form .consent-description label {\n  font-weight: normal;\n}\n.page-consent .consent-form .consent-remember {\n  padding-left: 16px;\n}\n\n.mt-15 {\n    margin-top:15px;\n}\n\n/* Wrapping element */\n/* Set some basic padding to keep content from hitting the edges */\n.body-content {\n    padding-left: 15px;\n    padding-right: 15px;\n}\n\n/* Set widths on the form inputs since otherwise they're 100% wide */\ninput,\nselect,\ntextarea {\n    max-width: 280px;\n}\n\n.select-filter {\n    background-color: transparent;\n    padding: 10px;\n    margin: 10px;\n    margin-right: 20px;\n    color: white;\n    padding-top: 20px;\n    padding-bottom: 3px;\n    min-width: 140px;\n    border-color: #37c7ca;\n    max-height: 43px;\n    -webkit-appearance: none;\n}\n\n    .select-filter option {\n        background-color: #00a69c;\n    }\n\nselect::-ms-expand {\n    display: none;\n}\n\n.select-filter-wrapper {\n    z-index: 0;\n    display:inline-block;\n    margin-left: -10px;\n}\n\n.select-filter-wrapper::before {\n    content: attr(data-name);\n    opacity: 0.5;\n    z-index: 1;\n    text-transform: uppercase;\n    position: absolute;\n    font-size: 10px;\n    margin-top: 15px;\n    margin-left: 21px;\n    color: white;\n}\n\n.select-filter-arrow {\n    position: absolute;\n    margin-left: 130px;\n    margin-top: 40px;\n}\n\n.btn-brand-small-filter {\n    margin-top: 10px;\n    position: absolute;\n    margin-left: 15px;\n}\n\n/* Carousel */\n.carousel-caption p {\n    font-size: 20px;\n    line-height: 1.4;\n}\n\n.layout-cart-image {\n    height: 36px;\n    margin-top: 5px;\n}\n\n.layout-cart-badge {\n    position: absolute;\n    margin-top: 2px;\n    margin-left: 14px;\n    background-color: #83d01b;\n    padding: 1px;\n    color: white;\n    border-radius: 50%;\n    width: 18px;\n    height: 18px;\n    font-size: 12px;\n    cursor: pointer;\n}\n\n/* buttons and links extension to use brackets: [ click me ] */\n.btn-bracketed:hover:before {\n    display: inline-block;\n    content: \"[\";\n    padding-right: 0.5em;\n    color: chartreuse;\n}\n\n.btn-bracketed:hover:after {\n    display: inline-block;\n    content: \"]\";\n    padding-left: 0.5em;\n    color: chartreuse;\n}\n\n.btn-brand {\n    background-color: #83D01B;\n    color: white;\n    padding: 10px 20px 10px 20px;\n    border-radius: 0px;\n    border: none;\n    width: 255px;\n    display: inline-block;\n    text-align: center;\n    text-transform: uppercase;\n    height: 45px;\n    font-size: 16px;\n    font-weight: normal;\n}\n\n.btn-brand::before {\n    content: '['\n}\n\n.btn-brand::after {\n    content: ']'\n}\n\n        .btn-brand:hover:before {\n            padding-right: 5px;\n        }\n\n        .btn-brand:hover:after {\n            padding-left: 5px;\n        }\n\n.btn-brand-big {\n    width: 360px;\n    margin-top: 20px;\n}\n\n.btn-brand-small {\n    width: 45px;\n}\n\n    .btn-brand-small::before {\n        content: '';\n    }\n\n    .btn-brand-small::after {\n        content: '';\n    }\n\n    .btn-brand-small:hover:before {\n        content: '';\n        padding: 0;\n    }\n\n    .btn-brand-small:hover:after {\n        content: '';\n        padding: 0;\n    }\n\n.btn-brand-dark {\n    background-color: #00a69c;\n}\n\n.btn-brand:hover {\n    color: white;\n    background-color: #83D01B;\n    text-decoration:none;\n}\n\n.btn-brand-dark:hover {\n    background-color: #00a69c;\n}\n\n.btn-cart {\n    float: right;\n    margin-top: 40px;\n    margin-bottom: 40px;\n}\n\n.btn-catalog-apply {\n    padding:0;\n}\n\n.form-label {\n    text-transform: uppercase;\n    font-weight: normal!important;\n    text-align: left;\n    margin-bottom: 10px !important;\n    color: #404040;\n}\n\n.form-input {\n    border-radius: 0;\n    padding: 10px;\n    height: 45px;\n}\n\n.form-input-small {\n    max-width: 100px!important;\n}\n\n.form-select {\n    border-radius: 0;\n    padding: 10px;\n    height: 45px;\n    width: 150px;\n}\n\n/* Make .svg files in the carousel display properly in older browsers */\n.carousel-inner .item img[src$=\".svg\"] {\n    width: 100%;\n}\n\n.navbar-inverse {\n    background-color: #FFF;\n    border-color: #FFF;\n}\n\n    /*.navbar-inverse li {\n        margin-top: 10px;\n    }*/\n\n.btn-login {\n    border: 1px solid #00A69C;\n    height: 36px!important;\n    margin-right: 10px;\n    margin-top: 10px;\n    background-color: white;\n    color: #00a69c;\n    text-transform:uppercase;\n    max-width: 140px;\n    width: 140px;\n    padding-top:8px!important;\n}\n\n.btn-login {\n    font-weight:normal!important;\n}\n\n    .btn-login::before {\n        content: '[';\n    }\n\n    .btn-login::after {\n        content: ']';\n    }\n\n    .btn-login:hover:before {\n        content: '[ ';\n    }\n\n    .btn-login:hover:after {\n        content: ' ]';\n    }\n\n.navbar-inverse li a {\n    height: 30px;\n    padding: 5px 20px;\n    color: #00A69C !important;\n}\n\n.navbar-brand {\n    margin-top: 20px;\n    background-image: url(../images/brand.PNG);\n    width: 201px;\n    height: 44px;\n    margin-left: 0px !important;\n}\n\n.nav > li > a {\n    color: white;\n}\n\n    .nav > li > a:hover, .nav > li > a:focus {\n        background-color: #00A69C;\n        font-weight: bolder;\n    }\n\n.container-fluid {\n    padding-left: 0px;\n    padding-right: 0px;\n}\n\n.home-banner {\n    width: 100%;\n    margin-right: 0px;\n    margin-left: 0px;\n    background-image: url(../images/main_banner.png);\n    background-size: cover;\n    height: 258px;\n    background-position: center;\n}\n\n.home-banner-text {\n    margin-top: 70px;\n}\n\n.home-catalog-container {\n    min-height: 400px;\n    margin-bottom: 20px;\n}\n\n.home-catalog-filter-container {\n    background-color: #00A69C;\n    height:63px;\n    line-height: 76px;\n}\n\n    .home-catalog-filter-container li a {\n        padding-top: 5px !important;\n    }\n\n.home-catalog-filter-brands::before {\n    content: 'BRAND';\n    color: white;\n    font-size: x-small;\n    opacity: 0.5;\n    margin: 10px 0px 0px 15px;\n}\n\n.home-catalog-filter-types::before {\n    content: 'TYPES';\n    color: white;\n    font-size: x-small;\n    opacity: 0.5;\n    margin: 10px 0px 0px 15px;\n}\n\n.home-catalog-item {\n    margin-top: 10px;\n    margin-bottom: 10px;\n}\n\n.home-catalog-item-image {\n    width: 100%;\n    object-fit: cover;\n    /* max-width: 320px; */\n    text-align: center;\n}\n\n.home-catalog-item-image-addCart {\n    background-color: #83D01B;\n    color: white;\n    display: inline-block;\n    height: 43px;\n    padding: 10px 20px 10px 20px;\n    font-weight: bold;\n    text-align: center;\n    margin-top: 10px;\n    margin-left: 60px;\n    margin-right: 60px;\n    font-size: 16px;\n    font-weight: normal;\n}\n\n    .home-catalog-item-image-addCart:hover {\n        color: white;\n        text-decoration: none;\n    }\n\n\n.home-catalog-item-image:hover:after {\n    cursor: pointer;\n}\n\n.home-catalog-item-title {\n    text-align: center;\n    text-transform: uppercase;\n    font-weight: 300;\n    font-size: 16px;\n    margin-top: 20px;\n}\n\n.home-catalog-item-price {\n    text-align: center;\n    font-weight: 900;\n    font-size: 28px;\n}\n\n    .home-catalog-item-price::before {\n        content: '$';\n    }\n\n.home-catalog-noResults {\n    text-align:center;\n    margin-top: 100px;\n}\n\n.container .nav .navbar-nav .col-sm-6 ::before {\n    content: 'BRAND';\n}\n\n.validation-summary-errors  li {\n    list-style: none;\n}\n\n.text {\n    color: #83D01B;\n}\n\n.text:hover {\n    color: #83D01B;\n}\n\nform .col-md-4 {\n    text-align: right;\n}\n\n.account-login-container {\n    min-height: 70vh;\n    text-align: center;\n    padding-top: 40px;\n}\n\n.account-register-container {\n    min-height: 70vh;\n    text-align: center !important;\n    align-content: center;\n}\n\n.cart-index-container {\n    min-height: 70vh;\n    padding-top: 40px;\n    margin-bottom: 30px;\n    min-width: 992px;\n}\n\n.register-container {\n    min-height: 70vh;\n    padding-top: 40px;\n    margin-bottom: 30px;\n    padding-left: 30px;\n}\n\n.order-create-container {\n    min-height: 70vh;\n    padding-top: 40px;\n    margin-bottom: 30px;\n    padding-left: 30px;\n    min-width: 995px;\n}\n\n.cart-product-column {\n    max-width: 120px;\n    text-transform: uppercase;\n    vertical-align: middle!important;\n}\n\n.order-create-container .cart-product-column {\n    max-width: 130px;\n}\n\n.cart-product-column-name {\n    width: 220px;\n}\n\n.cart-subtotal-label {\n    font-size: 12px;\n    color: #404040;\n    margin-top:10px;\n}\n\n.cart-subtotal-value {\n    font-size: 20px;\n    color: #00a69c;\n}\n\n.cart-total-label {\n    font-size: 14px;\n    color: #404040;\n    margin-top:10px;\n}\n\n.cart-total-value {\n    font-size: 28px;\n    color: #00a69c;\n    text-align: left;\n}\n\n.cart-product-image {\n    max-width: 210px;\n}\n\n.cart-section-total {\n    margin-bottom: 5px;\n    margin-left: 175px;\n    text-align: left;\n}\n\n.cart-product-column input {\n    width: 70px;\n    text-align: center;\n}\n\n.cart-refresh-button {\n   margin-top:0;\n   background-image: url('../images/refresh.svg');\n   color: white;\n   font-size: 8px;\n   width: 40px;\n   height: 40px;\n   background-color:transparent;\n   border:none;\n   margin-top: 25px;\n   margin-left:15px;\n}\n\n    .cart-refresh-button:hover {\n        background-color:transparent;\n    }\n\n.cart-totals {\n    border-bottom:none!important;\n}\n\n.input-validation-error {\n    border: 1px solid #fb0d0d;\n}\n\n.text-danger {\n    color: #fb0d0d;\n    font-size: 12px;\n}\n\n.cart {\n    border: none !important;\n}\n\n.form-horizontal h4 {\n    margin-top: 30px;\n}\n\n.form-horizontal .form-group {\n    margin-right: 0px!important;\n}\n\n.form-input-center {\n    margin: auto;\n}\n\n.order-index-container {\n    min-height: 70vh;\n    padding-top: 40px;\n    margin-bottom: 30px;\n}\n\n    .order-index-container .table tbody tr {\n        border-bottom:none;\n    }\n\n    .order-index-container .table tbody tr td {\n        border-top:none;\n        padding-top:10px;\n        padding-bottom:10px;\n    }\n\n\n\n    .order-index-container .table tbody tr:nth-child(even) {\n        background-color: #f5f5f5;\n    }\n\n.order-create-section-title {\n    margin-left: -15px;\n    text-transform: uppercase;\n}\n\n.order-create-section-items {\n    margin-left: -45px;\n    width: 102%;\n}\n\n.order-detail-button {\n    \n}\n\n    .order-detail-button a {\n        color: #83d01b;\n    }\n\n.order-detail-container {\n    min-height: 70vh;\n    padding-top: 40px;\n    margin-bottom: 30px;\n}\n\n    .order-detail-container .table tbody tr:first-child td{\n        border-top:none;\n    }\n\n    .order-detail-container .table tr{\n        border-bottom:none;\n    }\n\n.order-detail-section {\n    margin-top: 50px;\n}\n\n.order-detail-container .table {\n    margin-left: -7px;\n}\n\n.order-section-total {\n    margin-bottom: 5px;\n    margin-left: 40px;\n    text-align: left;\n}\n\n.fr {\n    float:right!important;\n}\n\n.down-arrow {\n    background-image: url('../images/arrow-down.png');\n    height: 7px;\n    width: 10px;\n    display: inline-block;\n    margin-left: 20px;\n}\n\n.logout-icon {\n    background-image: url('../images/logout.PNG');\n    display: inline-block;\n    height:19px;\n    width:19px;\n    margin-left: 15px;\n}\n\n.myorders-icon {\n    background-image: url('../images/my_orders.PNG');\n    display: inline-block;\n    height: 20px;\n    width: 20px;\n    margin-left: 15px;\n}\n\n.login-user {\n    position: absolute!important;\n    top: 30px;\n    right: 65px;\n    cursor:pointer;\n}\n\n.login-user-dropdown {\n    position: relative;\n    display: inline-block;\n}\n\n.login-user-dropdown-content {\n    display: none;\n    position: absolute;\n    background-color: #FFFFFF;\n    min-width: 160px;\n    box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);\n    /*left: 100px;*/\n    right: 0px;\n}\n\n.login-user-dropdown-content a {\n    color: black;\n    padding: 12px 16px;\n    text-decoration: none;\n    display: block;\n    text-align:right;\n    text-transform:uppercase;\n}\n\n.login-user:hover .login-user-dropdown-content {\n    display: block;\n}\n\n.down-arrow:hover > .login-user-dropdown-content {\n    display: block;\n}\n\n.login-user-dropdown-content a:hover {\n    color: #83d01b;\n}\n\n.es-header {\n    min-height: 80px!important;\n}\n\n.es-pager-bottom {\n    margin-top: 40px;\n}\n\n.es-pager-top {\n    margin-bottom: 20px;\n    margin-top: 20px;\n}\n\n.es-pager-top ul {\n    list-style: none;\n}\n\n.es-pager-bottom ul {\n    list-style: none;\n}\n\n.page-item {\n    cursor: pointer;\n}\n\n.next {\n    position: absolute;\n    right: 15px;\n    top: 0;\n}\n\n.previous {\n    position: absolute;\n    left: 0;\n    top: 0;\n}\n\n.is-disabled{\n    cursor: not-allowed;\n    opacity: .5;\n    pointer-events: none;\n}\n\n.table tr {\n    border-bottom:1px solid #ddd;\n}\n\n.table th {\n    text-transform: uppercase;\n}\n\n\n.navbar-nav {\n    margin-top: 10px;\n    margin-bottom: 7.5px;\n    margin-right: -10px;\n    float: right;\n}\n\n@media screen and (max-width: 1195px) {\n    .cart-product-column-name {\n        display:none;\n    }\n}\n\n/* Hide/rearrange for smaller screens */\n@media screen and (max-width: 767px) {\n    /* Hide captions */\n    .carousel-caption {\n        display: none;\n    }\n\n    footer .text {\n        text-align: left;\n        margin-top: -15px;\n    }\n\n    .cart-product-column-brand {\n        display:none;\n    }\n}\n\n@media screen and (min-width: 992px) {\n    .form-input {\n        width: 360px;\n        max-width: 360px;\n    }\n}\n\n@media screen and (max-width: 415px) {\n    .account-login-container {\n        margin-left: -50px;\n    }\n\n    .page-consent {\n        margin-left: 10px;\n        margin-right: 80px;\n        padding-right: 0px;\n        padding-left: 0px;\n    }\n}"
  },
  {
    "path": "src/Identity.API/wwwroot/js/signin-redirect.js",
    "content": "window.location.href = document.querySelector(\"meta[http-equiv=refresh]\").getAttribute(\"data-url\");\n"
  },
  {
    "path": "src/Identity.API/wwwroot/js/signout-redirect.js",
    "content": "window.addEventListener(\"load\", function () {\n    var a = document.querySelector(\"a.PostLogoutRedirectUri\");\n    if (a) {\n        window.location = a.href;\n    }\n});\n"
  },
  {
    "path": "src/Identity.API/wwwroot/js/site.js",
    "content": "﻿// Write your Javascript code.\n"
  },
  {
    "path": "src/IntegrationEventLogEF/EventStateEnum.cs",
    "content": "﻿namespace eShop.IntegrationEventLogEF;\n\npublic enum EventStateEnum\n{\n    NotPublished = 0,\n    InProgress = 1,\n    Published = 2,\n    PublishedFailed = 3\n}\n\n"
  },
  {
    "path": "src/IntegrationEventLogEF/GlobalUsings.cs",
    "content": "﻿global using System.ComponentModel.DataAnnotations.Schema;\nglobal using System.Data.Common;\nglobal using System.Reflection;\nglobal using System.Text.Json;\nglobal using Microsoft.EntityFrameworkCore;\nglobal using Microsoft.EntityFrameworkCore.Metadata.Builders;\nglobal using Microsoft.EntityFrameworkCore.Storage;\nglobal using eShop.EventBus.Events;\n"
  },
  {
    "path": "src/IntegrationEventLogEF/IntegrationEventLogEF.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <RootNamespace>eShop.IntegrationEventLogEF</RootNamespace>\n    <IsPublishable>false</IsPublishable>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Npgsql.EntityFrameworkCore.PostgreSQL\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\EventBus\\EventBus.csproj\" />\n  </ItemGroup>\n\n</Project>"
  },
  {
    "path": "src/IntegrationEventLogEF/IntegrationEventLogEntry.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace eShop.IntegrationEventLogEF;\n\npublic class IntegrationEventLogEntry\n{\n    private static readonly JsonSerializerOptions s_indentedOptions = new() { WriteIndented = true };\n    private static readonly JsonSerializerOptions s_caseInsensitiveOptions = new() { PropertyNameCaseInsensitive = true };\n\n    private IntegrationEventLogEntry() { }\n    public IntegrationEventLogEntry(IntegrationEvent @event, Guid transactionId)\n    {\n        EventId = @event.Id;\n        CreationTime = @event.CreationDate;\n        EventTypeName = @event.GetType().FullName;\n        Content = JsonSerializer.Serialize(@event, @event.GetType(), s_indentedOptions);\n        State = EventStateEnum.NotPublished;\n        TimesSent = 0;\n        TransactionId = transactionId;\n    }\n    public Guid EventId { get; private set; }\n    [Required]\n    public string EventTypeName { get; private set; }\n    [NotMapped]\n    public string EventTypeShortName => EventTypeName.Split('.')?.Last();\n    [NotMapped]\n    public IntegrationEvent IntegrationEvent { get; private set; }\n    public EventStateEnum State { get; set; }\n    public int TimesSent { get; set; }\n    public DateTime CreationTime { get; private set; }\n    [Required]\n    public string Content { get; private set; }\n    public Guid TransactionId { get; private set; }\n\n    public IntegrationEventLogEntry DeserializeJsonContent(Type type)\n    {\n        IntegrationEvent = JsonSerializer.Deserialize(Content, type, s_caseInsensitiveOptions) as IntegrationEvent;\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/IntegrationEventLogEF/IntegrationLogExtensions.cs",
    "content": "﻿namespace eShop.IntegrationEventLogEF;\n\npublic static class IntegrationLogExtensions\n{\n    public static void UseIntegrationEventLogs(this ModelBuilder builder)\n    {\n        builder.Entity<IntegrationEventLogEntry>(builder =>\n        {\n            builder.ToTable(\"IntegrationEventLog\");\n\n            builder.HasKey(e => e.EventId);\n        });\n    }\n}\n"
  },
  {
    "path": "src/IntegrationEventLogEF/Services/IIntegrationEventLogService.cs",
    "content": "﻿namespace eShop.IntegrationEventLogEF.Services;\n\npublic interface IIntegrationEventLogService\n{\n    Task<IEnumerable<IntegrationEventLogEntry>> RetrieveEventLogsPendingToPublishAsync(Guid transactionId);\n    Task SaveEventAsync(IntegrationEvent @event, IDbContextTransaction transaction);\n    Task MarkEventAsPublishedAsync(Guid eventId);\n    Task MarkEventAsInProgressAsync(Guid eventId);\n    Task MarkEventAsFailedAsync(Guid eventId);\n}\n"
  },
  {
    "path": "src/IntegrationEventLogEF/Services/IntegrationEventLogService.cs",
    "content": "﻿namespace eShop.IntegrationEventLogEF.Services;\n\npublic class IntegrationEventLogService<TContext> : IIntegrationEventLogService, IDisposable\n    where TContext : DbContext\n{\n    private volatile bool _disposedValue;\n    private readonly TContext _context;\n    private readonly Type[] _eventTypes;\n\n    public IntegrationEventLogService(TContext context)\n    {\n        _context = context;\n        _eventTypes = Assembly.Load(Assembly.GetEntryAssembly().FullName)\n            .GetTypes()\n            .Where(t => t.Name.EndsWith(nameof(IntegrationEvent)))\n            .ToArray();\n    }\n\n    public async Task<IEnumerable<IntegrationEventLogEntry>> RetrieveEventLogsPendingToPublishAsync(Guid transactionId)\n    {\n        var result = await _context.Set<IntegrationEventLogEntry>()\n            .Where(e => e.TransactionId == transactionId && e.State == EventStateEnum.NotPublished)\n            .ToListAsync();\n\n        if (result.Count != 0)\n        {\n            return result.OrderBy(o => o.CreationTime)\n                .Select(e => e.DeserializeJsonContent(_eventTypes.FirstOrDefault(t => t.Name == e.EventTypeShortName)));\n        }\n\n        return [];\n    }\n\n    public Task SaveEventAsync(IntegrationEvent @event, IDbContextTransaction transaction)\n    {\n        if (transaction == null) throw new ArgumentNullException(nameof(transaction));\n\n        var eventLogEntry = new IntegrationEventLogEntry(@event, transaction.TransactionId);\n\n        _context.Database.UseTransaction(transaction.GetDbTransaction());\n        _context.Set<IntegrationEventLogEntry>().Add(eventLogEntry);\n\n        return _context.SaveChangesAsync();\n    }\n\n    public Task MarkEventAsPublishedAsync(Guid eventId)\n    {\n        return UpdateEventStatus(eventId, EventStateEnum.Published);\n    }\n\n    public Task MarkEventAsInProgressAsync(Guid eventId)\n    {\n        return UpdateEventStatus(eventId, EventStateEnum.InProgress);\n    }\n\n    public Task MarkEventAsFailedAsync(Guid eventId)\n    {\n        return UpdateEventStatus(eventId, EventStateEnum.PublishedFailed);\n    }\n\n    private Task UpdateEventStatus(Guid eventId, EventStateEnum status)\n    {\n        var eventLogEntry = _context.Set<IntegrationEventLogEntry>().Single(ie => ie.EventId == eventId);\n        eventLogEntry.State = status;\n\n        if (status == EventStateEnum.InProgress)\n            eventLogEntry.TimesSent++;\n\n        return _context.SaveChangesAsync();\n    }\n\n    protected virtual void Dispose(bool disposing)\n    {\n        if (!_disposedValue)\n        {\n            if (disposing)\n            {\n                _context.Dispose();\n            }\n\n\n            _disposedValue = true;\n        }\n    }\n\n    public void Dispose()\n    {\n        Dispose(disposing: true);\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "src/IntegrationEventLogEF/Utilities/ResilientTransaction.cs",
    "content": "﻿namespace eShop.IntegrationEventLogEF.Utilities;\n\npublic class ResilientTransaction\n{\n    private readonly DbContext _context;\n    private ResilientTransaction(DbContext context) =>\n        _context = context ?? throw new ArgumentNullException(nameof(context));\n\n    public static ResilientTransaction New(DbContext context) => new(context);\n\n    public async Task ExecuteAsync(Func<Task> action)\n    {\n        //Use of an EF Core resiliency strategy when using multiple DbContexts within an explicit BeginTransaction():\n        //See: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency\n        var strategy = _context.Database.CreateExecutionStrategy();\n        await strategy.ExecuteAsync(async () =>\n        {\n            await using var transaction = await _context.Database.BeginTransactionAsync();\n            await action();\n            await transaction.CommitAsync();\n        });\n    }\n}\n"
  },
  {
    "path": "src/OrderProcessor/BackgroundTaskOptions.cs",
    "content": "﻿namespace eShop.OrderProcessor;\n\npublic class BackgroundTaskOptions\n{\n    public int GracePeriodTime { get; set; }\n\n    public int CheckUpdateTime { get; set; }\n}\n"
  },
  {
    "path": "src/OrderProcessor/Events/GracePeriodConfirmedIntegrationEvent.cs",
    "content": "﻿namespace eShop.OrderProcessor.Events\n{\n    using eShop.EventBus.Events;\n\n    public record GracePeriodConfirmedIntegrationEvent : IntegrationEvent\n    {\n        public int OrderId { get; }\n\n        public GracePeriodConfirmedIntegrationEvent(int orderId) =>\n            OrderId = orderId;\n    }\n}\n"
  },
  {
    "path": "src/OrderProcessor/Extensions/Extensions.cs",
    "content": "﻿using System.Text.Json.Serialization;\nusing eShop.OrderProcessor.Events;\n\nnamespace eShop.OrderProcessor.Extensions;\n\npublic static class Extensions\n{\n    public static void AddApplicationServices(this IHostApplicationBuilder builder)\n    {\n        builder.AddRabbitMqEventBus(\"eventbus\")\n               .ConfigureJsonOptions(options => options.TypeInfoResolverChain.Add(IntegrationEventContext.Default));\n\n        builder.AddNpgsqlDataSource(\"orderingdb\");\n\n        builder.Services.AddOptions<BackgroundTaskOptions>()\n            .BindConfiguration(nameof(BackgroundTaskOptions));\n\n        builder.Services.AddHostedService<GracePeriodManagerService>();\n    }\n}\n\n[JsonSerializable(typeof(GracePeriodConfirmedIntegrationEvent))]\npartial class IntegrationEventContext : JsonSerializerContext\n{\n\n}\n"
  },
  {
    "path": "src/OrderProcessor/GlobalUsings.cs",
    "content": "﻿global using Microsoft.AspNetCore.Builder;\nglobal using Microsoft.Extensions.DependencyInjection;\nglobal using Microsoft.Extensions.Hosting;\nglobal using Microsoft.Extensions.Logging;\nglobal using eShop.OrderProcessor.Extensions;\nglobal using eShop.OrderProcessor.Services;\nglobal using eShop.ServiceDefaults;\n"
  },
  {
    "path": "src/OrderProcessor/OrderProcessor.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Worker\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>\n    <PublishAot Condition=\"'$(EnableAotPublishing)' == 'true'\">true</PublishAot>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Aspire.Npgsql\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\eShop.ServiceDefaults\\eShop.ServiceDefaults.csproj\" />\n    <ProjectReference Include=\"..\\EventBusRabbitMQ\\EventBusRabbitMQ.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/OrderProcessor/Program.cs",
    "content": "﻿var builder = WebApplication.CreateBuilder(args);\n\nbuilder.AddBasicServiceDefaults();\nbuilder.AddApplicationServices();\n\nvar app = builder.Build();\n\napp.MapDefaultEndpoints();\n\nawait app.RunAsync();\n"
  },
  {
    "path": "src/OrderProcessor/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"OrderProcessor\": {\n      \"commandName\": \"Project\",\n      \"environmentVariables\": {\n        \"DOTNET_ENVIRONMENT\": \"Development\"\n      },\n      \"applicationUrl\": \"http://localhost:16888\"\n    }\n  }\n}"
  },
  {
    "path": "src/OrderProcessor/Services/GracePeriodManagerService.cs",
    "content": "﻿using eShop.EventBus.Abstractions;\nusing Microsoft.Extensions.Options;\nusing Npgsql;\nusing eShop.OrderProcessor.Events;\n\nnamespace eShop.OrderProcessor.Services\n{\n    public class GracePeriodManagerService(\n        IOptions<BackgroundTaskOptions> options,\n        IEventBus eventBus,\n        ILogger<GracePeriodManagerService> logger,\n        NpgsqlDataSource dataSource) : BackgroundService\n    {\n        private readonly BackgroundTaskOptions _options = options?.Value ?? throw new ArgumentNullException(nameof(options));\n\n        protected override async Task ExecuteAsync(CancellationToken stoppingToken)\n        {\n            var delayTime = TimeSpan.FromSeconds(_options.CheckUpdateTime);\n\n            if (logger.IsEnabled(LogLevel.Debug))\n            {\n                logger.LogDebug(\"GracePeriodManagerService is starting.\");\n                stoppingToken.Register(() => logger.LogDebug(\"GracePeriodManagerService background task is stopping.\"));\n            }\n\n            while (!stoppingToken.IsCancellationRequested)\n            {\n                if (logger.IsEnabled(LogLevel.Debug))\n                {\n                    logger.LogDebug(\"GracePeriodManagerService background task is doing background work.\");\n                }\n\n                await CheckConfirmedGracePeriodOrders();\n\n                await Task.Delay(delayTime, stoppingToken);\n            }\n\n            if (logger.IsEnabled(LogLevel.Debug))\n            {\n                logger.LogDebug(\"GracePeriodManagerService background task is stopping.\");\n            }\n        }\n\n        private async Task CheckConfirmedGracePeriodOrders()\n        {\n            if (logger.IsEnabled(LogLevel.Debug))\n            {\n                logger.LogDebug(\"Checking confirmed grace period orders\");\n            }\n\n            var orderIds = await GetConfirmedGracePeriodOrders();\n\n            foreach (var orderId in orderIds)\n            {\n                var confirmGracePeriodEvent = new GracePeriodConfirmedIntegrationEvent(orderId);\n\n                logger.LogInformation(\"Publishing integration event: {IntegrationEventId} - ({@IntegrationEvent})\", confirmGracePeriodEvent.Id, confirmGracePeriodEvent);\n\n                await eventBus.PublishAsync(confirmGracePeriodEvent);\n            }\n        }\n\n        private async ValueTask<List<int>> GetConfirmedGracePeriodOrders()\n        {\n            try\n            {\n                using var conn = dataSource.CreateConnection();\n                using var command = conn.CreateCommand();\n                command.CommandText = \"\"\"\n                    SELECT \"Id\"\n                    FROM ordering.orders\n                    WHERE CURRENT_TIMESTAMP - \"OrderDate\" >= @GracePeriodTime AND \"OrderStatus\" = 'Submitted'\n                    \"\"\";\n                command.Parameters.AddWithValue(\"GracePeriodTime\", TimeSpan.FromMinutes(_options.GracePeriodTime));\n\n                List<int> ids = [];\n\n                await conn.OpenAsync();\n                using var reader = await command.ExecuteReaderAsync();\n                while (await reader.ReadAsync())\n                {\n                    ids.Add(reader.GetInt32(0));\n                }\n\n                return ids;\n            }\n            catch (NpgsqlException exception)\n            {\n                logger.LogError(exception, \"Fatal error establishing database connection\");\n            }\n\n            return [];\n        }\n    }\n}\n"
  },
  {
    "path": "src/OrderProcessor/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"System\": \"Information\",\n      \"Microsoft\": \"Information\"\n    }\n  },\n  \"ConnectionStrings\": {\n    \"postgres\": \"Host=localhost;Database=OrderingDB;Username=postgres;Password=yourWeak(!)Password\"\n  }\n}\n"
  },
  {
    "path": "src/OrderProcessor/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  },\n  \"ConnectionStrings\": {\n    \"EventBus\": \"amqp://localhost\"\n  },\n  \"EventBus\": {\n    \"SubscriptionClientName\": \"OrderProcessor\"\n  },\n  \"BackgroundTaskOptions\": {\n    \"GracePeriodTime\": \"1\",\n    \"CheckUpdateTime\": \"30\"\n  }\n}"
  },
  {
    "path": "src/Ordering.API/Apis/OrderServices.cs",
    "content": "﻿public class OrderServices(\n    IMediator mediator,\n    IOrderQueries queries,\n    IIdentityService identityService,\n    ILogger<OrderServices> logger)\n{\n    public IMediator Mediator { get; set; } = mediator;\n    public ILogger<OrderServices> Logger { get; } = logger;\n    public IOrderQueries Queries { get; } = queries;\n    public IIdentityService IdentityService { get; } = identityService;\n}\n"
  },
  {
    "path": "src/Ordering.API/Apis/OrdersApi.cs",
    "content": "﻿using Microsoft.AspNetCore.Http.HttpResults;\nusing CardType = eShop.Ordering.API.Application.Queries.CardType;\nusing Order = eShop.Ordering.API.Application.Queries.Order;\n\npublic static class OrdersApi\n{\n    public static RouteGroupBuilder MapOrdersApiV1(this IEndpointRouteBuilder app)\n    {\n        var api = app.MapGroup(\"api/orders\").HasApiVersion(1.0);\n\n        api.MapPut(\"/cancel\", CancelOrderAsync);\n        api.MapPut(\"/ship\", ShipOrderAsync);\n        api.MapGet(\"{orderId:int}\", GetOrderAsync);\n        api.MapGet(\"/\", GetOrdersByUserAsync);\n        api.MapGet(\"/cardtypes\", GetCardTypesAsync);\n        api.MapPost(\"/draft\", CreateOrderDraftAsync);\n        api.MapPost(\"/\", CreateOrderAsync);\n\n        return api;\n    }\n\n    public static async Task<Results<Ok, BadRequest<string>, ProblemHttpResult>> CancelOrderAsync(\n        [FromHeader(Name = \"x-requestid\")] Guid requestId,\n        CancelOrderCommand command,\n        [AsParameters] OrderServices services)\n    {\n        if (requestId == Guid.Empty)\n        {\n            return TypedResults.BadRequest(\"Empty GUID is not valid for request ID\");\n        }\n\n        var requestCancelOrder = new IdentifiedCommand<CancelOrderCommand, bool>(command, requestId);\n\n        services.Logger.LogInformation(\n            \"Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})\",\n            requestCancelOrder.GetGenericTypeName(),\n            nameof(requestCancelOrder.Command.OrderNumber),\n            requestCancelOrder.Command.OrderNumber,\n            requestCancelOrder);\n\n        var commandResult = await services.Mediator.Send(requestCancelOrder);\n\n        if (!commandResult)\n        {\n            return TypedResults.Problem(detail: \"Cancel order failed to process.\", statusCode: 500);\n        }\n\n        return TypedResults.Ok();\n    }\n\n    public static async Task<Results<Ok, BadRequest<string>, ProblemHttpResult>> ShipOrderAsync(\n        [FromHeader(Name = \"x-requestid\")] Guid requestId,\n        ShipOrderCommand command,\n        [AsParameters] OrderServices services)\n    {\n        if (requestId == Guid.Empty)\n        {\n            return TypedResults.BadRequest(\"Empty GUID is not valid for request ID\");\n        }\n\n        var requestShipOrder = new IdentifiedCommand<ShipOrderCommand, bool>(command, requestId);\n\n        services.Logger.LogInformation(\n            \"Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})\",\n            requestShipOrder.GetGenericTypeName(),\n            nameof(requestShipOrder.Command.OrderNumber),\n            requestShipOrder.Command.OrderNumber,\n            requestShipOrder);\n\n        var commandResult = await services.Mediator.Send(requestShipOrder);\n\n        if (!commandResult)\n        {\n            return TypedResults.Problem(detail: \"Ship order failed to process.\", statusCode: 500);\n        }\n\n        return TypedResults.Ok();\n    }\n\n    public static async Task<Results<Ok<Order>, NotFound>> GetOrderAsync(int orderId, [AsParameters] OrderServices services)\n    {\n        try\n        {\n            var order = await services.Queries.GetOrderAsync(orderId);\n            return TypedResults.Ok(order);\n        }\n        catch\n        {\n            return TypedResults.NotFound();\n        }\n    }\n\n    public static async Task<Ok<IEnumerable<OrderSummary>>> GetOrdersByUserAsync([AsParameters] OrderServices services)\n    {\n        var userId = services.IdentityService.GetUserIdentity();\n        var orders = await services.Queries.GetOrdersFromUserAsync(userId);\n        return TypedResults.Ok(orders);\n    }\n\n    public static async Task<Ok<IEnumerable<CardType>>> GetCardTypesAsync(IOrderQueries orderQueries)\n    {\n        var cardTypes = await orderQueries.GetCardTypesAsync();\n        return TypedResults.Ok(cardTypes);\n    }\n\n    public static async Task<OrderDraftDTO> CreateOrderDraftAsync(CreateOrderDraftCommand command, [AsParameters] OrderServices services)\n    {\n        services.Logger.LogInformation(\n            \"Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})\",\n            command.GetGenericTypeName(),\n            nameof(command.BuyerId),\n            command.BuyerId,\n            command);\n\n        return await services.Mediator.Send(command);\n    }\n\n    public static async Task<Results<Ok, BadRequest<string>>> CreateOrderAsync(\n        [FromHeader(Name = \"x-requestid\")] Guid requestId,\n        CreateOrderRequest request,\n        [AsParameters] OrderServices services)\n    {\n        \n        //mask the credit card number\n        \n        services.Logger.LogInformation(\n            \"Sending command: {CommandName} - {IdProperty}: {CommandId}\",\n            request.GetGenericTypeName(),\n            nameof(request.UserId),\n            request.UserId); //don't log the request as it has CC number\n\n        if (requestId == Guid.Empty)\n        {\n            services.Logger.LogWarning(\"Invalid IntegrationEvent - RequestId is missing - {@IntegrationEvent}\", request);\n            return TypedResults.BadRequest(\"RequestId is missing.\");\n        }\n\n        using (services.Logger.BeginScope(new List<KeyValuePair<string, object>> { new(\"IdentifiedCommandId\", requestId) }))\n        {\n            var maskedCCNumber = request.CardNumber.Substring(request.CardNumber.Length - 4).PadLeft(request.CardNumber.Length, 'X');\n            var createOrderCommand = new CreateOrderCommand(request.Items, request.UserId, request.UserName, request.City, request.Street,\n                request.State, request.Country, request.ZipCode,\n                maskedCCNumber, request.CardHolderName, request.CardExpiration,\n                request.CardSecurityNumber, request.CardTypeId);\n\n            var requestCreateOrder = new IdentifiedCommand<CreateOrderCommand, bool>(createOrderCommand, requestId);\n\n            services.Logger.LogInformation(\n                \"Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})\",\n                requestCreateOrder.GetGenericTypeName(),\n                nameof(requestCreateOrder.Id),\n                requestCreateOrder.Id,\n                requestCreateOrder);\n\n            var result = await services.Mediator.Send(requestCreateOrder);\n\n            if (result)\n            {\n                services.Logger.LogInformation(\"CreateOrderCommand succeeded - RequestId: {RequestId}\", requestId);\n            }\n            else\n            {\n                services.Logger.LogWarning(\"CreateOrderCommand failed - RequestId: {RequestId}\", requestId);\n            }\n\n            return TypedResults.Ok();\n        }\n    }\n}\n\npublic record CreateOrderRequest(\n    string UserId,\n    string UserName,\n    string City,\n    string Street,\n    string State,\n    string Country,\n    string ZipCode,\n    string CardNumber,\n    string CardHolderName,\n    DateTime CardExpiration,\n    string CardSecurityNumber,\n    int CardTypeId,\n    string Buyer,\n    List<BasketItem> Items);\n"
  },
  {
    "path": "src/Ordering.API/Application/Behaviors/LoggingBehavior.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Behaviors;\npublic class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>\n{\n    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;\n    public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger) => _logger = logger;\n\n    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)\n    {\n        _logger.LogInformation(\"Handling command {CommandName} ({@Command})\", request.GetGenericTypeName(), request);\n        var response = await next();\n        _logger.LogInformation(\"Command {CommandName} handled - response: {@Response}\", request.GetGenericTypeName(), response);\n\n        return response;\n    }\n}\n\n"
  },
  {
    "path": "src/Ordering.API/Application/Behaviors/TransactionBehavior.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Behaviors;\n\nusing Microsoft.Extensions.Logging;\n\npublic class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>\n{\n    private readonly ILogger<TransactionBehavior<TRequest, TResponse>> _logger;\n    private readonly OrderingContext _dbContext;\n    private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;\n\n    public TransactionBehavior(OrderingContext dbContext,\n        IOrderingIntegrationEventService orderingIntegrationEventService,\n        ILogger<TransactionBehavior<TRequest, TResponse>> logger)\n    {\n        _dbContext = dbContext ?? throw new ArgumentException(nameof(OrderingContext));\n        _orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentException(nameof(orderingIntegrationEventService));\n        _logger = logger ?? throw new ArgumentException(nameof(ILogger));\n    }\n\n    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)\n    {\n        var response = default(TResponse);\n        var typeName = request.GetGenericTypeName();\n\n        try\n        {\n            if (_dbContext.HasActiveTransaction)\n            {\n                return await next();\n            }\n\n            var strategy = _dbContext.Database.CreateExecutionStrategy();\n\n            await strategy.ExecuteAsync(async () =>\n            {\n                Guid transactionId;\n\n                await using var transaction = await _dbContext.BeginTransactionAsync();\n                using (_logger.BeginScope(new List<KeyValuePair<string, object>> { new(\"TransactionContext\", transaction.TransactionId) }))\n                {\n                    _logger.LogInformation(\"Begin transaction {TransactionId} for {CommandName} ({@Command})\", transaction.TransactionId, typeName, request);\n\n                    response = await next();\n\n                    _logger.LogInformation(\"Commit transaction {TransactionId} for {CommandName}\", transaction.TransactionId, typeName);\n\n                    await _dbContext.CommitTransactionAsync(transaction);\n\n                    transactionId = transaction.TransactionId;\n                }\n\n                await _orderingIntegrationEventService.PublishEventsThroughEventBusAsync(transactionId);\n            });\n\n            return response;\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex, \"Error Handling transaction for {CommandName} ({@Command})\", typeName, request);\n\n            throw;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/Behaviors/ValidatorBehavior.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Behaviors;\n\npublic class ValidatorBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>\n{\n    private readonly ILogger<ValidatorBehavior<TRequest, TResponse>> _logger;\n    private readonly IEnumerable<IValidator<TRequest>> _validators;\n\n    public ValidatorBehavior(IEnumerable<IValidator<TRequest>> validators, ILogger<ValidatorBehavior<TRequest, TResponse>> logger)\n    {\n        _validators = validators;\n        _logger = logger;\n    }\n\n    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)\n    {\n        var typeName = request.GetGenericTypeName();\n\n        _logger.LogInformation(\"Validating command {CommandType}\", typeName);\n\n        var validationTasks = _validators.Select(v => v.ValidateAsync(request, cancellationToken));\n        var validationResults = await Task.WhenAll(validationTasks);\n        \n        var failures = validationResults\n            .SelectMany(result => result.Errors)\n            .Where(error => error != null)\n            .ToList();\n\n        if (failures.Any())\n        {\n            _logger.LogWarning(\"Validation errors - {CommandType} - Command: {@Command} - Errors: {@ValidationErrors}\", typeName, request, failures);\n\n            throw new OrderingDomainException(\n                $\"Command Validation Errors for type {typeof(TRequest).Name}\", new ValidationException(\"Validation exception\", failures));\n        }\n\n        return await next();\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/Commands/CancelOrderCommand.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Commands;\n\npublic record CancelOrderCommand(int OrderNumber) : IRequest<bool>;\n\n"
  },
  {
    "path": "src/Ordering.API/Application/Commands/CancelOrderCommandHandler.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Commands;\n\n// Regular CommandHandler\npublic class CancelOrderCommandHandler : IRequestHandler<CancelOrderCommand, bool>\n{\n    private readonly IOrderRepository _orderRepository;\n\n    public CancelOrderCommandHandler(IOrderRepository orderRepository)\n    {\n        _orderRepository = orderRepository;\n    }\n\n    /// <summary>\n    /// Handler which processes the command when\n    /// customer executes cancel order from app\n    /// </summary>\n    /// <param name=\"command\"></param>\n    /// <returns></returns>\n    public async Task<bool> Handle(CancelOrderCommand command, CancellationToken cancellationToken)\n    {\n        var orderToUpdate = await _orderRepository.GetAsync(command.OrderNumber);\n        if (orderToUpdate == null)\n        {\n            return false;\n        }\n\n        orderToUpdate.SetCancelledStatus();\n        return await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);\n    }\n}\n\n\n// Use for Idempotency in Command process\npublic class CancelOrderIdentifiedCommandHandler : IdentifiedCommandHandler<CancelOrderCommand, bool>\n{\n    public CancelOrderIdentifiedCommandHandler(\n        IMediator mediator,\n        IRequestManager requestManager,\n        ILogger<IdentifiedCommandHandler<CancelOrderCommand, bool>> logger)\n        : base(mediator, requestManager, logger)\n    {\n    }\n\n    protected override bool CreateResultForDuplicateRequest()\n    {\n        return true; // Ignore duplicate requests for processing order.\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/Commands/CreateOrderCommand.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Commands;\n\n// DDD and CQRS patterns comment: Note that it is recommended to implement immutable Commands\n// In this case, its immutability is achieved by having all the setters as private\n// plus only being able to update the data just once, when creating the object through its constructor.\n// References on Immutable Commands:  \n// http://cqrs.nu/Faq\n// https://docs.spine3.org/motivation/immutability.html \n// http://blog.gauffin.org/2012/06/griffin-container-introducing-command-support/\n// https://docs.microsoft.com/dotnet/csharp/programming-guide/classes-and-structs/how-to-implement-a-lightweight-class-with-auto-implemented-properties\n\nusing eShop.Ordering.API.Application.Models;\nusing eShop.Ordering.API.Extensions;\n\n[DataContract]\npublic class CreateOrderCommand\n    : IRequest<bool>\n{\n    [DataMember]\n    private readonly List<OrderItemDTO> _orderItems;\n\n    [DataMember]\n    public string UserId { get; private set; }\n\n    [DataMember]\n    public string UserName { get; private set; }\n\n    [DataMember]\n    public string City { get; private set; }\n\n    [DataMember]\n    public string Street { get; private set; }\n\n    [DataMember]\n    public string State { get; private set; }\n\n    [DataMember]\n    public string Country { get; private set; }\n\n    [DataMember]\n    public string ZipCode { get; private set; }\n\n    [DataMember]\n    public string CardNumber { get; private set; }\n\n    [DataMember]\n    public string CardHolderName { get; private set; }\n\n    [DataMember]\n    public DateTime CardExpiration { get; private set; }\n\n    [DataMember]\n    public string CardSecurityNumber { get; private set; }\n\n    [DataMember]\n    public int CardTypeId { get; private set; }\n\n    [DataMember]\n    public IEnumerable<OrderItemDTO> OrderItems => _orderItems;\n\n    public CreateOrderCommand()\n    {\n        _orderItems = new List<OrderItemDTO>();\n    }\n\n    public CreateOrderCommand(List<BasketItem> basketItems, string userId, string userName, string city, string street, string state, string country, string zipcode,\n        string cardNumber, string cardHolderName, DateTime cardExpiration,\n        string cardSecurityNumber, int cardTypeId)\n    {\n        _orderItems = basketItems.ToOrderItemsDTO().ToList();\n        UserId = userId;\n        UserName = userName;\n        City = city;\n        Street = street;\n        State = state;\n        Country = country;\n        ZipCode = zipcode;\n        CardNumber = cardNumber;\n        CardHolderName = cardHolderName;\n        CardExpiration = cardExpiration;\n        CardSecurityNumber = cardSecurityNumber;\n        CardTypeId = cardTypeId;\n    }\n}\n\n"
  },
  {
    "path": "src/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Commands;\n\nusing eShop.Ordering.Domain.AggregatesModel.OrderAggregate;\n\n// Regular CommandHandler\npublic class CreateOrderCommandHandler\n    : IRequestHandler<CreateOrderCommand, bool>\n{\n    private readonly IOrderRepository _orderRepository;\n    private readonly IIdentityService _identityService;\n    private readonly IMediator _mediator;\n    private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;\n    private readonly ILogger<CreateOrderCommandHandler> _logger;\n\n    // Using DI to inject infrastructure persistence Repositories\n    public CreateOrderCommandHandler(IMediator mediator,\n        IOrderingIntegrationEventService orderingIntegrationEventService,\n        IOrderRepository orderRepository,\n        IIdentityService identityService,\n        ILogger<CreateOrderCommandHandler> logger)\n    {\n        _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));\n        _identityService = identityService ?? throw new ArgumentNullException(nameof(identityService));\n        _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));\n        _orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService));\n        _logger = logger ?? throw new ArgumentNullException(nameof(logger));\n    }\n\n    public async Task<bool> Handle(CreateOrderCommand message, CancellationToken cancellationToken)\n    {\n        // Add Integration event to clean the basket\n        var orderStartedIntegrationEvent = new OrderStartedIntegrationEvent(message.UserId);\n        await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStartedIntegrationEvent);\n\n        // Add/Update the Buyer AggregateRoot\n        // DDD patterns comment: Add child entities and value-objects through the Order Aggregate-Root\n        // methods and constructor so validations, invariants and business logic \n        // make sure that consistency is preserved across the whole aggregate\n        var address = new Address(message.Street, message.City, message.State, message.Country, message.ZipCode);\n        var order = new Order(message.UserId, message.UserName, address, message.CardTypeId, message.CardNumber, message.CardSecurityNumber, message.CardHolderName, message.CardExpiration);\n\n        foreach (var item in message.OrderItems)\n        {\n            order.AddOrderItem(item.ProductId, item.ProductName, item.UnitPrice, item.Discount, item.PictureUrl, item.Units);\n        }\n\n        _logger.LogInformation(\"Creating Order - Order: {@Order}\", order);\n\n        _orderRepository.Add(order);\n\n        return await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);\n    }\n}\n\n\n// Use for Idempotency in Command process\npublic class CreateOrderIdentifiedCommandHandler : IdentifiedCommandHandler<CreateOrderCommand, bool>\n{\n    public CreateOrderIdentifiedCommandHandler(\n        IMediator mediator,\n        IRequestManager requestManager,\n        ILogger<IdentifiedCommandHandler<CreateOrderCommand, bool>> logger)\n        : base(mediator, requestManager, logger)\n    {\n    }\n\n    protected override bool CreateResultForDuplicateRequest()\n    {\n        return true; // Ignore duplicate requests for creating order.\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/Commands/CreateOrderDraftCommand.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Commands;\nusing eShop.Ordering.API.Application.Models;\n\npublic record CreateOrderDraftCommand(string BuyerId, IEnumerable<BasketItem> Items) : IRequest<OrderDraftDTO>;\n"
  },
  {
    "path": "src/Ordering.API/Application/Commands/CreateOrderDraftCommandHandler.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Commands;\n\nusing eShop.Ordering.API.Extensions;\nusing eShop.Ordering.Domain.AggregatesModel.OrderAggregate;\n\n// Regular CommandHandler\npublic class CreateOrderDraftCommandHandler\n    : IRequestHandler<CreateOrderDraftCommand, OrderDraftDTO>\n{\n    public Task<OrderDraftDTO> Handle(CreateOrderDraftCommand message, CancellationToken cancellationToken)\n    {\n        var order = Order.NewDraft();\n        var orderItems = message.Items.Select(i => i.ToOrderItemDTO());\n        foreach (var item in orderItems)\n        {\n            order.AddOrderItem(item.ProductId, item.ProductName, item.UnitPrice, item.Discount, item.PictureUrl, item.Units);\n        }\n\n        return Task.FromResult(OrderDraftDTO.FromOrder(order));\n    }\n}\n\npublic record OrderDraftDTO\n{\n    public IEnumerable<OrderItemDTO> OrderItems { get; init; }\n    public decimal Total { get; init; }\n\n    public static OrderDraftDTO FromOrder(Order order)\n    {\n        return new OrderDraftDTO()\n        {\n            OrderItems = order.OrderItems.Select(oi => new OrderItemDTO\n            {\n                Discount = oi.Discount,\n                ProductId = oi.ProductId,\n                UnitPrice = oi.UnitPrice,\n                PictureUrl = oi.PictureUrl,\n                Units = oi.Units,\n                ProductName = oi.ProductName\n            }),\n            Total = order.GetTotal()\n        };\n    }\n}\n\npublic record OrderItemDTO\n{\n    public int ProductId { get; init; }\n\n    public string ProductName { get; init; }\n\n    public decimal UnitPrice { get; init; }\n\n    public decimal Discount { get; init; }\n\n    public int Units { get; init; }\n\n    public string PictureUrl { get; init; }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/Commands/IdentifiedCommand.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Commands;\n\npublic class IdentifiedCommand<T, R> : IRequest<R>\n    where T : IRequest<R>\n{\n    public T Command { get; }\n    public Guid Id { get; }\n    public IdentifiedCommand(T command, Guid id)\n    {\n        Command = command;\n        Id = id;\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/Commands/IdentifiedCommandHandler.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Commands;\n\n/// <summary>\n/// Provides a base implementation for handling duplicate request and ensuring idempotent updates, in the cases where\n/// a requestid sent by client is used to detect duplicate requests.\n/// </summary>\n/// <typeparam name=\"T\">Type of the command handler that performs the operation if request is not duplicated</typeparam>\n/// <typeparam name=\"R\">Return value of the inner command handler</typeparam>\npublic abstract class IdentifiedCommandHandler<T, R> : IRequestHandler<IdentifiedCommand<T, R>, R>\n    where T : IRequest<R>\n{\n    private readonly IMediator _mediator;\n    private readonly IRequestManager _requestManager;\n    private readonly ILogger<IdentifiedCommandHandler<T, R>> _logger;\n\n    public IdentifiedCommandHandler(\n        IMediator mediator,\n        IRequestManager requestManager,\n        ILogger<IdentifiedCommandHandler<T, R>> logger)\n    {\n        ArgumentNullException.ThrowIfNull(logger);\n        _mediator = mediator;\n        _requestManager = requestManager;\n        _logger = logger;\n    }\n\n    /// <summary>\n    /// Creates the result value to return if a previous request was found\n    /// </summary>\n    /// <returns></returns>\n    protected abstract R CreateResultForDuplicateRequest();\n\n    /// <summary>\n    /// This method handles the command. It just ensures that no other request exists with the same ID, and if this is the case\n    /// just enqueues the original inner command.\n    /// </summary>\n    /// <param name=\"message\">IdentifiedCommand which contains both original command & request ID</param>\n    /// <returns>Return value of inner command or default value if request same ID was found</returns>\n    public async Task<R> Handle(IdentifiedCommand<T, R> message, CancellationToken cancellationToken)\n    {\n        var alreadyExists = await _requestManager.ExistAsync(message.Id);\n        if (alreadyExists)\n        {\n            return CreateResultForDuplicateRequest();\n        }\n        else\n        {\n            await _requestManager.CreateRequestForCommandAsync<T>(message.Id);\n            try\n            {\n                var command = message.Command;\n                var commandName = command.GetGenericTypeName();\n                var idProperty = string.Empty;\n                var commandId = string.Empty;\n\n                switch (command)\n                {\n                    case CreateOrderCommand createOrderCommand:\n                        idProperty = nameof(createOrderCommand.UserId);\n                        commandId = createOrderCommand.UserId;\n                        break;\n\n                    case CancelOrderCommand cancelOrderCommand:\n                        idProperty = nameof(cancelOrderCommand.OrderNumber);\n                        commandId = $\"{cancelOrderCommand.OrderNumber}\";\n                        break;\n\n                    case ShipOrderCommand shipOrderCommand:\n                        idProperty = nameof(shipOrderCommand.OrderNumber);\n                        commandId = $\"{shipOrderCommand.OrderNumber}\";\n                        break;\n\n                    default:\n                        idProperty = \"Id?\";\n                        commandId = \"n/a\";\n                        break;\n                }\n\n                _logger.LogInformation(\n                    \"Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})\",\n                    commandName,\n                    idProperty,\n                    commandId,\n                    command);\n\n                // Send the embedded business command to mediator so it runs its related CommandHandler \n                var result = await _mediator.Send(command, cancellationToken);\n\n                _logger.LogInformation(\n                    \"Command result: {@Result} - {CommandName} - {IdProperty}: {CommandId} ({@Command})\",\n                    result,\n                    commandName,\n                    idProperty,\n                    commandId,\n                    command);\n\n                return result;\n            }\n            catch\n            {\n                return default;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/Commands/SetAwaitingValidationOrderStatusCommand.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Commands;\n\npublic record SetAwaitingValidationOrderStatusCommand(int OrderNumber) : IRequest<bool>;\n"
  },
  {
    "path": "src/Ordering.API/Application/Commands/SetAwaitingValidationOrderStatusCommandHandler.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Commands;\n\n// Regular CommandHandler\npublic class SetAwaitingValidationOrderStatusCommandHandler : IRequestHandler<SetAwaitingValidationOrderStatusCommand, bool>\n{\n    private readonly IOrderRepository _orderRepository;\n\n    public SetAwaitingValidationOrderStatusCommandHandler(IOrderRepository orderRepository)\n    {\n        _orderRepository = orderRepository;\n    }\n\n    /// <summary>\n    /// Handler which processes the command when\n    /// graceperiod has finished\n    /// </summary>\n    /// <param name=\"command\"></param>\n    /// <returns></returns>\n    public async Task<bool> Handle(SetAwaitingValidationOrderStatusCommand command, CancellationToken cancellationToken)\n    {\n        var orderToUpdate = await _orderRepository.GetAsync(command.OrderNumber);\n        if (orderToUpdate == null)\n        {\n            return false;\n        }\n\n        orderToUpdate.SetAwaitingValidationStatus();\n        return await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);\n    }\n}\n\n\n// Use for Idempotency in Command process\npublic class SetAwaitingValidationIdentifiedOrderStatusCommandHandler : IdentifiedCommandHandler<SetAwaitingValidationOrderStatusCommand, bool>\n{\n    public SetAwaitingValidationIdentifiedOrderStatusCommandHandler(\n        IMediator mediator,\n        IRequestManager requestManager,\n        ILogger<IdentifiedCommandHandler<SetAwaitingValidationOrderStatusCommand, bool>> logger)\n        : base(mediator, requestManager, logger)\n    {\n    }\n\n    protected override bool CreateResultForDuplicateRequest()\n    {\n        return true; // Ignore duplicate requests for processing order.\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/Commands/SetPaidOrderStatusCommand.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Commands;\n\npublic record SetPaidOrderStatusCommand(int OrderNumber) : IRequest<bool>;\n"
  },
  {
    "path": "src/Ordering.API/Application/Commands/SetPaidOrderStatusCommandHandler.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Commands;\n\n// Regular CommandHandler\npublic class SetPaidOrderStatusCommandHandler : IRequestHandler<SetPaidOrderStatusCommand, bool>\n{\n    private readonly IOrderRepository _orderRepository;\n\n    public SetPaidOrderStatusCommandHandler(IOrderRepository orderRepository)\n    {\n        _orderRepository = orderRepository;\n    }\n\n    /// <summary>\n    /// Handler which processes the command when\n    /// Shipment service confirms the payment\n    /// </summary>\n    /// <param name=\"command\"></param>\n    /// <returns></returns>\n    public async Task<bool> Handle(SetPaidOrderStatusCommand command, CancellationToken cancellationToken)\n    {\n        // Simulate a work time for validating the payment\n        await Task.Delay(10000, cancellationToken);\n\n        var orderToUpdate = await _orderRepository.GetAsync(command.OrderNumber);\n        if (orderToUpdate == null)\n        {\n            return false;\n        }\n\n        orderToUpdate.SetPaidStatus();\n        return await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);\n    }\n}\n\n\n// Use for Idempotency in Command process\npublic class SetPaidIdentifiedOrderStatusCommandHandler : IdentifiedCommandHandler<SetPaidOrderStatusCommand, bool>\n{\n    public SetPaidIdentifiedOrderStatusCommandHandler(\n        IMediator mediator,\n        IRequestManager requestManager,\n        ILogger<IdentifiedCommandHandler<SetPaidOrderStatusCommand, bool>> logger)\n        : base(mediator, requestManager, logger)\n    {\n    }\n\n    protected override bool CreateResultForDuplicateRequest()\n    {\n        return true; // Ignore duplicate requests for processing order.\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/Commands/SetStockConfirmedOrderStatusCommand.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Commands;\n\npublic record SetStockConfirmedOrderStatusCommand(int OrderNumber) : IRequest<bool>;\n"
  },
  {
    "path": "src/Ordering.API/Application/Commands/SetStockConfirmedOrderStatusCommandHandler.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Commands;\n\n// Regular CommandHandler\npublic class SetStockConfirmedOrderStatusCommandHandler : IRequestHandler<SetStockConfirmedOrderStatusCommand, bool>\n{\n    private readonly IOrderRepository _orderRepository;\n\n    public SetStockConfirmedOrderStatusCommandHandler(IOrderRepository orderRepository)\n    {\n        _orderRepository = orderRepository;\n    }\n\n    /// <summary>\n    /// Handler which processes the command when\n    /// Stock service confirms the request\n    /// </summary>\n    /// <param name=\"command\"></param>\n    /// <returns></returns>\n    public async Task<bool> Handle(SetStockConfirmedOrderStatusCommand command, CancellationToken cancellationToken)\n    {\n        // Simulate a work time for confirming the stock\n        await Task.Delay(10000, cancellationToken);\n\n        var orderToUpdate = await _orderRepository.GetAsync(command.OrderNumber);\n        if (orderToUpdate == null)\n        {\n            return false;\n        }\n\n        orderToUpdate.SetStockConfirmedStatus();\n        return await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);\n    }\n}\n\n\n// Use for Idempotency in Command process\npublic class SetStockConfirmedOrderStatusIdentifiedCommandHandler : IdentifiedCommandHandler<SetStockConfirmedOrderStatusCommand, bool>\n{\n    public SetStockConfirmedOrderStatusIdentifiedCommandHandler(\n        IMediator mediator,\n        IRequestManager requestManager,\n        ILogger<IdentifiedCommandHandler<SetStockConfirmedOrderStatusCommand, bool>> logger)\n        : base(mediator, requestManager, logger)\n    {\n    }\n\n    protected override bool CreateResultForDuplicateRequest()\n    {\n        return true; // Ignore duplicate requests for processing order.\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/Commands/SetStockRejectedOrderStatusCommand.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Commands;\n\npublic record SetStockRejectedOrderStatusCommand(int OrderNumber, List<int> OrderStockItems) : IRequest<bool>;\n"
  },
  {
    "path": "src/Ordering.API/Application/Commands/SetStockRejectedOrderStatusCommandHandler.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Commands;\n\n// Regular CommandHandler\npublic class SetStockRejectedOrderStatusCommandHandler : IRequestHandler<SetStockRejectedOrderStatusCommand, bool>\n{\n    private readonly IOrderRepository _orderRepository;\n\n    public SetStockRejectedOrderStatusCommandHandler(IOrderRepository orderRepository)\n    {\n        _orderRepository = orderRepository;\n    }\n\n    /// <summary>\n    /// Handler which processes the command when\n    /// Stock service rejects the request\n    /// </summary>\n    /// <param name=\"command\"></param>\n    /// <returns></returns>\n    public async Task<bool> Handle(SetStockRejectedOrderStatusCommand command, CancellationToken cancellationToken)\n    {\n        // Simulate a work time for rejecting the stock\n        await Task.Delay(10000, cancellationToken);\n\n        var orderToUpdate = await _orderRepository.GetAsync(command.OrderNumber);\n        if (orderToUpdate == null)\n        {\n            return false;\n        }\n\n        orderToUpdate.SetCancelledStatusWhenStockIsRejected(command.OrderStockItems);\n\n        return await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);\n    }\n}\n\n\n// Use for Idempotency in Command process\npublic class SetStockRejectedOrderStatusIdentifiedCommandHandler : IdentifiedCommandHandler<SetStockRejectedOrderStatusCommand, bool>\n{\n    public SetStockRejectedOrderStatusIdentifiedCommandHandler(\n        IMediator mediator,\n        IRequestManager requestManager,\n        ILogger<IdentifiedCommandHandler<SetStockRejectedOrderStatusCommand, bool>> logger)\n        : base(mediator, requestManager, logger)\n    {\n    }\n\n    protected override bool CreateResultForDuplicateRequest()\n    {\n        return true; // Ignore duplicate requests for processing order.\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/Commands/ShipOrderCommand.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Commands;\n\npublic record ShipOrderCommand(int OrderNumber) : IRequest<bool>;\n"
  },
  {
    "path": "src/Ordering.API/Application/Commands/ShipOrderCommandHandler.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Commands;\n\n// Regular CommandHandler\npublic class ShipOrderCommandHandler : IRequestHandler<ShipOrderCommand, bool>\n{\n    private readonly IOrderRepository _orderRepository;\n\n    public ShipOrderCommandHandler(IOrderRepository orderRepository)\n    {\n        _orderRepository = orderRepository;\n    }\n\n    /// <summary>\n    /// Handler which processes the command when\n    /// administrator executes ship order from app\n    /// </summary>\n    /// <param name=\"command\"></param>\n    /// <returns></returns>\n    public async Task<bool> Handle(ShipOrderCommand command, CancellationToken cancellationToken)\n    {\n        var orderToUpdate = await _orderRepository.GetAsync(command.OrderNumber);\n        if (orderToUpdate == null)\n        {\n            return false;\n        }\n\n        orderToUpdate.SetShippedStatus();\n        return await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);\n    }\n}\n\n\n// Use for Idempotency in Command process\npublic class ShipOrderIdentifiedCommandHandler : IdentifiedCommandHandler<ShipOrderCommand, bool>\n{\n    public ShipOrderIdentifiedCommandHandler(\n        IMediator mediator,\n        IRequestManager requestManager,\n        ILogger<IdentifiedCommandHandler<ShipOrderCommand, bool>> logger)\n        : base(mediator, requestManager, logger)\n    {\n    }\n\n    protected override bool CreateResultForDuplicateRequest()\n    {\n        return true; // Ignore duplicate requests for processing order.\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/DomainEventHandlers/OrderCancelledDomainEventHandler.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.DomainEventHandlers;\n\npublic partial class OrderCancelledDomainEventHandler\n                : INotificationHandler<OrderCancelledDomainEvent>\n{\n    private readonly IOrderRepository _orderRepository;\n    private readonly IBuyerRepository _buyerRepository;\n    private readonly ILogger _logger;\n    private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;\n\n    public OrderCancelledDomainEventHandler(\n        IOrderRepository orderRepository,\n        ILogger<OrderCancelledDomainEventHandler> logger,\n        IBuyerRepository buyerRepository,\n        IOrderingIntegrationEventService orderingIntegrationEventService)\n    {\n        _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));\n        _logger = logger ?? throw new ArgumentNullException(nameof(logger));\n        _buyerRepository = buyerRepository ?? throw new ArgumentNullException(nameof(buyerRepository));\n        _orderingIntegrationEventService = orderingIntegrationEventService;\n    }\n\n    public async Task Handle(OrderCancelledDomainEvent domainEvent, CancellationToken cancellationToken)\n    {\n        OrderingApiTrace.LogOrderStatusUpdated(_logger, domainEvent.Order.Id, OrderStatus.Cancelled);\n\n        var order = await _orderRepository.GetAsync(domainEvent.Order.Id);\n        var buyer = await _buyerRepository.FindByIdAsync(order.BuyerId.Value);\n\n        var integrationEvent = new OrderStatusChangedToCancelledIntegrationEvent(order.Id, order.OrderStatus, buyer.Name, buyer.IdentityGuid);\n        await _orderingIntegrationEventService.AddAndSaveEventAsync(integrationEvent);\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/DomainEventHandlers/OrderShippedDomainEventHandler.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.DomainEventHandlers;\n\npublic class OrderShippedDomainEventHandler\n                : INotificationHandler<OrderShippedDomainEvent>\n{\n    private readonly IOrderRepository _orderRepository;\n    private readonly IBuyerRepository _buyerRepository;\n    private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;\n    private readonly ILogger _logger;\n\n    public OrderShippedDomainEventHandler(\n        IOrderRepository orderRepository,\n        ILogger<OrderShippedDomainEventHandler> logger,\n        IBuyerRepository buyerRepository,\n        IOrderingIntegrationEventService orderingIntegrationEventService)\n    {\n        _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));\n        _logger = logger ?? throw new ArgumentNullException(nameof(logger));\n        _buyerRepository = buyerRepository ?? throw new ArgumentNullException(nameof(buyerRepository));\n        _orderingIntegrationEventService = orderingIntegrationEventService;\n    }\n\n    public async Task Handle(OrderShippedDomainEvent domainEvent, CancellationToken cancellationToken)\n    {\n        OrderingApiTrace.LogOrderStatusUpdated(_logger, domainEvent.Order.Id, OrderStatus.Shipped);\n\n        var order = await _orderRepository.GetAsync(domainEvent.Order.Id);\n        var buyer = await _buyerRepository.FindByIdAsync(order.BuyerId.Value);\n\n        var integrationEvent = new OrderStatusChangedToShippedIntegrationEvent(order.Id, order.OrderStatus, buyer.Name, buyer.IdentityGuid);\n        await _orderingIntegrationEventService.AddAndSaveEventAsync(integrationEvent);\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/DomainEventHandlers/OrderStatusChangedToAwaitingValidationDomainEventHandler.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.DomainEventHandlers;\n\npublic class OrderStatusChangedToAwaitingValidationDomainEventHandler\n                : INotificationHandler<OrderStatusChangedToAwaitingValidationDomainEvent>\n{\n    private readonly IOrderRepository _orderRepository;\n    private readonly ILogger _logger;\n    private readonly IBuyerRepository _buyerRepository;\n    private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;\n\n    public OrderStatusChangedToAwaitingValidationDomainEventHandler(\n        IOrderRepository orderRepository,\n        ILogger<OrderStatusChangedToAwaitingValidationDomainEventHandler> logger,\n        IBuyerRepository buyerRepository,\n        IOrderingIntegrationEventService orderingIntegrationEventService)\n    {\n        _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));\n        _logger = logger ?? throw new ArgumentNullException(nameof(logger));\n        _buyerRepository = buyerRepository;\n        _orderingIntegrationEventService = orderingIntegrationEventService;\n    }\n\n    public async Task Handle(OrderStatusChangedToAwaitingValidationDomainEvent domainEvent, CancellationToken cancellationToken)\n    {\n        OrderingApiTrace.LogOrderStatusUpdated(_logger, domainEvent.OrderId, OrderStatus.AwaitingValidation);\n\n        var order = await _orderRepository.GetAsync(domainEvent.OrderId);\n        var buyer = await _buyerRepository.FindByIdAsync(order.BuyerId.Value);\n\n        var orderStockList = domainEvent.OrderItems\n            .Select(orderItem => new OrderStockItem(orderItem.ProductId, orderItem.Units));\n\n        var integrationEvent = new OrderStatusChangedToAwaitingValidationIntegrationEvent(order.Id, order.OrderStatus, buyer.Name, buyer.IdentityGuid, orderStockList);\n        await _orderingIntegrationEventService.AddAndSaveEventAsync(integrationEvent);\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/DomainEventHandlers/OrderStatusChangedToPaidDomainEventHandler.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.DomainEventHandlers;\n\npublic class OrderStatusChangedToPaidDomainEventHandler : INotificationHandler<OrderStatusChangedToPaidDomainEvent>\n{\n    private readonly IOrderRepository _orderRepository;\n    private readonly ILogger _logger;\n    private readonly IBuyerRepository _buyerRepository;\n    private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;\n\n    public OrderStatusChangedToPaidDomainEventHandler(\n        IOrderRepository orderRepository,\n        ILogger<OrderStatusChangedToPaidDomainEventHandler> logger,\n        IBuyerRepository buyerRepository,\n        IOrderingIntegrationEventService orderingIntegrationEventService)\n    {\n        _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));\n        _logger = logger ?? throw new ArgumentNullException(nameof(logger));\n        _buyerRepository = buyerRepository ?? throw new ArgumentNullException(nameof(buyerRepository));\n        _orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService));\n    }\n\n    public async Task Handle(OrderStatusChangedToPaidDomainEvent domainEvent, CancellationToken cancellationToken)\n    {\n        OrderingApiTrace.LogOrderStatusUpdated(_logger, domainEvent.OrderId, OrderStatus.Paid);\n\n        var order = await _orderRepository.GetAsync(domainEvent.OrderId);\n        var buyer = await _buyerRepository.FindByIdAsync(order.BuyerId.Value);\n\n        var orderStockList = domainEvent.OrderItems\n            .Select(orderItem => new OrderStockItem(orderItem.ProductId, orderItem.Units));\n\n        var integrationEvent = new OrderStatusChangedToPaidIntegrationEvent(\n            domainEvent.OrderId,\n            order.OrderStatus,\n            buyer.Name,\n            buyer.IdentityGuid,\n            orderStockList);\n\n        await _orderingIntegrationEventService.AddAndSaveEventAsync(integrationEvent);\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/DomainEventHandlers/OrderStatusChangedToStockConfirmedDomainEventHandler.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.DomainEventHandlers;\n\npublic class OrderStatusChangedToStockConfirmedDomainEventHandler\n                : INotificationHandler<OrderStatusChangedToStockConfirmedDomainEvent>\n{\n    private readonly IOrderRepository _orderRepository;\n    private readonly IBuyerRepository _buyerRepository;\n    private readonly ILogger _logger;\n    private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;\n\n    public OrderStatusChangedToStockConfirmedDomainEventHandler(\n        IOrderRepository orderRepository,\n        IBuyerRepository buyerRepository,\n        ILogger<OrderStatusChangedToStockConfirmedDomainEventHandler> logger,\n        IOrderingIntegrationEventService orderingIntegrationEventService)\n    {\n        _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));\n        _buyerRepository = buyerRepository ?? throw new ArgumentNullException(nameof(buyerRepository));\n        _logger = logger ?? throw new ArgumentNullException(nameof(logger));\n        _orderingIntegrationEventService = orderingIntegrationEventService;\n    }\n\n    public async Task Handle(OrderStatusChangedToStockConfirmedDomainEvent domainEvent, CancellationToken cancellationToken)\n    {\n        OrderingApiTrace.LogOrderStatusUpdated(_logger, domainEvent.OrderId, OrderStatus.StockConfirmed);\n\n        var order = await _orderRepository.GetAsync(domainEvent.OrderId);\n        var buyer = await _buyerRepository.FindByIdAsync(order.BuyerId.Value);\n\n        var integrationEvent = new OrderStatusChangedToStockConfirmedIntegrationEvent(order.Id, order.OrderStatus, buyer.Name, buyer.IdentityGuid);\n        await _orderingIntegrationEventService.AddAndSaveEventAsync(integrationEvent);\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/DomainEventHandlers/UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.DomainEventHandlers;\n\npublic class UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler : INotificationHandler<BuyerAndPaymentMethodVerifiedDomainEvent>\n{\n    private readonly IOrderRepository _orderRepository;\n    private readonly ILogger _logger;\n\n    public UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler(\n        IOrderRepository orderRepository,\n        ILogger<UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler> logger)\n    {\n        _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));\n        _logger = logger ?? throw new ArgumentNullException(nameof(logger));\n    }\n\n    // Domain Logic comment:\n    // When the Buyer and Buyer's payment method have been created or verified that they existed, \n    // then we can update the original Order with the BuyerId and PaymentId (foreign keys)\n    public async Task Handle(BuyerAndPaymentMethodVerifiedDomainEvent domainEvent, CancellationToken cancellationToken)\n    {\n        var orderToUpdate = await _orderRepository.GetAsync(domainEvent.OrderId);\n        orderToUpdate.SetPaymentMethodVerified(domainEvent.Buyer.Id, domainEvent.Payment.Id); \n        OrderingApiTrace.LogOrderPaymentMethodUpdated(_logger, domainEvent.OrderId, nameof(domainEvent.Payment), domainEvent.Payment.Id);\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/DomainEventHandlers/ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.DomainEventHandlers;\n\npublic class ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler\n                    : INotificationHandler<OrderStartedDomainEvent>\n{\n    private readonly ILogger _logger;\n    private readonly IBuyerRepository _buyerRepository;\n    private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;\n\n    public ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler(\n        ILogger<ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler> logger,\n        IBuyerRepository buyerRepository,\n        IOrderingIntegrationEventService orderingIntegrationEventService)\n    {\n        _buyerRepository = buyerRepository ?? throw new ArgumentNullException(nameof(buyerRepository));\n        _orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService));\n        _logger = logger ?? throw new ArgumentNullException(nameof(logger));\n    }\n\n    public async Task Handle(OrderStartedDomainEvent domainEvent, CancellationToken cancellationToken)\n    {\n        var cardTypeId = domainEvent.CardTypeId != 0 ? domainEvent.CardTypeId : 1;\n        var buyer = await _buyerRepository.FindAsync(domainEvent.UserId);\n        var buyerExisted = buyer is not null;\n\n        if (!buyerExisted)\n        {\n            buyer = new Buyer(domainEvent.UserId, domainEvent.UserName);\n        }\n\n        // REVIEW: The event this creates needs to be sent after SaveChanges has propagated the buyer Id. It currently only\n        // works by coincidence. If we remove HiLo or if anything decides to yield earlier, it will break.\n\n        buyer.VerifyOrAddPaymentMethod(cardTypeId,\n                                        $\"Payment Method on {DateTime.UtcNow}\",\n                                        domainEvent.CardNumber,\n                                        domainEvent.CardSecurityNumber,\n                                        domainEvent.CardHolderName,\n                                        domainEvent.CardExpiration,\n                                        domainEvent.Order.Id);\n\n        if (!buyerExisted)\n        {\n            _buyerRepository.Add(buyer);\n        }\n\n        await _buyerRepository.UnitOfWork\n            .SaveEntitiesAsync(cancellationToken);\n\n        var integrationEvent = new OrderStatusChangedToSubmittedIntegrationEvent(domainEvent.Order.Id, domainEvent.Order.OrderStatus, buyer.Name, buyer.IdentityGuid);\n        await _orderingIntegrationEventService.AddAndSaveEventAsync(integrationEvent);\n        OrderingApiTrace.LogOrderBuyerAndPaymentValidatedOrUpdated(_logger, buyer.Id, domainEvent.Order.Id);\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/IntegrationEvents/EventHandling/GracePeriodConfirmedIntegrationEventHandler.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.IntegrationEvents.EventHandling;\n\npublic class GracePeriodConfirmedIntegrationEventHandler(\n    IMediator mediator,\n    ILogger<GracePeriodConfirmedIntegrationEventHandler> logger) : IIntegrationEventHandler<GracePeriodConfirmedIntegrationEvent>\n{\n    /// <summary>\n    /// Event handler which confirms that the grace period\n    /// has been completed and order will not initially be cancelled.\n    /// Therefore, the order process continues for validation. \n    /// </summary>\n    /// <param name=\"event\">       \n    /// </param>\n    /// <returns></returns>\n    public async Task Handle(GracePeriodConfirmedIntegrationEvent @event)\n    {\n        logger.LogInformation(\"Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})\", @event.Id, @event);\n\n        var command = new SetAwaitingValidationOrderStatusCommand(@event.OrderId);\n\n        logger.LogInformation(\n            \"Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})\",\n            command.GetGenericTypeName(),\n            nameof(command.OrderNumber),\n            command.OrderNumber,\n            command);\n\n        await mediator.Send(command);\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentFailedIntegrationEventHandler.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.IntegrationEvents.EventHandling;\n\npublic class OrderPaymentFailedIntegrationEventHandler(\n    IMediator mediator,\n    ILogger<OrderPaymentFailedIntegrationEventHandler> logger) :\n    IIntegrationEventHandler<OrderPaymentFailedIntegrationEvent>\n{\n    public async Task Handle(OrderPaymentFailedIntegrationEvent @event)\n    {\n        logger.LogInformation(\"Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})\", @event.Id, @event);\n\n        var command = new CancelOrderCommand(@event.OrderId);\n\n        logger.LogInformation(\n            \"Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})\",\n            command.GetGenericTypeName(),\n            nameof(command.OrderNumber),\n            command.OrderNumber,\n            command);\n\n        await mediator.Send(command);\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/IntegrationEvents/EventHandling/OrderPaymentSucceededIntegrationEventHandler.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.IntegrationEvents.EventHandling;\n\npublic class OrderPaymentSucceededIntegrationEventHandler(\n    IMediator mediator,\n    ILogger<OrderPaymentSucceededIntegrationEventHandler> logger) :\n    IIntegrationEventHandler<OrderPaymentSucceededIntegrationEvent>\n{\n    public async Task Handle(OrderPaymentSucceededIntegrationEvent @event)\n    {\n        logger.LogInformation(\"Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})\", @event.Id, @event);\n\n        var command = new SetPaidOrderStatusCommand(@event.OrderId);\n\n        logger.LogInformation(\n            \"Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})\",\n            command.GetGenericTypeName(),\n            nameof(command.OrderNumber),\n            command.OrderNumber,\n            command);\n\n        await mediator.Send(command);\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockConfirmedIntegrationEventHandler.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.IntegrationEvents.EventHandling;\n\npublic class OrderStockConfirmedIntegrationEventHandler(\n    IMediator mediator,\n    ILogger<OrderStockConfirmedIntegrationEventHandler> logger) :\n    IIntegrationEventHandler<OrderStockConfirmedIntegrationEvent>\n{\n    public async Task Handle(OrderStockConfirmedIntegrationEvent @event)\n    {\n        logger.LogInformation(\"Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})\", @event.Id, @event);\n\n        var command = new SetStockConfirmedOrderStatusCommand(@event.OrderId);\n\n        logger.LogInformation(\n            \"Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})\",\n            command.GetGenericTypeName(),\n            nameof(command.OrderNumber),\n            command.OrderNumber,\n            command);\n\n        await mediator.Send(command);\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/IntegrationEvents/EventHandling/OrderStockRejectedIntegrationEventHandler.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.IntegrationEvents.EventHandling;\npublic class OrderStockRejectedIntegrationEventHandler(\n    IMediator mediator,\n    ILogger<OrderStockRejectedIntegrationEventHandler> logger) : IIntegrationEventHandler<OrderStockRejectedIntegrationEvent>\n{\n    public async Task Handle(OrderStockRejectedIntegrationEvent @event)\n    {\n        logger.LogInformation(\"Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})\", @event.Id, @event);\n\n        var orderStockRejectedItems = @event.OrderStockItems\n            .FindAll(c => !c.HasStock)\n            .Select(c => c.ProductId)\n            .ToList();\n\n        var command = new SetStockRejectedOrderStatusCommand(@event.OrderId, orderStockRejectedItems);\n\n        logger.LogInformation(\n            \"Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})\",\n            command.GetGenericTypeName(),\n            nameof(command.OrderNumber),\n            command.OrderNumber,\n            command);\n\n        await mediator.Send(command);\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/IntegrationEvents/Events/GracePeriodConfirmedIntegrationEvent.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.IntegrationEvents.Events;\n\npublic record GracePeriodConfirmedIntegrationEvent : IntegrationEvent\n{\n    public int OrderId { get; }\n\n    public GracePeriodConfirmedIntegrationEvent(int orderId) =>\n        OrderId = orderId;\n}\n\n"
  },
  {
    "path": "src/Ordering.API/Application/IntegrationEvents/Events/OrderPaymentFailedIntegrationEvent .cs",
    "content": "﻿namespace eShop.Ordering.API.Application.IntegrationEvents.Events;\n\npublic record OrderPaymentFailedIntegrationEvent : IntegrationEvent\n{\n    public int OrderId { get; }\n\n    public OrderPaymentFailedIntegrationEvent(int orderId) => OrderId = orderId;\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/IntegrationEvents/Events/OrderPaymentSucceededIntegrationEvent.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.IntegrationEvents.Events;\n\npublic record OrderPaymentSucceededIntegrationEvent : IntegrationEvent\n{\n    public int OrderId { get; }\n\n    public OrderPaymentSucceededIntegrationEvent(int orderId) => OrderId = orderId;\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/IntegrationEvents/Events/OrderStartedIntegrationEvent.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.IntegrationEvents.Events;\n\n// Integration Events notes:\n// An Event is “something that has happened in the past”, therefore its name has to be\n// An Integration Event is an event that can cause side effects to other microservices, Bounded-Contexts or external systems.\npublic record OrderStartedIntegrationEvent : IntegrationEvent\n{\n    public string UserId { get; init; }\n\n    public OrderStartedIntegrationEvent(string userId)\n        => UserId = userId;\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.IntegrationEvents.Events;\n\npublic record OrderStatusChangedToAwaitingValidationIntegrationEvent : IntegrationEvent\n{\n    public int OrderId { get; }\n    public OrderStatus OrderStatus { get; }\n    public string BuyerName { get; }\n    public string BuyerIdentityGuid { get; }\n    public IEnumerable<OrderStockItem> OrderStockItems { get; }\n\n    public OrderStatusChangedToAwaitingValidationIntegrationEvent(\n        int orderId, OrderStatus orderStatus, string buyerName, string buyerIdentityGuid,\n        IEnumerable<OrderStockItem> orderStockItems)\n    {\n        OrderId = orderId;\n        OrderStockItems = orderStockItems;\n        OrderStatus = orderStatus;\n        BuyerName = buyerName;\n        BuyerIdentityGuid = buyerIdentityGuid;\n    }\n}\n\npublic record OrderStockItem\n{\n    public int ProductId { get; }\n    public int Units { get; }\n\n    public OrderStockItem(int productId, int units)\n    {\n        ProductId = productId;\n        Units = units;\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToCancelledIntegrationEvent.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.IntegrationEvents.Events;\n\npublic record OrderStatusChangedToCancelledIntegrationEvent : IntegrationEvent\n{\n    public int OrderId { get; }\n    public OrderStatus OrderStatus { get; }\n    public string BuyerName { get; }\n    public string BuyerIdentityGuid { get; }\n\n    public OrderStatusChangedToCancelledIntegrationEvent\n        (int orderId, OrderStatus orderStatus, string buyerName, string buyerIdentityGuid)\n    {\n        OrderId = orderId;\n        OrderStatus = orderStatus;\n        BuyerName = buyerName;\n        BuyerIdentityGuid = buyerIdentityGuid;\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToPaidIntegrationEvent.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.IntegrationEvents.Events;\n\npublic record OrderStatusChangedToPaidIntegrationEvent : IntegrationEvent\n{\n    public int OrderId { get; }\n    public OrderStatus OrderStatus { get; }\n    public string BuyerName { get; }\n    public string BuyerIdentityGuid { get; }\n    public IEnumerable<OrderStockItem> OrderStockItems { get; }\n\n    public OrderStatusChangedToPaidIntegrationEvent(int orderId,\n        OrderStatus orderStatus, string buyerName, string buyerIdentityGuid,\n        IEnumerable<OrderStockItem> orderStockItems)\n    {\n        OrderId = orderId;\n        OrderStockItems = orderStockItems;\n        OrderStatus = orderStatus;\n        BuyerName = buyerName;\n        BuyerIdentityGuid = buyerIdentityGuid;\n    }\n}\n\n"
  },
  {
    "path": "src/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToShippedIntegrationEvent.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.IntegrationEvents.Events;\n\npublic record OrderStatusChangedToShippedIntegrationEvent : IntegrationEvent\n{\n    public int OrderId { get; }\n    public OrderStatus OrderStatus { get; }\n    public string BuyerName { get; }\n    public string BuyerIdentityGuid { get; }\n\n    public OrderStatusChangedToShippedIntegrationEvent(\n        int orderId, OrderStatus orderStatus, string buyerName, string buyerIdentityGuid)\n    {\n        OrderId = orderId;\n        OrderStatus = orderStatus;\n        BuyerName = buyerName;\n        BuyerIdentityGuid = buyerIdentityGuid;\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedToStockConfirmedIntegrationEvent.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.IntegrationEvents.Events;\n\npublic record OrderStatusChangedToStockConfirmedIntegrationEvent : IntegrationEvent\n{\n    public int OrderId { get; }\n    public OrderStatus OrderStatus { get; }\n    public string BuyerName { get; }\n    public string BuyerIdentityGuid { get; }\n\n    public OrderStatusChangedToStockConfirmedIntegrationEvent(\n        int orderId, OrderStatus orderStatus, string buyerName, string buyerIdentityGuid)\n    {\n        OrderId = orderId;\n        OrderStatus = orderStatus;\n        BuyerName = buyerName;\n        BuyerIdentityGuid = buyerIdentityGuid;\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/IntegrationEvents/Events/OrderStatusChangedTosubmittedIntegrationEvent.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.IntegrationEvents.Events;\n\npublic record OrderStatusChangedToSubmittedIntegrationEvent : IntegrationEvent\n{\n    public int OrderId { get; }\n    public OrderStatus OrderStatus { get; }\n    public string BuyerName { get; }\n    public string BuyerIdentityGuid { get; }\n\n    public OrderStatusChangedToSubmittedIntegrationEvent(\n        int orderId, OrderStatus orderStatus, string buyerName, string buyerIdentityGuid)\n    {\n        OrderId = orderId;\n        OrderStatus = orderStatus;\n        BuyerName = buyerName;\n        BuyerIdentityGuid = buyerIdentityGuid;\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/IntegrationEvents/Events/OrderStockConfirmedIntegrationEvent.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.IntegrationEvents.Events;\n\npublic record OrderStockConfirmedIntegrationEvent : IntegrationEvent\n{\n    public int OrderId { get; }\n\n    public OrderStockConfirmedIntegrationEvent(int orderId) => OrderId = orderId;\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/IntegrationEvents/Events/OrderStockRejectedIntegrationEvent.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.IntegrationEvents.Events;\n\npublic record OrderStockRejectedIntegrationEvent : IntegrationEvent\n{\n    public int OrderId { get; }\n\n    public List<ConfirmedOrderStockItem> OrderStockItems { get; }\n\n    public OrderStockRejectedIntegrationEvent(int orderId,\n        List<ConfirmedOrderStockItem> orderStockItems)\n    {\n        OrderId = orderId;\n        OrderStockItems = orderStockItems;\n    }\n}\n\npublic record ConfirmedOrderStockItem\n{\n    public int ProductId { get; }\n    public bool HasStock { get; }\n\n    public ConfirmedOrderStockItem(int productId, bool hasStock)\n    {\n        ProductId = productId;\n        HasStock = hasStock;\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/IntegrationEvents/IOrderingIntegrationEventService.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.IntegrationEvents;\n\npublic interface IOrderingIntegrationEventService\n{\n    Task PublishEventsThroughEventBusAsync(Guid transactionId);\n    Task AddAndSaveEventAsync(IntegrationEvent evt);\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/IntegrationEvents/OrderingIntegrationEventService.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.IntegrationEvents;\n\npublic class OrderingIntegrationEventService(IEventBus eventBus,\n    OrderingContext orderingContext,\n    IIntegrationEventLogService integrationEventLogService,\n    ILogger<OrderingIntegrationEventService> logger) : IOrderingIntegrationEventService\n{\n    private readonly IEventBus _eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus));\n    private readonly OrderingContext _orderingContext = orderingContext ?? throw new ArgumentNullException(nameof(orderingContext));\n    private readonly IIntegrationEventLogService _eventLogService = integrationEventLogService ?? throw new ArgumentNullException(nameof(integrationEventLogService));\n    private readonly ILogger<OrderingIntegrationEventService> _logger = logger ?? throw new ArgumentNullException(nameof(logger));\n\n    public async Task PublishEventsThroughEventBusAsync(Guid transactionId)\n    {\n        var pendingLogEvents = await _eventLogService.RetrieveEventLogsPendingToPublishAsync(transactionId);\n\n        foreach (var logEvt in pendingLogEvents)\n        {\n            _logger.LogInformation(\"Publishing integration event: {IntegrationEventId} - ({@IntegrationEvent})\", logEvt.EventId, logEvt.IntegrationEvent);\n\n            try\n            {\n                await _eventLogService.MarkEventAsInProgressAsync(logEvt.EventId);\n                await _eventBus.PublishAsync(logEvt.IntegrationEvent);\n                await _eventLogService.MarkEventAsPublishedAsync(logEvt.EventId);\n            }\n            catch (Exception ex)\n            {\n                _logger.LogError(ex, \"Error publishing integration event: {IntegrationEventId}\", logEvt.EventId);\n\n                await _eventLogService.MarkEventAsFailedAsync(logEvt.EventId);\n            }\n        }\n    }\n\n    public async Task AddAndSaveEventAsync(IntegrationEvent evt)\n    {\n        _logger.LogInformation(\"Enqueuing integration event {IntegrationEventId} to repository ({@IntegrationEvent})\", evt.Id, evt);\n\n        await _eventLogService.SaveEventAsync(evt, _orderingContext.GetCurrentTransaction());\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/Models/BasketItem.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Models;\n\npublic class BasketItem\n{\n    public string Id { get; init; }\n    public int ProductId { get; init; }\n    public string ProductName { get; init; }\n    public decimal UnitPrice { get; init; }\n    public decimal OldUnitPrice { get; init; }\n    public int Quantity { get; init; }\n    public string PictureUrl { get; init; }\n}\n\n"
  },
  {
    "path": "src/Ordering.API/Application/Models/CustomerBasket.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Models;\n\npublic class CustomerBasket\n{\n    public string BuyerId { get; set; }\n    public List<BasketItem> Items { get; set; }\n\n    public CustomerBasket(string buyerId, List<BasketItem> items)\n    {\n        BuyerId = buyerId;\n        Items = items;\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/Queries/IOrderQueries.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Queries;\n\npublic interface IOrderQueries\n{\n    Task<Order> GetOrderAsync(int id);\n\n    Task<IEnumerable<OrderSummary>> GetOrdersFromUserAsync(string userId);\n\n    Task<IEnumerable<CardType>> GetCardTypesAsync();\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/Queries/OrderQueries.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Queries;\n\npublic class OrderQueries(OrderingContext context)\n    : IOrderQueries\n{\n    public async Task<Order> GetOrderAsync(int id)\n    {\n        var order = await context.Orders\n            .Include(o => o.OrderItems)\n            .FirstOrDefaultAsync(o => o.Id == id);\n      \n        if (order is null)\n            throw new KeyNotFoundException();\n\n        return new Order\n        {\n            OrderNumber = order.Id,\n            Date = order.OrderDate,\n            Description = order.Description,\n            City = order.Address.City,\n            Country = order.Address.Country,\n            State = order.Address.State,\n            Street = order.Address.Street,\n            Zipcode = order.Address.ZipCode,\n            Status = order.OrderStatus.ToString(),\n            Total = order.GetTotal(),\n            OrderItems = order.OrderItems.Select(oi => new Orderitem\n            {\n                ProductName = oi.ProductName,\n                Units = oi.Units,\n                UnitPrice = (double)oi.UnitPrice,\n                PictureUrl = oi.PictureUrl\n            }).ToList()\n        };\n    }\n\n    public async Task<IEnumerable<OrderSummary>> GetOrdersFromUserAsync(string userId)\n    {\n        return await context.Orders\n            .Where(o => o.Buyer.IdentityGuid == userId)  \n            .Select(o => new OrderSummary\n            {\n                OrderNumber = o.Id,\n                Date = o.OrderDate,\n                Status = o.OrderStatus.ToString(),\n                Total =(double) o.OrderItems.Sum(oi => oi.UnitPrice* oi.Units)\n            })\n            .ToListAsync();\n    } \n    \n    public async Task<IEnumerable<CardType>> GetCardTypesAsync() => \n        await context.CardTypes.Select(c=> new CardType { Id = c.Id, Name = c.Name }).ToListAsync();\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/Queries/OrderViewModel.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Queries;\n\npublic record Orderitem\n{\n    public string ProductName { get; init; }\n    public int Units { get; init; }\n    public double UnitPrice { get; init; }\n    public string PictureUrl { get; init; }\n}\n\npublic record Order\n{\n    public int OrderNumber { get; init; }\n    public DateTime Date { get; init; }\n    public string Status { get; init; }\n    public string Description { get; init; }\n    public string Street { get; init; }\n    public string City { get; init; }\n    public string State { get; init; }\n    public string Zipcode { get; init; }\n    public string Country { get; init; }\n    public List<Orderitem> OrderItems { get; set; }\n    public decimal Total { get; set; }\n}\n\npublic record OrderSummary\n{\n    public int OrderNumber { get; init; }\n    public DateTime Date { get; init; }\n    public string Status { get; init; }\n    public double Total { get; init; }\n}\n\npublic record CardType\n{\n    public int Id { get; init; }\n    public string Name { get; init; }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/Validations/CancelOrderCommandValidator.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Validations;\n\npublic class CancelOrderCommandValidator : AbstractValidator<CancelOrderCommand>\n{\n    public CancelOrderCommandValidator(ILogger<CancelOrderCommandValidator> logger)\n    {\n        RuleFor(order => order.OrderNumber).NotEmpty().WithMessage(\"No orderId found\");\n\n        if (logger.IsEnabled(LogLevel.Trace))\n        {\n            logger.LogTrace(\"INSTANCE CREATED - {ClassName}\", GetType().Name);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/Validations/CreateOrderCommandValidator.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Validations;\npublic class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>\n{\n    public CreateOrderCommandValidator(ILogger<CreateOrderCommandValidator> logger)\n    {\n        RuleFor(command => command.City).NotEmpty();\n        RuleFor(command => command.Street).NotEmpty();\n        RuleFor(command => command.State).NotEmpty();\n        RuleFor(command => command.Country).NotEmpty();\n        RuleFor(command => command.ZipCode).NotEmpty();\n        RuleFor(command => command.CardNumber).NotEmpty().Length(12, 19);\n        RuleFor(command => command.CardHolderName).NotEmpty();\n        RuleFor(command => command.CardExpiration).NotEmpty().Must(BeValidExpirationDate).WithMessage(\"Please specify a valid card expiration date\");\n        RuleFor(command => command.CardSecurityNumber).NotEmpty().Length(3);\n        RuleFor(command => command.CardTypeId).NotEmpty();\n        RuleFor(command => command.OrderItems).Must(ContainOrderItems).WithMessage(\"No order items found\");\n\n        if (logger.IsEnabled(LogLevel.Trace))\n        {\n            logger.LogTrace(\"INSTANCE CREATED - {ClassName}\", GetType().Name);\n        }\n    }\n\n    private bool BeValidExpirationDate(DateTime dateTime)\n    {\n        return dateTime >= DateTime.UtcNow;\n    }\n\n    private bool ContainOrderItems(IEnumerable<OrderItemDTO> orderItems)\n    {\n        return orderItems.Any();\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/Validations/IdentifiedCommandValidator.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Validations;\n\npublic class IdentifiedCommandValidator : AbstractValidator<IdentifiedCommand<CreateOrderCommand, bool>>\n{\n    public IdentifiedCommandValidator(ILogger<IdentifiedCommandValidator> logger)\n    {\n        RuleFor(command => command.Id).NotEmpty();\n\n        if (logger.IsEnabled(LogLevel.Trace))\n        {\n            logger.LogTrace(\"INSTANCE CREATED - {ClassName}\", GetType().Name);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Application/Validations/ShipOrderCommandValidator.cs",
    "content": "﻿namespace eShop.Ordering.API.Application.Validations;\n\npublic class ShipOrderCommandValidator : AbstractValidator<ShipOrderCommand>\n{\n    public ShipOrderCommandValidator(ILogger<ShipOrderCommandValidator> logger)\n    {\n        RuleFor(order => order.OrderNumber).NotEmpty().WithMessage(\"No orderId found\");\n\n        if (logger.IsEnabled(LogLevel.Trace))\n        {\n            logger.LogTrace(\"INSTANCE CREATED - {ClassName}\", GetType().Name);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Extensions/BasketItemExtensions.cs",
    "content": "﻿namespace eShop.Ordering.API.Extensions;\n\npublic static class BasketItemExtensions\n{\n    public static IEnumerable<OrderItemDTO> ToOrderItemsDTO(this IEnumerable<BasketItem> basketItems)\n    {\n        foreach (var item in basketItems)\n        {\n            yield return item.ToOrderItemDTO();\n        }\n    }\n\n    public static OrderItemDTO ToOrderItemDTO(this BasketItem item)\n    {\n        return new OrderItemDTO()\n        {\n            ProductId = item.ProductId,\n            ProductName = item.ProductName,\n            PictureUrl = item.PictureUrl,\n            UnitPrice = item.UnitPrice,\n            Units = item.Quantity\n        };\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Extensions/Extensions.cs",
    "content": "﻿using FluentValidation;\n\ninternal static class Extensions\n{\n    public static void AddApplicationServices(this IHostApplicationBuilder builder)\n    {\n        var services = builder.Services;\n        \n        // Add the authentication services to DI\n        builder.AddDefaultAuthentication();\n\n        // Pooling is disabled because of the following error:\n        // Unhandled exception. System.InvalidOperationException:\n        // The DbContext of type 'OrderingContext' cannot be pooled because it does not have a public constructor accepting a single parameter of type DbContextOptions or has more than one constructor.\n        services.AddDbContext<OrderingContext>(options =>\n        {\n            options.UseNpgsql(builder.Configuration.GetConnectionString(\"orderingdb\"));\n        });\n        builder.EnrichNpgsqlDbContext<OrderingContext>();\n\n        services.AddMigration<OrderingContext, OrderingContextSeed>();\n\n        // Add the integration services that consume the DbContext\n        services.AddTransient<IIntegrationEventLogService, IntegrationEventLogService<OrderingContext>>();\n\n        services.AddTransient<IOrderingIntegrationEventService, OrderingIntegrationEventService>();\n\n        builder.AddRabbitMqEventBus(\"eventbus\")\n               .AddEventBusSubscriptions();\n\n        services.AddHttpContextAccessor();\n        services.AddTransient<IIdentityService, IdentityService>();\n\n        // Configure mediatR\n        services.AddMediatR(cfg =>\n        {\n            cfg.RegisterServicesFromAssemblyContaining(typeof(Program));\n\n            cfg.AddOpenBehavior(typeof(LoggingBehavior<,>));\n            cfg.AddOpenBehavior(typeof(ValidatorBehavior<,>));\n            cfg.AddOpenBehavior(typeof(TransactionBehavior<,>));\n        });\n\n        // Register the command validators for the validator behavior (validators based on FluentValidation library)\n        services.AddValidatorsFromAssemblyContaining<CancelOrderCommandValidator>();\n\n        services.AddScoped<IOrderQueries, OrderQueries>();\n        services.AddScoped<IBuyerRepository, BuyerRepository>();\n        services.AddScoped<IOrderRepository, OrderRepository>();\n        services.AddScoped<IRequestManager, RequestManager>();\n    }\n\n    private static void AddEventBusSubscriptions(this IEventBusBuilder eventBus)\n    {\n        eventBus.AddSubscription<GracePeriodConfirmedIntegrationEvent, GracePeriodConfirmedIntegrationEventHandler>();\n        eventBus.AddSubscription<OrderStockConfirmedIntegrationEvent, OrderStockConfirmedIntegrationEventHandler>();\n        eventBus.AddSubscription<OrderStockRejectedIntegrationEvent, OrderStockRejectedIntegrationEventHandler>();\n        eventBus.AddSubscription<OrderPaymentFailedIntegrationEvent, OrderPaymentFailedIntegrationEventHandler>();\n        eventBus.AddSubscription<OrderPaymentSucceededIntegrationEvent, OrderPaymentSucceededIntegrationEventHandler>();\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Extensions/LinqSelectExtensions.cs",
    "content": "﻿namespace eShop.Ordering.API.Extensions;\n\npublic static class LinqSelectExtensions\n{\n    public static IEnumerable<SelectTryResult<TSource, TResult>> SelectTry<TSource, TResult>(this IEnumerable<TSource> enumerable, Func<TSource, TResult> selector)\n    {\n        foreach (TSource element in enumerable)\n        {\n            SelectTryResult<TSource, TResult> returnedValue;\n            try\n            {\n                returnedValue = new SelectTryResult<TSource, TResult>(element, selector(element), null);\n            }\n            catch (Exception ex)\n            {\n                returnedValue = new SelectTryResult<TSource, TResult>(element, default, ex);\n            }\n            yield return returnedValue;\n        }\n    }\n\n    public static IEnumerable<TResult> OnCaughtException<TSource, TResult>(this IEnumerable<SelectTryResult<TSource, TResult>> enumerable, Func<Exception, TResult> exceptionHandler)\n    {\n        return enumerable.Select(x => x.CaughtException == null ? x.Result : exceptionHandler(x.CaughtException));\n    }\n\n    public static IEnumerable<TResult> OnCaughtException<TSource, TResult>(this IEnumerable<SelectTryResult<TSource, TResult>> enumerable, Func<TSource, Exception, TResult> exceptionHandler)\n    {\n        return enumerable.Select(x => x.CaughtException == null ? x.Result : exceptionHandler(x.Source, x.CaughtException));\n    }\n\n    public class SelectTryResult<TSource, TResult>\n    {\n        internal SelectTryResult(TSource source, TResult result, Exception exception)\n        {\n            Source = source;\n            Result = result;\n            CaughtException = exception;\n        }\n\n        public TSource Source { get; private set; }\n        public TResult Result { get; private set; }\n        public Exception CaughtException { get; private set; }\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Extensions/OrderingApiTrace.cs",
    "content": "﻿namespace eShop.Ordering.API.Extensions;\n\ninternal static partial class OrderingApiTrace\n{\n    [LoggerMessage(EventId = 1, EventName = \"OrderStatusUpdated\", Level = LogLevel.Trace, Message = \"Order with Id: {OrderId} has been successfully updated to status {Status}\")]\n    public static partial void LogOrderStatusUpdated(ILogger logger, int orderId, OrderStatus status);\n\n    [LoggerMessage(EventId = 2, EventName = \"PaymentMethodUpdated\", Level = LogLevel.Trace, Message = \"Order with Id: {OrderId} has been successfully updated with a payment method {PaymentMethod} ({Id})\")]\n    public static partial void LogOrderPaymentMethodUpdated(ILogger logger, int orderId, string paymentMethod, int id);\n\n    [LoggerMessage(EventId = 3, EventName = \"BuyerAndPaymentValidatedOrUpdated\", Level = LogLevel.Trace, Message = \"Buyer {BuyerId} and related payment method were validated or updated for order Id: {OrderId}.\")]\n    public static partial void LogOrderBuyerAndPaymentValidatedOrUpdated(ILogger logger, int buyerId, int orderId);\n}\n"
  },
  {
    "path": "src/Ordering.API/GlobalUsings.cs",
    "content": "﻿global using Asp.Versioning.Conventions;\nglobal using System.Runtime.Serialization;\nglobal using FluentValidation;\nglobal using MediatR;\nglobal using Microsoft.AspNetCore.Mvc;\nglobal using Microsoft.EntityFrameworkCore;\nglobal using eShop.EventBus.Abstractions;\nglobal using eShop.EventBus.Events;\nglobal using eShop.EventBus.Extensions;\nglobal using eShop.IntegrationEventLogEF.Services;\nglobal using eShop.Ordering.API.Application.Behaviors;\nglobal using eShop.Ordering.API.Application.Commands;\nglobal using eShop.Ordering.API.Application.IntegrationEvents;\nglobal using eShop.Ordering.API.Application.IntegrationEvents.EventHandling;\nglobal using eShop.Ordering.API.Application.IntegrationEvents.Events;\nglobal using eShop.Ordering.API.Application.Models;\nglobal using eShop.Ordering.API.Application.Queries;\nglobal using eShop.Ordering.API.Application.Validations;\nglobal using eShop.Ordering.API.Extensions;\nglobal using eShop.Ordering.API.Infrastructure;\nglobal using eShop.Ordering.API.Infrastructure.Services;\nglobal using eShop.Ordering.Domain.AggregatesModel.BuyerAggregate;\nglobal using eShop.Ordering.Domain.AggregatesModel.OrderAggregate;\nglobal using eShop.Ordering.Domain.Events;\nglobal using eShop.Ordering.Domain.Exceptions;\nglobal using eShop.Ordering.Domain.SeedWork;\nglobal using eShop.Ordering.Infrastructure;\nglobal using eShop.Ordering.Infrastructure.Idempotency;\nglobal using eShop.Ordering.Infrastructure.Repositories;\nglobal using eShop.ServiceDefaults;\n"
  },
  {
    "path": "src/Ordering.API/Infrastructure/OrderingContextSeed.cs",
    "content": "﻿namespace eShop.Ordering.API.Infrastructure;\n\nusing eShop.Ordering.Domain.AggregatesModel.BuyerAggregate;\n\npublic class OrderingContextSeed: IDbSeeder<OrderingContext>\n{\n    public async Task SeedAsync(OrderingContext context)\n    {\n        if (!context.CardTypes.Any())\n        {\n            context.CardTypes.AddRange(GetPredefinedCardTypes());\n\n            await context.SaveChangesAsync();\n        }\n\n        await context.SaveChangesAsync();\n    }\n\n    private static IEnumerable<CardType> GetPredefinedCardTypes()\n    {\n        yield return new CardType { Id = 1, Name = \"Amex\" };\n        yield return new CardType { Id = 2, Name = \"Visa\" };\n        yield return new CardType { Id = 3, Name = \"MasterCard\" };\n    }\n}\n"
  },
  {
    "path": "src/Ordering.API/Infrastructure/Services/IIdentityService.cs",
    "content": "﻿namespace eShop.Ordering.API.Infrastructure.Services;\n\npublic interface IIdentityService\n{\n    string GetUserIdentity();\n\n    string GetUserName();\n}\n\n"
  },
  {
    "path": "src/Ordering.API/Infrastructure/Services/IdentityService.cs",
    "content": "﻿namespace eShop.Ordering.API.Infrastructure.Services;\n\npublic class IdentityService(IHttpContextAccessor context) : IIdentityService\n{\n    public string GetUserIdentity()\n        => context.HttpContext?.User.FindFirst(\"sub\")?.Value;\n\n    public string GetUserName()\n        => context.HttpContext?.User.Identity?.Name;\n}\n"
  },
  {
    "path": "src/Ordering.API/Ordering.API.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <RootNamespace>eShop.Ordering.API</RootNamespace>\n    <UserSecretsId>7161b768-033d-41c7-bc5d-37528275e1f3</UserSecretsId>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <Compile Include=\"..\\Shared\\ActivityExtensions.cs\" Link=\"Extensions\\ActivityExtensions.cs\" />\n    <Compile Include=\"..\\Shared\\MigrateDbContextExtensions.cs\" Link=\"Extensions\\MigrateDbContextExtensions.cs\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <InternalsVisibleTo Include=\"Ordering.FunctionalTests\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\EventBusRabbitMQ\\EventBusRabbitMQ.csproj\" />\n    <ProjectReference Include=\"..\\IntegrationEventLogEF\\IntegrationEventLogEF.csproj\" />\n    <ProjectReference Include=\"..\\eShop.ServiceDefaults\\eShop.ServiceDefaults.csproj\" />\n    <ProjectReference Include=\"..\\Ordering.Domain\\Ordering.Domain.csproj\" />\n    <ProjectReference Include=\"..\\Ordering.Infrastructure\\Ordering.Infrastructure.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Asp.Versioning.Http\" />\n    <PackageReference Include=\"Aspire.Npgsql.EntityFrameworkCore.PostgreSQL\" />\n    <PackageReference Include=\"FluentValidation\" />\n    <PackageReference Include=\"FluentValidation.DependencyInjectionExtensions\" />\n    <PackageReference Include=\"Microsoft.EntityFrameworkCore.Tools\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <Content Update=\"appsettings.Development.json\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n      <ExcludeFromSingleFile>true</ExcludeFromSingleFile>\n      <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>\n    </Content>\n  </ItemGroup>\n\n</Project>"
  },
  {
    "path": "src/Ordering.API/Program.Testing.cs",
    "content": "﻿// Require a public Program class to implement the\n// fixture for the WebApplicationFactory in the\n// integration tests. Using IVT is not sufficient\n// in this case, because the accessibility of the\n// `Program` type is checked.\npublic partial class Program { }\n"
  },
  {
    "path": "src/Ordering.API/Program.cs",
    "content": "﻿var builder = WebApplication.CreateBuilder(args);\n\nbuilder.AddServiceDefaults();\nbuilder.AddApplicationServices();\nbuilder.Services.AddProblemDetails();\n\nvar withApiVersioning = builder.Services.AddApiVersioning();\n\nbuilder.AddDefaultOpenApi(withApiVersioning);\n\nvar app = builder.Build();\n\napp.MapDefaultEndpoints();\n\nvar orders = app.NewVersionedApi(\"Orders\");\n\norders.MapOrdersApiV1()\n      .RequireAuthorization();\n\napp.UseDefaultOpenApi();\napp.Run();\n"
  },
  {
    "path": "src/Ordering.API/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"http\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"applicationUrl\": \"http://localhost:5224/\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "src/Ordering.API/appsettings.Development.json",
    "content": "{\n  \"ConnectionStrings\": {\n    \"OrderingDB\": \"Host=localhost;Database=OrderingDB;Username=postgres;Password=yourWeak(!)Password\"\n  }\n}"
  },
  {
    "path": "src/Ordering.API/appsettings.json",
    "content": "﻿{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  },\n  \"AllowedHosts\": \"*\",\n  \"OpenApi\": {\n    \"Endpoint\": {\n      \"Name\": \"Ordering.API V1\"\n    },\n    \"Document\": {\n      \"Description\": \"The Ordering Service HTTP API\",\n      \"Title\": \"eShop - Ordering HTTP API\",\n      \"Version\": \"v1\"\n    },\n    \"Auth\": {\n      \"ClientId\": \"orderingswaggerui\",\n      \"AppName\": \"Ordering Swagger UI\"\n    }\n  },\n  \"ConnectionStrings\": {\n    \"EventBus\": \"amqp://localhost\"\n  },\n  \"EventBus\": {\n    \"SubscriptionClientName\": \"Ordering\"\n  },\n  \"Identity\": {\n    \"Audience\": \"orders\",\n    \"Scopes\": {\n      \"orders\": \"Ordering API\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/Ordering.Domain/AggregatesModel/BuyerAggregate/Buyer.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace eShop.Ordering.Domain.AggregatesModel.BuyerAggregate;\n\npublic class Buyer\n    : Entity, IAggregateRoot\n{\n    [Required]\n    public string IdentityGuid { get; private set; }\n\n    public string Name { get; private set; }\n\n    private List<PaymentMethod> _paymentMethods;\n\n    public IEnumerable<PaymentMethod> PaymentMethods => _paymentMethods.AsReadOnly();\n\n    protected Buyer()\n    {\n\n        _paymentMethods = new List<PaymentMethod>();\n    }\n\n    public Buyer(string identity, string name) : this()\n    {\n        IdentityGuid = !string.IsNullOrWhiteSpace(identity) ? identity : throw new ArgumentNullException(nameof(identity));\n        Name = !string.IsNullOrWhiteSpace(name) ? name : throw new ArgumentNullException(nameof(name));\n    }\n\n    public PaymentMethod VerifyOrAddPaymentMethod(\n        int cardTypeId, string alias, string cardNumber,\n        string securityNumber, string cardHolderName, DateTime expiration, int orderId)\n    {\n        var existingPayment = _paymentMethods\n            .SingleOrDefault(p => p.IsEqualTo(cardTypeId, cardNumber, expiration));\n\n        if (existingPayment != null)\n        {\n            AddDomainEvent(new BuyerAndPaymentMethodVerifiedDomainEvent(this, existingPayment, orderId));\n\n            return existingPayment;\n        }\n\n        var payment = new PaymentMethod(cardTypeId, alias, cardNumber, securityNumber, cardHolderName, expiration);\n\n        _paymentMethods.Add(payment);\n\n        AddDomainEvent(new BuyerAndPaymentMethodVerifiedDomainEvent(this, payment, orderId));\n\n        return payment;\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Domain/AggregatesModel/BuyerAggregate/CardType.cs",
    "content": "﻿namespace eShop.Ordering.Domain.AggregatesModel.BuyerAggregate;\n\npublic sealed class CardType\n{\n    public int Id { get; init; }\n    public required string Name { get; init; }\n}\n"
  },
  {
    "path": "src/Ordering.Domain/AggregatesModel/BuyerAggregate/IBuyerRepository.cs",
    "content": "﻿namespace eShop.Ordering.Domain.AggregatesModel.BuyerAggregate;\n\n//This is just the RepositoryContracts or Interface defined at the Domain Layer\n//as requisite for the Buyer Aggregate\n\npublic interface IBuyerRepository : IRepository<Buyer>\n{\n    Buyer Add(Buyer buyer);\n    Buyer Update(Buyer buyer);\n    Task<Buyer> FindAsync(string BuyerIdentityGuid);\n    Task<Buyer> FindByIdAsync(int id);\n}\n\n"
  },
  {
    "path": "src/Ordering.Domain/AggregatesModel/BuyerAggregate/PaymentMethod.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace eShop.Ordering.Domain.AggregatesModel.BuyerAggregate;\n\npublic class PaymentMethod : Entity\n{\n    [Required]\n    private string _alias;\n    [Required]\n    private string _cardNumber;\n    private string _securityNumber;\n    [Required]\n    private string _cardHolderName;\n    private DateTime _expiration;\n\n    private int _cardTypeId;\n    public CardType CardType { get; private set; }\n\n    protected PaymentMethod() { }\n\n    public PaymentMethod(int cardTypeId, string alias, string cardNumber, string securityNumber, string cardHolderName, DateTime expiration)\n    {\n        _cardNumber = !string.IsNullOrWhiteSpace(cardNumber) ? cardNumber : throw new OrderingDomainException(nameof(cardNumber));\n        _securityNumber = !string.IsNullOrWhiteSpace(securityNumber) ? securityNumber : throw new OrderingDomainException(nameof(securityNumber));\n        _cardHolderName = !string.IsNullOrWhiteSpace(cardHolderName) ? cardHolderName : throw new OrderingDomainException(nameof(cardHolderName));\n\n        if (expiration < DateTime.UtcNow)\n        {\n            throw new OrderingDomainException(nameof(expiration));\n        }\n\n        _alias = alias;\n        _expiration = expiration;\n        _cardTypeId = cardTypeId;\n    }\n\n    public bool IsEqualTo(int cardTypeId, string cardNumber, DateTime expiration)\n    {\n        return _cardTypeId == cardTypeId\n            && _cardNumber == cardNumber\n            && _expiration == expiration;\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Domain/AggregatesModel/OrderAggregate/Address.cs",
    "content": "﻿using eShop.Ordering.Domain.SeedWork;\n\nnamespace eShop.Ordering.Domain.AggregatesModel.OrderAggregate;\n\npublic class Address : ValueObject\n{\n    public string Street { get; private set; }\n    public string City { get; private set; }\n    public string State { get; private set; }\n    public string Country { get; private set; }\n    public string ZipCode { get; private set; }\n\n    public Address() { }\n\n    public Address(string street, string city, string state, string country, string zipcode)\n    {\n        Street = street;\n        City = city;\n        State = state;\n        Country = country;\n        ZipCode = zipcode;\n    }\n\n    protected override IEnumerable<object> GetEqualityComponents()\n    {\n        // Using a yield return statement to return each element one at a time\n        yield return Street;\n        yield return City;\n        yield return State;\n        yield return Country;\n        yield return ZipCode;\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Domain/AggregatesModel/OrderAggregate/IOrderRepository.cs",
    "content": "﻿namespace eShop.Ordering.Domain.AggregatesModel.OrderAggregate;\n\n//This is just the RepositoryContracts or Interface defined at the Domain Layer\n//as requisite for the Order Aggregate\n\npublic interface IOrderRepository : IRepository<Order>\n{\n    Order Add(Order order);\n\n    void Update(Order order);\n\n    Task<Order> GetAsync(int orderId);\n}\n"
  },
  {
    "path": "src/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace eShop.Ordering.Domain.AggregatesModel.OrderAggregate;\n\npublic class Order\n    : Entity, IAggregateRoot\n{\n    public DateTime OrderDate { get; private set; }\n\n    // Address is a Value Object pattern example persisted as EF Core 2.0 owned entity\n    [Required]\n    public Address Address { get; private set; }\n\n    public int? BuyerId { get; private set; }\n\n    public Buyer Buyer { get; }\n\n    public OrderStatus OrderStatus { get; private set; }\n    \n    public string Description { get; private set; }\n\n    // Draft orders have this set to true. Currently we don't check anywhere the draft status of an Order, but we could do it if needed\n#pragma warning disable CS0414 // The field 'Order._isDraft' is assigned but its value is never used\n    private bool _isDraft;\n#pragma warning restore CS0414\n\n    // DDD Patterns comment\n    // Using a private collection field, better for DDD Aggregate's encapsulation\n    // so OrderItems cannot be added from \"outside the AggregateRoot\" directly to the collection,\n    // but only through the method OrderAggregateRoot.AddOrderItem() which includes behavior.\n    private readonly List<OrderItem> _orderItems;\n   \n    public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly();\n\n    public int? PaymentId { get; private set; }\n\n    public static Order NewDraft()\n    {\n        var order = new Order\n        {\n            _isDraft = true\n        };\n        return order;\n    }\n\n    protected Order()\n    {\n        _orderItems = new List<OrderItem>();\n        _isDraft = false;\n    }\n\n    public Order(string userId, string userName, Address address, int cardTypeId, string cardNumber, string cardSecurityNumber,\n            string cardHolderName, DateTime cardExpiration, int? buyerId = null, int? paymentMethodId = null) : this()\n    {\n        BuyerId = buyerId;\n        PaymentId = paymentMethodId;\n        OrderStatus = OrderStatus.Submitted;\n        OrderDate = DateTime.UtcNow;\n        Address = address;\n\n        // Add the OrderStarterDomainEvent to the domain events collection \n        // to be raised/dispatched when committing changes into the Database [ After DbContext.SaveChanges() ]\n        AddOrderStartedDomainEvent(userId, userName, cardTypeId, cardNumber,\n                                    cardSecurityNumber, cardHolderName, cardExpiration);\n    }\n\n    // DDD Patterns comment\n    // This Order AggregateRoot's method \"AddOrderItem()\" should be the only way to add Items to the Order,\n    // so any behavior (discounts, etc.) and validations are controlled by the AggregateRoot \n    // in order to maintain consistency between the whole Aggregate. \n    public void AddOrderItem(int productId, string productName, decimal unitPrice, decimal discount, string pictureUrl, int units = 1)\n    {\n        var existingOrderForProduct = _orderItems.SingleOrDefault(o => o.ProductId == productId);\n\n        if (existingOrderForProduct != null)\n        {\n            //if previous line exist modify it with higher discount  and units..\n            if (discount > existingOrderForProduct.Discount)\n            {\n                existingOrderForProduct.SetNewDiscount(discount);\n            }\n\n            existingOrderForProduct.AddUnits(units);\n        }\n        else\n        {\n            //add validated new order item\n            var orderItem = new OrderItem(productId, productName, unitPrice, discount, pictureUrl, units);\n            _orderItems.Add(orderItem);\n        }\n    }\n\n    public void SetPaymentMethodVerified(int buyerId, int paymentId)\n    {\n        BuyerId = buyerId;\n        PaymentId = paymentId;\n    }\n    \n    public void SetAwaitingValidationStatus()\n    {\n        if (OrderStatus == OrderStatus.Submitted)\n        {\n            AddDomainEvent(new OrderStatusChangedToAwaitingValidationDomainEvent(Id, _orderItems));\n            OrderStatus = OrderStatus.AwaitingValidation;\n        }\n    }\n\n    public void SetStockConfirmedStatus()\n    {\n        if (OrderStatus == OrderStatus.AwaitingValidation)\n        {\n            AddDomainEvent(new OrderStatusChangedToStockConfirmedDomainEvent(Id));\n\n            OrderStatus = OrderStatus.StockConfirmed;\n            Description = \"All the items were confirmed with available stock.\";\n        }\n    }\n\n    public void SetPaidStatus()\n    {\n        if (OrderStatus == OrderStatus.StockConfirmed)\n        {\n            AddDomainEvent(new OrderStatusChangedToPaidDomainEvent(Id, OrderItems));\n\n            OrderStatus = OrderStatus.Paid;\n            Description = \"The payment was performed at a simulated \\\"American Bank checking bank account ending on XX35071\\\"\";\n        }\n    }\n\n    public void SetShippedStatus()\n    {\n        if (OrderStatus != OrderStatus.Paid)\n        {\n            StatusChangeException(OrderStatus.Shipped);\n        }\n\n        OrderStatus = OrderStatus.Shipped;\n        Description = \"The order was shipped.\";\n        AddDomainEvent(new OrderShippedDomainEvent(this));\n    }\n\n    public void SetCancelledStatus()\n    {\n        if (OrderStatus == OrderStatus.Paid ||\n            OrderStatus == OrderStatus.Shipped)\n        {\n            StatusChangeException(OrderStatus.Cancelled);\n        }\n\n        OrderStatus = OrderStatus.Cancelled;\n        Description = \"The order was cancelled.\";\n        AddDomainEvent(new OrderCancelledDomainEvent(this));\n    }\n\n    public void SetCancelledStatusWhenStockIsRejected(IEnumerable<int> orderStockRejectedItems)\n    {\n        if (OrderStatus == OrderStatus.AwaitingValidation)\n        {\n            OrderStatus = OrderStatus.Cancelled;\n\n            var itemsStockRejectedProductNames = OrderItems\n                .Where(c => orderStockRejectedItems.Contains(c.ProductId))\n                .Select(c => c.ProductName);\n\n            var itemsStockRejectedDescription = string.Join(\", \", itemsStockRejectedProductNames);\n            Description = $\"The product items don't have stock: ({itemsStockRejectedDescription}).\";\n        }\n    }\n\n    private void AddOrderStartedDomainEvent(string userId, string userName, int cardTypeId, string cardNumber,\n            string cardSecurityNumber, string cardHolderName, DateTime cardExpiration)\n    {\n        var orderStartedDomainEvent = new OrderStartedDomainEvent(this, userId, userName, cardTypeId,\n                                                                    cardNumber, cardSecurityNumber,\n                                                                    cardHolderName, cardExpiration);\n\n        this.AddDomainEvent(orderStartedDomainEvent);\n    }\n\n    private void StatusChangeException(OrderStatus orderStatusToChange)\n    {\n        throw new OrderingDomainException($\"Is not possible to change the order status from {OrderStatus} to {orderStatusToChange}.\");\n    }\n\n    public decimal GetTotal() => _orderItems.Sum(o => o.Units * o.UnitPrice);\n}\n"
  },
  {
    "path": "src/Ordering.Domain/AggregatesModel/OrderAggregate/OrderItem.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace eShop.Ordering.Domain.AggregatesModel.OrderAggregate;\n\npublic class OrderItem\n    : Entity\n{\n    [Required]\n    public string ProductName { get; private set; }\n    \n    public string PictureUrl { get; private set;}\n    \n    public decimal UnitPrice { get; private set;}\n    \n    public decimal Discount { get; private set; }\n    \n    public int Units { get; private set; }\n\n    public int ProductId { get; private set; }\n\n    protected OrderItem() { }\n\n    public OrderItem(int productId, string productName, decimal unitPrice, decimal discount, string pictureUrl, int units = 1)\n    {\n        if (units <= 0)\n        {\n            throw new OrderingDomainException(\"Invalid number of units\");\n        }\n\n        if ((unitPrice * units) < discount)\n        {\n            throw new OrderingDomainException(\"The total of order item is lower than applied discount\");\n        }\n\n        ProductId = productId;\n\n        ProductName = productName;\n        UnitPrice = unitPrice;\n        Discount = discount;\n        Units = units;\n        PictureUrl = pictureUrl;\n    }\n    \n    public void SetNewDiscount(decimal discount)\n    {\n        if (discount < 0)\n        {\n            throw new OrderingDomainException(\"Discount is not valid\");\n        }\n\n        Discount = discount;\n    }\n\n    public void AddUnits(int units)\n    {\n        if (units < 0)\n        {\n            throw new OrderingDomainException(\"Invalid units\");\n        }\n\n        Units += units;\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs",
    "content": "﻿using System.Text.Json.Serialization;\n\nnamespace eShop.Ordering.Domain.AggregatesModel.OrderAggregate;\n\n[JsonConverter(typeof(JsonStringEnumConverter))]\npublic enum OrderStatus\n{\n    Submitted = 1,\n    AwaitingValidation = 2,\n    StockConfirmed = 3,\n    Paid = 4,\n    Shipped = 5,\n    Cancelled = 6\n}\n"
  },
  {
    "path": "src/Ordering.Domain/Events/BuyerPaymentMethodVerifiedDomainEvent.cs",
    "content": "﻿namespace eShop.Ordering.Domain.Events;\n\npublic class BuyerAndPaymentMethodVerifiedDomainEvent\n    : INotification\n{\n    public Buyer Buyer { get; private set; }\n    public PaymentMethod Payment { get; private set; }\n    public int OrderId { get; private set; }\n\n    public BuyerAndPaymentMethodVerifiedDomainEvent(Buyer buyer, PaymentMethod payment, int orderId)\n    {\n        Buyer = buyer;\n        Payment = payment;\n        OrderId = orderId;\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Domain/Events/OrderCancelledDomainEvent.cs",
    "content": "﻿namespace eShop.Ordering.Domain.Events;\n\npublic class OrderCancelledDomainEvent : INotification\n{\n    public Order Order { get; }\n\n    public OrderCancelledDomainEvent(Order order)\n    {\n        Order = order;\n    }\n}\n\n"
  },
  {
    "path": "src/Ordering.Domain/Events/OrderShippedDomainEvent.cs",
    "content": "﻿namespace eShop.Ordering.Domain.Events;\n\npublic class OrderShippedDomainEvent : INotification\n{\n    public Order Order { get; }\n\n    public OrderShippedDomainEvent(Order order)\n    {\n        Order = order;\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Domain/Events/OrderStartedDomainEvent.cs",
    "content": "﻿\nnamespace eShop.Ordering.Domain.Events;\n\n/// <summary>\n/// Event used when an order is created\n/// </summary>\npublic record class OrderStartedDomainEvent(\n    Order Order, \n    string UserId,\n    string UserName,\n    int CardTypeId,\n    string CardNumber,\n    string CardSecurityNumber,\n    string CardHolderName,\n    DateTime CardExpiration) : INotification;\n"
  },
  {
    "path": "src/Ordering.Domain/Events/OrderStatusChangedToAwaitingValidationDomainEvent.cs",
    "content": "﻿namespace eShop.Ordering.Domain.Events;\n\n/// <summary>\n/// Event used when the grace period order is confirmed\n/// </summary>\npublic class OrderStatusChangedToAwaitingValidationDomainEvent\n        : INotification\n{\n    public int OrderId { get; }\n    public IEnumerable<OrderItem> OrderItems { get; }\n\n    public OrderStatusChangedToAwaitingValidationDomainEvent(int orderId,\n        IEnumerable<OrderItem> orderItems)\n    {\n        OrderId = orderId;\n        OrderItems = orderItems;\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Domain/Events/OrderStatusChangedToPaidDomainEvent.cs",
    "content": "namespace eShop.Ordering.Domain.Events;\n\n/// <summary>\n/// Event used when the order is paid\n/// </summary>\npublic class OrderStatusChangedToPaidDomainEvent\n    : INotification\n{\n    public int OrderId { get; }\n    public IEnumerable<OrderItem> OrderItems { get; }\n\n    public OrderStatusChangedToPaidDomainEvent(int orderId,\n        IEnumerable<OrderItem> orderItems)\n    {\n        OrderId = orderId;\n        OrderItems = orderItems;\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Domain/Events/OrderStatusChangedToStockConfirmedDomainEvent.cs",
    "content": "﻿namespace eShop.Ordering.Domain.Events;\n\n/// <summary>\n/// Event used when the order stock items are confirmed\n/// </summary>\npublic class OrderStatusChangedToStockConfirmedDomainEvent\n    : INotification\n{\n    public int OrderId { get; }\n\n    public OrderStatusChangedToStockConfirmedDomainEvent(int orderId)\n        => OrderId = orderId;\n}\n"
  },
  {
    "path": "src/Ordering.Domain/Exceptions/OrderingDomainException.cs",
    "content": "﻿namespace eShop.Ordering.Domain.Exceptions;\n\n/// <summary>\n/// Exception type for domain exceptions\n/// </summary>\npublic class OrderingDomainException : Exception\n{\n    public OrderingDomainException()\n    { }\n\n    public OrderingDomainException(string message)\n        : base(message)\n    { }\n\n    public OrderingDomainException(string message, Exception innerException)\n        : base(message, innerException)\n    { }\n}\n"
  },
  {
    "path": "src/Ordering.Domain/GlobalUsings.cs",
    "content": "﻿global using System.Reflection;\nglobal using eShop.Ordering.Domain.Exceptions;\nglobal using MediatR;\nglobal using eShop.Ordering.Domain.AggregatesModel.BuyerAggregate;\nglobal using eShop.Ordering.Domain.AggregatesModel.OrderAggregate;\nglobal using eShop.Ordering.Domain.Events;\nglobal using eShop.Ordering.Domain.Seedwork;\n"
  },
  {
    "path": "src/Ordering.Domain/Ordering.Domain.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"MediatR\" />\n    <PackageReference Include=\"System.Reflection.TypeExtensions\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Ordering.Domain/SeedWork/Entity.cs",
    "content": "﻿namespace eShop.Ordering.Domain.Seedwork;\n\npublic abstract class Entity\n{\n    int? _requestedHashCode;\n    int _Id;\n    public virtual int Id\n    {\n        get\n        {\n            return _Id;\n        }\n        protected set\n        {\n            _Id = value;\n        }\n    }\n\n    private List<INotification> _domainEvents;\n    public IReadOnlyCollection<INotification> DomainEvents => _domainEvents?.AsReadOnly();\n\n    public void AddDomainEvent(INotification eventItem)\n    {\n        _domainEvents = _domainEvents ?? new List<INotification>();\n        _domainEvents.Add(eventItem);\n    }\n\n    public void RemoveDomainEvent(INotification eventItem)\n    {\n        _domainEvents?.Remove(eventItem);\n    }\n\n    public void ClearDomainEvents()\n    {\n        _domainEvents?.Clear();\n    }\n\n    public bool IsTransient()\n    {\n        return this.Id == default;\n    }\n\n    public override bool Equals(object obj)\n    {\n        if (obj == null || !(obj is Entity))\n            return false;\n\n        if (Object.ReferenceEquals(this, obj))\n            return true;\n\n        if (this.GetType() != obj.GetType())\n            return false;\n\n        Entity item = (Entity)obj;\n\n        if (item.IsTransient() || this.IsTransient())\n            return false;\n        else\n            return item.Id == this.Id;\n    }\n\n    public override int GetHashCode()\n    {\n        if (!IsTransient())\n        {\n            if (!_requestedHashCode.HasValue)\n                _requestedHashCode = this.Id.GetHashCode() ^ 31; // XOR for random distribution (http://blogs.msdn.com/b/ericlippert/archive/2011/02/28/guidelines-and-rules-for-gethashcode.aspx)\n\n            return _requestedHashCode.Value;\n        }\n        else\n            return base.GetHashCode();\n\n    }\n    public static bool operator ==(Entity left, Entity right)\n    {\n        if (Object.Equals(left, null))\n            return (Object.Equals(right, null)) ? true : false;\n        else\n            return left.Equals(right);\n    }\n\n    public static bool operator !=(Entity left, Entity right)\n    {\n        return !(left == right);\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Domain/SeedWork/IAggregateRoot.cs",
    "content": "﻿namespace eShop.Ordering.Domain.Seedwork;\n\npublic interface IAggregateRoot { }\n\n\n"
  },
  {
    "path": "src/Ordering.Domain/SeedWork/IRepository.cs",
    "content": "﻿namespace eShop.Ordering.Domain.Seedwork;\n\npublic interface IRepository<T> where T : IAggregateRoot\n{\n    IUnitOfWork UnitOfWork { get; }\n}\n"
  },
  {
    "path": "src/Ordering.Domain/SeedWork/IUnitOfWork.cs",
    "content": "﻿namespace eShop.Ordering.Domain.Seedwork;\n\npublic interface IUnitOfWork : IDisposable\n{\n    Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);\n    Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "src/Ordering.Domain/SeedWork/ValueObject.cs",
    "content": "﻿namespace eShop.Ordering.Domain.SeedWork;\n\npublic abstract class ValueObject\n{\n    protected static bool EqualOperator(ValueObject left, ValueObject right)\n    {\n        if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))\n        {\n            return false;\n        }\n        return ReferenceEquals(left, null) || left.Equals(right);\n    }\n\n    protected static bool NotEqualOperator(ValueObject left, ValueObject right)\n    {\n        return !(EqualOperator(left, right));\n    }\n\n    protected abstract IEnumerable<object> GetEqualityComponents();\n\n    public override bool Equals(object obj)\n    {\n        if (obj == null || obj.GetType() != GetType())\n        {\n            return false;\n        }\n\n        var other = (ValueObject)obj;\n\n        return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());\n    }\n\n    public override int GetHashCode()\n    {\n        return GetEqualityComponents()\n            .Select(x => x != null ? x.GetHashCode() : 0)\n            .Aggregate((x, y) => x ^ y);\n    }\n\n    public ValueObject GetCopy()\n    {\n        return this.MemberwiseClone() as ValueObject;\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Infrastructure/EntityConfigurations/BuyerEntityTypeConfiguration.cs",
    "content": "﻿namespace eShop.Ordering.Infrastructure.EntityConfigurations;\n\nclass BuyerEntityTypeConfiguration\n    : IEntityTypeConfiguration<Buyer>\n{\n    public void Configure(EntityTypeBuilder<Buyer> buyerConfiguration)\n    {\n        buyerConfiguration.ToTable(\"buyers\");\n\n        buyerConfiguration.Ignore(b => b.DomainEvents);\n\n        buyerConfiguration.Property(b => b.Id)\n            .UseHiLo(\"buyerseq\");\n\n        buyerConfiguration.Property(b => b.IdentityGuid)\n            .HasMaxLength(200);\n\n        buyerConfiguration.HasIndex(\"IdentityGuid\")\n            .IsUnique(true);\n\n        buyerConfiguration.HasMany(b => b.PaymentMethods)\n            .WithOne();\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Infrastructure/EntityConfigurations/CardTypeEntityTypeConfiguration.cs",
    "content": "﻿namespace eShop.Ordering.Infrastructure.EntityConfigurations;\n\nclass CardTypeEntityTypeConfiguration\n    : IEntityTypeConfiguration<CardType>\n{\n    public void Configure(EntityTypeBuilder<CardType> cardTypesConfiguration)\n    {\n        cardTypesConfiguration.ToTable(\"cardtypes\");\n\n        cardTypesConfiguration.Property(ct => ct.Id)\n            .ValueGeneratedNever();\n\n        cardTypesConfiguration.Property(ct => ct.Name)\n            .HasMaxLength(200)\n            .IsRequired();\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Infrastructure/EntityConfigurations/ClientRequestEntityTypeConfiguration.cs",
    "content": "﻿namespace eShop.Ordering.Infrastructure.EntityConfigurations;\n\nclass ClientRequestEntityTypeConfiguration\n    : IEntityTypeConfiguration<ClientRequest>\n{\n    public void Configure(EntityTypeBuilder<ClientRequest> requestConfiguration)\n    {\n        requestConfiguration.ToTable(\"requests\");\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Infrastructure/EntityConfigurations/OrderEntityTypeConfiguration.cs",
    "content": "﻿namespace eShop.Ordering.Infrastructure.EntityConfigurations;\n\nclass OrderEntityTypeConfiguration : IEntityTypeConfiguration<Order>\n{\n    public void Configure(EntityTypeBuilder<Order> orderConfiguration)\n    {\n        orderConfiguration.ToTable(\"orders\");\n\n        orderConfiguration.Ignore(b => b.DomainEvents);\n\n        orderConfiguration.Property(o => o.Id)\n            .UseHiLo(\"orderseq\");\n\n        //Address value object persisted as owned entity type supported since EF Core 2.0\n        orderConfiguration\n            .OwnsOne(o => o.Address);\n\n        orderConfiguration\n            .Property(o => o.OrderStatus)\n            .HasConversion<string>()\n            .HasMaxLength(30);\n\n        orderConfiguration\n            .Property(o => o.PaymentId)\n            .HasColumnName(\"PaymentMethodId\");\n\n        orderConfiguration.HasOne<PaymentMethod>()\n            .WithMany()\n            .HasForeignKey(o => o.PaymentId)\n            .OnDelete(DeleteBehavior.Restrict);\n\n        orderConfiguration.HasOne(o => o.Buyer)\n            .WithMany()\n            .HasForeignKey(o => o.BuyerId);\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Infrastructure/EntityConfigurations/OrderItemEntityTypeConfiguration.cs",
    "content": "﻿namespace eShop.Ordering.Infrastructure.EntityConfigurations;\n\nclass OrderItemEntityTypeConfiguration\n    : IEntityTypeConfiguration<OrderItem>\n{\n    public void Configure(EntityTypeBuilder<OrderItem> orderItemConfiguration)\n    {\n        orderItemConfiguration.ToTable(\"orderItems\");\n\n        orderItemConfiguration.Ignore(b => b.DomainEvents);\n\n        orderItemConfiguration.Property(o => o.Id)\n            .UseHiLo(\"orderitemseq\");\n\n        orderItemConfiguration.Property<int>(\"OrderId\");\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Infrastructure/EntityConfigurations/PaymentMethodEntityTypeConfiguration.cs",
    "content": "﻿namespace eShop.Ordering.Infrastructure.EntityConfigurations;\n\nclass PaymentMethodEntityTypeConfiguration\n    : IEntityTypeConfiguration<PaymentMethod>\n{\n    public void Configure(EntityTypeBuilder<PaymentMethod> paymentConfiguration)\n    {\n        paymentConfiguration.ToTable(\"paymentmethods\");\n\n        paymentConfiguration.Ignore(b => b.DomainEvents);\n\n        paymentConfiguration.Property(b => b.Id)\n            .UseHiLo(\"paymentseq\");\n\n        paymentConfiguration.Property<int>(\"BuyerId\");\n\n        paymentConfiguration\n            .Property(\"_cardHolderName\")\n            .HasColumnName(\"CardHolderName\")\n            .HasMaxLength(200);\n\n        paymentConfiguration\n            .Property(\"_alias\")\n            .HasColumnName(\"Alias\")\n            .HasMaxLength(200);\n\n        paymentConfiguration\n            .Property(\"_cardNumber\")\n            .HasColumnName(\"CardNumber\")\n            .HasMaxLength(25)\n            .IsRequired();\n\n        paymentConfiguration\n            .Property(\"_expiration\")\n            .HasColumnName(\"Expiration\")\n            .HasMaxLength(25);\n\n        paymentConfiguration\n            .Property(\"_cardTypeId\")\n            .HasColumnName(\"CardTypeId\");\n\n        paymentConfiguration.HasOne(p => p.CardType)\n            .WithMany()\n            .HasForeignKey(\"_cardTypeId\");\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Infrastructure/GlobalUsings.cs",
    "content": "﻿global using System.Data;\nglobal using MediatR;\nglobal using Microsoft.EntityFrameworkCore;\nglobal using Microsoft.EntityFrameworkCore.Design;\nglobal using Microsoft.EntityFrameworkCore.Metadata.Builders;\nglobal using Microsoft.EntityFrameworkCore.Storage;\nglobal using eShop.Ordering.Domain.AggregatesModel.BuyerAggregate;\nglobal using eShop.Ordering.Domain.AggregatesModel.OrderAggregate;\nglobal using eShop.Ordering.Domain.Exceptions;\nglobal using eShop.Ordering.Domain.Seedwork;\nglobal using eShop.Ordering.Infrastructure.EntityConfigurations;\nglobal using eShop.Ordering.Infrastructure.Idempotency;\n"
  },
  {
    "path": "src/Ordering.Infrastructure/Idempotency/ClientRequest.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace eShop.Ordering.Infrastructure.Idempotency;\n\npublic class ClientRequest\n{\n    public Guid Id { get; set; }\n    [Required]\n    public string Name { get; set; }\n    public DateTime Time { get; set; }\n}\n"
  },
  {
    "path": "src/Ordering.Infrastructure/Idempotency/IRequestManager.cs",
    "content": "﻿namespace eShop.Ordering.Infrastructure.Idempotency;\n\npublic interface IRequestManager\n{\n    Task<bool> ExistAsync(Guid id);\n\n    Task CreateRequestForCommandAsync<T>(Guid id);\n}\n"
  },
  {
    "path": "src/Ordering.Infrastructure/Idempotency/RequestManager.cs",
    "content": "﻿namespace eShop.Ordering.Infrastructure.Idempotency;\n\npublic class RequestManager : IRequestManager\n{\n    private readonly OrderingContext _context;\n\n    public RequestManager(OrderingContext context)\n    {\n        _context = context ?? throw new ArgumentNullException(nameof(context));\n    }\n\n\n    public async Task<bool> ExistAsync(Guid id)\n    {\n        var request = await _context.\n            FindAsync<ClientRequest>(id);\n\n        return request != null;\n    }\n\n    public async Task CreateRequestForCommandAsync<T>(Guid id)\n    {\n        var exists = await ExistAsync(id);\n\n        var request = exists ?\n            throw new OrderingDomainException($\"Request with {id} already exists\") :\n            new ClientRequest()\n            {\n                Id = id,\n                Name = typeof(T).Name,\n                Time = DateTime.UtcNow\n            };\n\n        _context.Add(request);\n\n        await _context.SaveChangesAsync();\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Infrastructure/MediatorExtension.cs",
    "content": "﻿namespace eShop.Ordering.Infrastructure;\n\nstatic class MediatorExtension\n{\n    public static async Task DispatchDomainEventsAsync(this IMediator mediator, OrderingContext ctx)\n    {\n        var domainEntities = ctx.ChangeTracker\n            .Entries<Entity>()\n            .Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any());\n\n        var domainEvents = domainEntities\n            .SelectMany(x => x.Entity.DomainEvents)\n            .ToList();\n\n        domainEntities.ToList()\n            .ForEach(entity => entity.Entity.ClearDomainEvents());\n\n        foreach (var domainEvent in domainEvents)\n            await mediator.Publish(domainEvent);\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Infrastructure/Migrations/20230925222426_Initial.Designer.cs",
    "content": "﻿// <auto-generated />\nusing System;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Infrastructure;\nusing Microsoft.EntityFrameworkCore.Migrations;\nusing Microsoft.EntityFrameworkCore.Storage.ValueConversion;\nusing eShop.Ordering.Infrastructure;\nusing Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;\n\n#nullable disable\n\nnamespace Ordering.Infrastructure.Migrations\n{\n    [DbContext(typeof(OrderingContext))]\n    [Migration(\"20230925222426_Initial\")]\n    partial class Initial\n    {\n        /// <inheritdoc />\n        protected override void BuildTargetModel(ModelBuilder modelBuilder)\n        {\n#pragma warning disable 612, 618\n            modelBuilder\n                .HasAnnotation(\"ProductVersion\", \"8.0.0-rc.1.23419.6\")\n                .HasAnnotation(\"Relational:MaxIdentifierLength\", 63);\n\n            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);\n\n            modelBuilder.HasSequence(\"buyerseq\", \"ordering\")\n                .IncrementsBy(10);\n\n            modelBuilder.HasSequence(\"orderitemseq\")\n                .IncrementsBy(10);\n\n            modelBuilder.HasSequence(\"orderseq\", \"ordering\")\n                .IncrementsBy(10);\n\n            modelBuilder.HasSequence(\"paymentseq\", \"ordering\")\n                .IncrementsBy(10);\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"buyerseq\", \"ordering\");\n\n                    b.Property<string>(\"IdentityGuid\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\");\n\n                    b.Property<string>(\"Name\")\n                        .HasColumnType(\"text\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"IdentityGuid\")\n                        .IsUnique();\n\n                    b.ToTable(\"buyers\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.CardType\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .HasColumnType(\"integer\")\n                        .HasDefaultValue(1);\n\n                    b.Property<string>(\"Name\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"cardtypes\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"paymentseq\", \"ordering\");\n\n                    b.Property<int>(\"BuyerId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"_alias\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\")\n                        .HasColumnName(\"Alias\");\n\n                    b.Property<string>(\"_cardHolderName\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\")\n                        .HasColumnName(\"CardHolderName\");\n\n                    b.Property<string>(\"_cardNumber\")\n                        .IsRequired()\n                        .HasMaxLength(25)\n                        .HasColumnType(\"character varying(25)\")\n                        .HasColumnName(\"CardNumber\");\n\n                    b.Property<int>(\"_cardTypeId\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"CardTypeId\");\n\n                    b.Property<DateTime>(\"_expiration\")\n                        .HasMaxLength(25)\n                        .HasColumnType(\"timestamp with time zone\")\n                        .HasColumnName(\"Expiration\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"BuyerId\");\n\n                    b.HasIndex(\"_cardTypeId\");\n\n                    b.ToTable(\"paymentmethods\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Order\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"orderseq\", \"ordering\");\n\n                    b.Property<string>(\"Description\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<int?>(\"_buyerId\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"BuyerId\");\n\n                    b.Property<DateTime>(\"_orderDate\")\n                        .HasColumnType(\"timestamp with time zone\")\n                        .HasColumnName(\"OrderDate\");\n\n                    b.Property<int>(\"_orderStatusId\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"OrderStatusId\");\n\n                    b.Property<int?>(\"_paymentMethodId\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"PaymentMethodId\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"_buyerId\");\n\n                    b.HasIndex(\"_orderStatusId\");\n\n                    b.HasIndex(\"_paymentMethodId\");\n\n                    b.ToTable(\"orders\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.OrderItem\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"orderitemseq\");\n\n                    b.Property<int>(\"OrderId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<int>(\"ProductId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<decimal>(\"_discount\")\n                        .HasColumnType(\"numeric\")\n                        .HasColumnName(\"Discount\");\n\n                    b.Property<string>(\"_pictureUrl\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"PictureUrl\");\n\n                    b.Property<string>(\"_productName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"ProductName\");\n\n                    b.Property<decimal>(\"_unitPrice\")\n                        .HasColumnType(\"numeric\")\n                        .HasColumnName(\"UnitPrice\");\n\n                    b.Property<int>(\"_units\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"Units\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"OrderId\");\n\n                    b.ToTable(\"orderItems\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.OrderStatus\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .HasColumnType(\"integer\")\n                        .HasDefaultValue(1);\n\n                    b.Property<string>(\"Name\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"orderstatus\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Infrastructure.Idempotency.ClientRequest\", b =>\n                {\n                    b.Property<Guid>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"uuid\");\n\n                    b.Property<string>(\"Name\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<DateTime>(\"Time\")\n                        .HasColumnType(\"timestamp with time zone\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"requests\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod\", b =>\n                {\n                    b.HasOne(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer\", null)\n                        .WithMany(\"PaymentMethods\")\n                        .HasForeignKey(\"BuyerId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.HasOne(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.CardType\", \"CardType\")\n                        .WithMany()\n                        .HasForeignKey(\"_cardTypeId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"CardType\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Order\", b =>\n                {\n                    b.HasOne(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer\", null)\n                        .WithMany()\n                        .HasForeignKey(\"_buyerId\");\n\n                    b.HasOne(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.OrderStatus\", \"OrderStatus\")\n                        .WithMany()\n                        .HasForeignKey(\"_orderStatusId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.HasOne(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod\", null)\n                        .WithMany()\n                        .HasForeignKey(\"_paymentMethodId\")\n                        .OnDelete(DeleteBehavior.Restrict);\n\n                    b.OwnsOne(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Address\", \"Address\", b1 =>\n                        {\n                            b1.Property<int>(\"OrderId\")\n                                .ValueGeneratedOnAdd()\n                                .HasColumnType(\"integer\");\n\n                            NpgsqlPropertyBuilderExtensions.UseHiLo(b1.Property<int>(\"OrderId\"), \"orderseq\", \"ordering\");\n\n                            b1.Property<string>(\"City\")\n                                .HasColumnType(\"text\");\n\n                            b1.Property<string>(\"Country\")\n                                .HasColumnType(\"text\");\n\n                            b1.Property<string>(\"State\")\n                                .HasColumnType(\"text\");\n\n                            b1.Property<string>(\"Street\")\n                                .HasColumnType(\"text\");\n\n                            b1.Property<string>(\"ZipCode\")\n                                .HasColumnType(\"text\");\n\n                            b1.HasKey(\"OrderId\");\n\n                            b1.ToTable(\"orders\", \"ordering\");\n\n                            b1.WithOwner()\n                                .HasForeignKey(\"OrderId\");\n                        });\n\n                    b.Navigation(\"Address\");\n\n                    b.Navigation(\"OrderStatus\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.OrderItem\", b =>\n                {\n                    b.HasOne(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Order\", null)\n                        .WithMany(\"OrderItems\")\n                        .HasForeignKey(\"OrderId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer\", b =>\n                {\n                    b.Navigation(\"PaymentMethods\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Order\", b =>\n                {\n                    b.Navigation(\"OrderItems\");\n                });\n#pragma warning restore 612, 618\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Infrastructure/Migrations/20230925222426_Initial.cs",
    "content": "﻿using System;\nusing Microsoft.EntityFrameworkCore.Migrations;\n\n#nullable disable\n\nnamespace Ordering.Infrastructure.Migrations\n{\n    /// <inheritdoc />\n    public partial class Initial : Migration\n    {\n        /// <inheritdoc />\n        protected override void Up(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.EnsureSchema(\n                name: \"ordering\");\n\n            migrationBuilder.CreateSequence(\n                name: \"buyerseq\",\n                schema: \"ordering\",\n                incrementBy: 10);\n\n            migrationBuilder.CreateSequence(\n                name: \"orderitemseq\",\n                incrementBy: 10);\n\n            migrationBuilder.CreateSequence(\n                name: \"orderseq\",\n                schema: \"ordering\",\n                incrementBy: 10);\n\n            migrationBuilder.CreateSequence(\n                name: \"paymentseq\",\n                schema: \"ordering\",\n                incrementBy: 10);\n\n            migrationBuilder.CreateTable(\n                name: \"buyers\",\n                schema: \"ordering\",\n                columns: table => new\n                {\n                    Id = table.Column<int>(type: \"integer\", nullable: false),\n                    IdentityGuid = table.Column<string>(type: \"character varying(200)\", maxLength: 200, nullable: false),\n                    Name = table.Column<string>(type: \"text\", nullable: true)\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_buyers\", x => x.Id);\n                });\n\n            migrationBuilder.CreateTable(\n                name: \"cardtypes\",\n                schema: \"ordering\",\n                columns: table => new\n                {\n                    Id = table.Column<int>(type: \"integer\", nullable: false, defaultValue: 1),\n                    Name = table.Column<string>(type: \"character varying(200)\", maxLength: 200, nullable: false)\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_cardtypes\", x => x.Id);\n                });\n\n            migrationBuilder.CreateTable(\n                name: \"orderstatus\",\n                schema: \"ordering\",\n                columns: table => new\n                {\n                    Id = table.Column<int>(type: \"integer\", nullable: false, defaultValue: 1),\n                    Name = table.Column<string>(type: \"character varying(200)\", maxLength: 200, nullable: false)\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_orderstatus\", x => x.Id);\n                });\n\n            migrationBuilder.CreateTable(\n                name: \"requests\",\n                schema: \"ordering\",\n                columns: table => new\n                {\n                    Id = table.Column<Guid>(type: \"uuid\", nullable: false),\n                    Name = table.Column<string>(type: \"text\", nullable: false),\n                    Time = table.Column<DateTime>(type: \"timestamp with time zone\", nullable: false)\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_requests\", x => x.Id);\n                });\n\n            migrationBuilder.CreateTable(\n                name: \"paymentmethods\",\n                schema: \"ordering\",\n                columns: table => new\n                {\n                    Id = table.Column<int>(type: \"integer\", nullable: false),\n                    CardTypeId = table.Column<int>(type: \"integer\", nullable: false),\n                    BuyerId = table.Column<int>(type: \"integer\", nullable: false),\n                    Alias = table.Column<string>(type: \"character varying(200)\", maxLength: 200, nullable: false),\n                    CardHolderName = table.Column<string>(type: \"character varying(200)\", maxLength: 200, nullable: false),\n                    CardNumber = table.Column<string>(type: \"character varying(25)\", maxLength: 25, nullable: false),\n                    Expiration = table.Column<DateTime>(type: \"timestamp with time zone\", maxLength: 25, nullable: false)\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_paymentmethods\", x => x.Id);\n                    table.ForeignKey(\n                        name: \"FK_paymentmethods_buyers_BuyerId\",\n                        column: x => x.BuyerId,\n                        principalSchema: \"ordering\",\n                        principalTable: \"buyers\",\n                        principalColumn: \"Id\",\n                        onDelete: ReferentialAction.Cascade);\n                    table.ForeignKey(\n                        name: \"FK_paymentmethods_cardtypes_CardTypeId\",\n                        column: x => x.CardTypeId,\n                        principalSchema: \"ordering\",\n                        principalTable: \"cardtypes\",\n                        principalColumn: \"Id\",\n                        onDelete: ReferentialAction.Cascade);\n                });\n\n            migrationBuilder.CreateTable(\n                name: \"orders\",\n                schema: \"ordering\",\n                columns: table => new\n                {\n                    Id = table.Column<int>(type: \"integer\", nullable: false),\n                    Address_Street = table.Column<string>(type: \"text\", nullable: true),\n                    Address_City = table.Column<string>(type: \"text\", nullable: true),\n                    Address_State = table.Column<string>(type: \"text\", nullable: true),\n                    Address_Country = table.Column<string>(type: \"text\", nullable: true),\n                    Address_ZipCode = table.Column<string>(type: \"text\", nullable: true),\n                    OrderStatusId = table.Column<int>(type: \"integer\", nullable: false),\n                    Description = table.Column<string>(type: \"text\", nullable: true),\n                    BuyerId = table.Column<int>(type: \"integer\", nullable: true),\n                    OrderDate = table.Column<DateTime>(type: \"timestamp with time zone\", nullable: false),\n                    PaymentMethodId = table.Column<int>(type: \"integer\", nullable: true)\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_orders\", x => x.Id);\n                    table.ForeignKey(\n                        name: \"FK_orders_buyers_BuyerId\",\n                        column: x => x.BuyerId,\n                        principalSchema: \"ordering\",\n                        principalTable: \"buyers\",\n                        principalColumn: \"Id\");\n                    table.ForeignKey(\n                        name: \"FK_orders_orderstatus_OrderStatusId\",\n                        column: x => x.OrderStatusId,\n                        principalSchema: \"ordering\",\n                        principalTable: \"orderstatus\",\n                        principalColumn: \"Id\",\n                        onDelete: ReferentialAction.Cascade);\n                    table.ForeignKey(\n                        name: \"FK_orders_paymentmethods_PaymentMethodId\",\n                        column: x => x.PaymentMethodId,\n                        principalSchema: \"ordering\",\n                        principalTable: \"paymentmethods\",\n                        principalColumn: \"Id\",\n                        onDelete: ReferentialAction.Restrict);\n                });\n\n            migrationBuilder.CreateTable(\n                name: \"orderItems\",\n                schema: \"ordering\",\n                columns: table => new\n                {\n                    Id = table.Column<int>(type: \"integer\", nullable: false),\n                    ProductId = table.Column<int>(type: \"integer\", nullable: false),\n                    OrderId = table.Column<int>(type: \"integer\", nullable: false),\n                    Discount = table.Column<decimal>(type: \"numeric\", nullable: false),\n                    PictureUrl = table.Column<string>(type: \"text\", nullable: true),\n                    ProductName = table.Column<string>(type: \"text\", nullable: false),\n                    UnitPrice = table.Column<decimal>(type: \"numeric\", nullable: false),\n                    Units = table.Column<int>(type: \"integer\", nullable: false)\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_orderItems\", x => x.Id);\n                    table.ForeignKey(\n                        name: \"FK_orderItems_orders_OrderId\",\n                        column: x => x.OrderId,\n                        principalSchema: \"ordering\",\n                        principalTable: \"orders\",\n                        principalColumn: \"Id\",\n                        onDelete: ReferentialAction.Cascade);\n                });\n\n            migrationBuilder.CreateIndex(\n                name: \"IX_buyers_IdentityGuid\",\n                schema: \"ordering\",\n                table: \"buyers\",\n                column: \"IdentityGuid\",\n                unique: true);\n\n            migrationBuilder.CreateIndex(\n                name: \"IX_orderItems_OrderId\",\n                schema: \"ordering\",\n                table: \"orderItems\",\n                column: \"OrderId\");\n\n            migrationBuilder.CreateIndex(\n                name: \"IX_orders_BuyerId\",\n                schema: \"ordering\",\n                table: \"orders\",\n                column: \"BuyerId\");\n\n            migrationBuilder.CreateIndex(\n                name: \"IX_orders_OrderStatusId\",\n                schema: \"ordering\",\n                table: \"orders\",\n                column: \"OrderStatusId\");\n\n            migrationBuilder.CreateIndex(\n                name: \"IX_orders_PaymentMethodId\",\n                schema: \"ordering\",\n                table: \"orders\",\n                column: \"PaymentMethodId\");\n\n            migrationBuilder.CreateIndex(\n                name: \"IX_paymentmethods_BuyerId\",\n                schema: \"ordering\",\n                table: \"paymentmethods\",\n                column: \"BuyerId\");\n\n            migrationBuilder.CreateIndex(\n                name: \"IX_paymentmethods_CardTypeId\",\n                schema: \"ordering\",\n                table: \"paymentmethods\",\n                column: \"CardTypeId\");\n        }\n\n        /// <inheritdoc />\n        protected override void Down(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.DropTable(\n                name: \"orderItems\",\n                schema: \"ordering\");\n\n            migrationBuilder.DropTable(\n                name: \"requests\",\n                schema: \"ordering\");\n\n            migrationBuilder.DropTable(\n                name: \"orders\",\n                schema: \"ordering\");\n\n            migrationBuilder.DropTable(\n                name: \"orderstatus\",\n                schema: \"ordering\");\n\n            migrationBuilder.DropTable(\n                name: \"paymentmethods\",\n                schema: \"ordering\");\n\n            migrationBuilder.DropTable(\n                name: \"buyers\",\n                schema: \"ordering\");\n\n            migrationBuilder.DropTable(\n                name: \"cardtypes\",\n                schema: \"ordering\");\n\n            migrationBuilder.DropSequence(\n                name: \"buyerseq\",\n                schema: \"ordering\");\n\n            migrationBuilder.DropSequence(\n                name: \"orderitemseq\");\n\n            migrationBuilder.DropSequence(\n                name: \"orderseq\",\n                schema: \"ordering\");\n\n            migrationBuilder.DropSequence(\n                name: \"paymentseq\",\n                schema: \"ordering\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Infrastructure/Migrations/20231021004633_FixOrderitemseqSchema.Designer.cs",
    "content": "﻿// <auto-generated />\nusing System;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Infrastructure;\nusing Microsoft.EntityFrameworkCore.Migrations;\nusing Microsoft.EntityFrameworkCore.Storage.ValueConversion;\nusing eShop.Ordering.Infrastructure;\nusing Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;\n\n#nullable disable\n\nnamespace Ordering.Infrastructure.Migrations\n{\n    [DbContext(typeof(OrderingContext))]\n    [Migration(\"20231021004633_FixOrderitemseqSchema\")]\n    partial class FixOrderitemseqSchema\n    {\n        /// <inheritdoc />\n        protected override void BuildTargetModel(ModelBuilder modelBuilder)\n        {\n#pragma warning disable 612, 618\n            modelBuilder\n                .HasDefaultSchema(\"ordering\")\n                .HasAnnotation(\"ProductVersion\", \"8.0.0-rc.2.23480.1\")\n                .HasAnnotation(\"Relational:MaxIdentifierLength\", 63);\n\n            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);\n\n            modelBuilder.HasSequence(\"buyerseq\")\n                .IncrementsBy(10);\n\n            modelBuilder.HasSequence(\"orderitemseq\")\n                .IncrementsBy(10);\n\n            modelBuilder.HasSequence(\"orderseq\")\n                .IncrementsBy(10);\n\n            modelBuilder.HasSequence(\"paymentseq\")\n                .IncrementsBy(10);\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"buyerseq\");\n\n                    b.Property<string>(\"IdentityGuid\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\");\n\n                    b.Property<string>(\"Name\")\n                        .HasColumnType(\"text\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"IdentityGuid\")\n                        .IsUnique();\n\n                    b.ToTable(\"buyers\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.CardType\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"Name\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"cardtypes\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"paymentseq\");\n\n                    b.Property<int>(\"BuyerId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"_alias\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\")\n                        .HasColumnName(\"Alias\");\n\n                    b.Property<string>(\"_cardHolderName\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\")\n                        .HasColumnName(\"CardHolderName\");\n\n                    b.Property<string>(\"_cardNumber\")\n                        .IsRequired()\n                        .HasMaxLength(25)\n                        .HasColumnType(\"character varying(25)\")\n                        .HasColumnName(\"CardNumber\");\n\n                    b.Property<int>(\"_cardTypeId\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"CardTypeId\");\n\n                    b.Property<DateTime>(\"_expiration\")\n                        .HasMaxLength(25)\n                        .HasColumnType(\"timestamp with time zone\")\n                        .HasColumnName(\"Expiration\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"BuyerId\");\n\n                    b.HasIndex(\"_cardTypeId\");\n\n                    b.ToTable(\"paymentmethods\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Order\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"orderseq\");\n\n                    b.Property<string>(\"Description\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<int?>(\"_buyerId\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"BuyerId\");\n\n                    b.Property<DateTime>(\"_orderDate\")\n                        .HasColumnType(\"timestamp with time zone\")\n                        .HasColumnName(\"OrderDate\");\n\n                    b.Property<int>(\"_orderStatusId\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"OrderStatusId\");\n\n                    b.Property<int?>(\"_paymentMethodId\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"PaymentMethodId\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"_buyerId\");\n\n                    b.HasIndex(\"_orderStatusId\");\n\n                    b.HasIndex(\"_paymentMethodId\");\n\n                    b.ToTable(\"orders\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.OrderItem\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"orderitemseq\");\n\n                    b.Property<int>(\"OrderId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<int>(\"ProductId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<decimal>(\"_discount\")\n                        .HasColumnType(\"numeric\")\n                        .HasColumnName(\"Discount\");\n\n                    b.Property<string>(\"_pictureUrl\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"PictureUrl\");\n\n                    b.Property<string>(\"_productName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"ProductName\");\n\n                    b.Property<decimal>(\"_unitPrice\")\n                        .HasColumnType(\"numeric\")\n                        .HasColumnName(\"UnitPrice\");\n\n                    b.Property<int>(\"_units\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"Units\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"OrderId\");\n\n                    b.ToTable(\"orderItems\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.OrderStatus\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"Name\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"orderstatus\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Infrastructure.Idempotency.ClientRequest\", b =>\n                {\n                    b.Property<Guid>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"uuid\");\n\n                    b.Property<string>(\"Name\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<DateTime>(\"Time\")\n                        .HasColumnType(\"timestamp with time zone\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"requests\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod\", b =>\n                {\n                    b.HasOne(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer\", null)\n                        .WithMany(\"PaymentMethods\")\n                        .HasForeignKey(\"BuyerId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.HasOne(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.CardType\", \"CardType\")\n                        .WithMany()\n                        .HasForeignKey(\"_cardTypeId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"CardType\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Order\", b =>\n                {\n                    b.HasOne(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer\", null)\n                        .WithMany()\n                        .HasForeignKey(\"_buyerId\");\n\n                    b.HasOne(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.OrderStatus\", \"OrderStatus\")\n                        .WithMany()\n                        .HasForeignKey(\"_orderStatusId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.HasOne(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod\", null)\n                        .WithMany()\n                        .HasForeignKey(\"_paymentMethodId\")\n                        .OnDelete(DeleteBehavior.Restrict);\n\n                    b.OwnsOne(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Address\", \"Address\", b1 =>\n                        {\n                            b1.Property<int>(\"OrderId\")\n                                .HasColumnType(\"integer\");\n\n                            b1.Property<string>(\"City\")\n                                .HasColumnType(\"text\");\n\n                            b1.Property<string>(\"Country\")\n                                .HasColumnType(\"text\");\n\n                            b1.Property<string>(\"State\")\n                                .HasColumnType(\"text\");\n\n                            b1.Property<string>(\"Street\")\n                                .HasColumnType(\"text\");\n\n                            b1.Property<string>(\"ZipCode\")\n                                .HasColumnType(\"text\");\n\n                            b1.HasKey(\"OrderId\");\n\n                            b1.ToTable(\"orders\", \"ordering\");\n\n                            b1.WithOwner()\n                                .HasForeignKey(\"OrderId\");\n                        });\n\n                    b.Navigation(\"Address\")\n                        .IsRequired();\n\n                    b.Navigation(\"OrderStatus\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.OrderItem\", b =>\n                {\n                    b.HasOne(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Order\", null)\n                        .WithMany(\"OrderItems\")\n                        .HasForeignKey(\"OrderId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer\", b =>\n                {\n                    b.Navigation(\"PaymentMethods\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Order\", b =>\n                {\n                    b.Navigation(\"OrderItems\");\n                });\n#pragma warning restore 612, 618\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Infrastructure/Migrations/20231021004633_FixOrderitemseqSchema.cs",
    "content": "﻿using Microsoft.EntityFrameworkCore.Migrations;\n\n#nullable disable\n\nnamespace Ordering.Infrastructure.Migrations\n{\n    /// <inheritdoc />\n    public partial class FixOrderitemseqSchema : Migration\n    {\n        /// <inheritdoc />\n        protected override void Up(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.RenameSequence(\n                name: \"orderitemseq\",\n                newName: \"orderitemseq\",\n                newSchema: \"ordering\");\n\n            migrationBuilder.AlterColumn<int>(\n                name: \"Id\",\n                schema: \"ordering\",\n                table: \"orderstatus\",\n                type: \"integer\",\n                nullable: false,\n                oldClrType: typeof(int),\n                oldType: \"integer\",\n                oldDefaultValue: 1);\n\n            migrationBuilder.AlterColumn<int>(\n                name: \"Id\",\n                schema: \"ordering\",\n                table: \"cardtypes\",\n                type: \"integer\",\n                nullable: false,\n                oldClrType: typeof(int),\n                oldType: \"integer\",\n                oldDefaultValue: 1);\n        }\n\n        /// <inheritdoc />\n        protected override void Down(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.RenameSequence(\n                name: \"orderitemseq\",\n                schema: \"ordering\",\n                newName: \"orderitemseq\");\n\n            migrationBuilder.AlterColumn<int>(\n                name: \"Id\",\n                schema: \"ordering\",\n                table: \"orderstatus\",\n                type: \"integer\",\n                nullable: false,\n                defaultValue: 1,\n                oldClrType: typeof(int),\n                oldType: \"integer\");\n\n            migrationBuilder.AlterColumn<int>(\n                name: \"Id\",\n                schema: \"ordering\",\n                table: \"cardtypes\",\n                type: \"integer\",\n                nullable: false,\n                defaultValue: 1,\n                oldClrType: typeof(int),\n                oldType: \"integer\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Infrastructure/Migrations/20231026091055_Outbox.Designer.cs",
    "content": "﻿// <auto-generated />\nusing System;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Infrastructure;\nusing Microsoft.EntityFrameworkCore.Migrations;\nusing Microsoft.EntityFrameworkCore.Storage.ValueConversion;\nusing eShop.Ordering.Infrastructure;\nusing Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;\n\n#nullable disable\n\nnamespace Ordering.Infrastructure.Migrations\n{\n    [DbContext(typeof(OrderingContext))]\n    [Migration(\"20231026091055_Outbox\")]\n    partial class Outbox\n    {\n        /// <inheritdoc />\n        protected override void BuildTargetModel(ModelBuilder modelBuilder)\n        {\n#pragma warning disable 612, 618\n            modelBuilder\n                .HasDefaultSchema(\"ordering\")\n                .HasAnnotation(\"ProductVersion\", \"8.0.0-rtm.23512.13\")\n                .HasAnnotation(\"Relational:MaxIdentifierLength\", 63);\n\n            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);\n\n            modelBuilder.HasSequence(\"buyerseq\")\n                .IncrementsBy(10);\n\n            modelBuilder.HasSequence(\"orderitemseq\")\n                .IncrementsBy(10);\n\n            modelBuilder.HasSequence(\"orderseq\")\n                .IncrementsBy(10);\n\n            modelBuilder.HasSequence(\"paymentseq\")\n                .IncrementsBy(10);\n\n            modelBuilder.Entity(\"eShop.IntegrationEventLogEF.IntegrationEventLogEntry\", b =>\n                {\n                    b.Property<Guid>(\"EventId\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"uuid\");\n\n                    b.Property<string>(\"Content\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<DateTime>(\"CreationTime\")\n                        .HasColumnType(\"timestamp with time zone\");\n\n                    b.Property<string>(\"EventTypeName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<int>(\"State\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<int>(\"TimesSent\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<Guid>(\"TransactionId\")\n                        .HasColumnType(\"uuid\");\n\n                    b.HasKey(\"EventId\");\n\n                    b.ToTable(\"IntegrationEventLog\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"buyerseq\");\n\n                    b.Property<string>(\"IdentityGuid\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\");\n\n                    b.Property<string>(\"Name\")\n                        .HasColumnType(\"text\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"IdentityGuid\")\n                        .IsUnique();\n\n                    b.ToTable(\"buyers\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.CardType\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"Name\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"cardtypes\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"paymentseq\");\n\n                    b.Property<int>(\"BuyerId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"_alias\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\")\n                        .HasColumnName(\"Alias\");\n\n                    b.Property<string>(\"_cardHolderName\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\")\n                        .HasColumnName(\"CardHolderName\");\n\n                    b.Property<string>(\"_cardNumber\")\n                        .IsRequired()\n                        .HasMaxLength(25)\n                        .HasColumnType(\"character varying(25)\")\n                        .HasColumnName(\"CardNumber\");\n\n                    b.Property<int>(\"_cardTypeId\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"CardTypeId\");\n\n                    b.Property<DateTime>(\"_expiration\")\n                        .HasMaxLength(25)\n                        .HasColumnType(\"timestamp with time zone\")\n                        .HasColumnName(\"Expiration\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"BuyerId\");\n\n                    b.HasIndex(\"_cardTypeId\");\n\n                    b.ToTable(\"paymentmethods\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Order\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"orderseq\");\n\n                    b.Property<string>(\"Description\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<int?>(\"_buyerId\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"BuyerId\");\n\n                    b.Property<DateTime>(\"_orderDate\")\n                        .HasColumnType(\"timestamp with time zone\")\n                        .HasColumnName(\"OrderDate\");\n\n                    b.Property<int>(\"_orderStatusId\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"OrderStatusId\");\n\n                    b.Property<int?>(\"_paymentMethodId\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"PaymentMethodId\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"_buyerId\");\n\n                    b.HasIndex(\"_orderStatusId\");\n\n                    b.HasIndex(\"_paymentMethodId\");\n\n                    b.ToTable(\"orders\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.OrderItem\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"orderitemseq\");\n\n                    b.Property<int>(\"OrderId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<int>(\"ProductId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<decimal>(\"_discount\")\n                        .HasColumnType(\"numeric\")\n                        .HasColumnName(\"Discount\");\n\n                    b.Property<string>(\"_pictureUrl\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"PictureUrl\");\n\n                    b.Property<string>(\"_productName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"ProductName\");\n\n                    b.Property<decimal>(\"_unitPrice\")\n                        .HasColumnType(\"numeric\")\n                        .HasColumnName(\"UnitPrice\");\n\n                    b.Property<int>(\"_units\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"Units\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"OrderId\");\n\n                    b.ToTable(\"orderItems\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.OrderStatus\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"Name\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"orderstatus\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Infrastructure.Idempotency.ClientRequest\", b =>\n                {\n                    b.Property<Guid>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"uuid\");\n\n                    b.Property<string>(\"Name\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<DateTime>(\"Time\")\n                        .HasColumnType(\"timestamp with time zone\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"requests\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod\", b =>\n                {\n                    b.HasOne(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer\", null)\n                        .WithMany(\"PaymentMethods\")\n                        .HasForeignKey(\"BuyerId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.HasOne(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.CardType\", \"CardType\")\n                        .WithMany()\n                        .HasForeignKey(\"_cardTypeId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"CardType\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Order\", b =>\n                {\n                    b.HasOne(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer\", null)\n                        .WithMany()\n                        .HasForeignKey(\"_buyerId\");\n\n                    b.HasOne(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.OrderStatus\", \"OrderStatus\")\n                        .WithMany()\n                        .HasForeignKey(\"_orderStatusId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.HasOne(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod\", null)\n                        .WithMany()\n                        .HasForeignKey(\"_paymentMethodId\")\n                        .OnDelete(DeleteBehavior.Restrict);\n\n                    b.OwnsOne(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Address\", \"Address\", b1 =>\n                        {\n                            b1.Property<int>(\"OrderId\")\n                                .HasColumnType(\"integer\");\n\n                            b1.Property<string>(\"City\")\n                                .HasColumnType(\"text\");\n\n                            b1.Property<string>(\"Country\")\n                                .HasColumnType(\"text\");\n\n                            b1.Property<string>(\"State\")\n                                .HasColumnType(\"text\");\n\n                            b1.Property<string>(\"Street\")\n                                .HasColumnType(\"text\");\n\n                            b1.Property<string>(\"ZipCode\")\n                                .HasColumnType(\"text\");\n\n                            b1.HasKey(\"OrderId\");\n\n                            b1.ToTable(\"orders\", \"ordering\");\n\n                            b1.WithOwner()\n                                .HasForeignKey(\"OrderId\");\n                        });\n\n                    b.Navigation(\"Address\")\n                        .IsRequired();\n\n                    b.Navigation(\"OrderStatus\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.OrderItem\", b =>\n                {\n                    b.HasOne(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Order\", null)\n                        .WithMany(\"OrderItems\")\n                        .HasForeignKey(\"OrderId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer\", b =>\n                {\n                    b.Navigation(\"PaymentMethods\");\n                });\n\n            modelBuilder.Entity(\" eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Order\", b =>\n                {\n                    b.Navigation(\"OrderItems\");\n                });\n#pragma warning restore 612, 618\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Infrastructure/Migrations/20231026091055_Outbox.cs",
    "content": "﻿using System;\nusing Microsoft.EntityFrameworkCore.Migrations;\n\n#nullable disable\n\nnamespace Ordering.Infrastructure.Migrations\n{\n    /// <inheritdoc />\n    public partial class Outbox : Migration\n    {\n        /// <inheritdoc />\n        protected override void Up(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.CreateTable(\n                name: \"IntegrationEventLog\",\n                schema: \"ordering\",\n                columns: table => new\n                {\n                    EventId = table.Column<Guid>(type: \"uuid\", nullable: false),\n                    EventTypeName = table.Column<string>(type: \"text\", nullable: false),\n                    State = table.Column<int>(type: \"integer\", nullable: false),\n                    TimesSent = table.Column<int>(type: \"integer\", nullable: false),\n                    CreationTime = table.Column<DateTime>(type: \"timestamp with time zone\", nullable: false),\n                    Content = table.Column<string>(type: \"text\", nullable: false),\n                    TransactionId = table.Column<Guid>(type: \"uuid\", nullable: false)\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_IntegrationEventLog\", x => x.EventId);\n                });\n        }\n\n        /// <inheritdoc />\n        protected override void Down(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.DropTable(\n                name: \"IntegrationEventLog\",\n                schema: \"ordering\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Infrastructure/Migrations/20240106121712_UseEnumForOrderStatus.Designer.cs",
    "content": "﻿// <auto-generated />\nusing System;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Infrastructure;\nusing Microsoft.EntityFrameworkCore.Migrations;\nusing Microsoft.EntityFrameworkCore.Storage.ValueConversion;\nusing Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;\nusing eShop.Ordering.Infrastructure;\n\n#nullable disable\n\nnamespace Ordering.Infrastructure.Migrations\n{\n    [DbContext(typeof(OrderingContext))]\n    [Migration(\"20240106121712_UseEnumForOrderStatus\")]\n    partial class UseEnumForOrderStatus\n    {\n        /// <inheritdoc />\n        protected override void BuildTargetModel(ModelBuilder modelBuilder)\n        {\n#pragma warning disable 612, 618\n            modelBuilder\n                .HasDefaultSchema(\"ordering\")\n                .HasAnnotation(\"ProductVersion\", \"8.0.0\")\n                .HasAnnotation(\"Relational:MaxIdentifierLength\", 63);\n\n            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);\n\n            modelBuilder.HasSequence(\"buyerseq\")\n                .IncrementsBy(10);\n\n            modelBuilder.HasSequence(\"orderitemseq\")\n                .IncrementsBy(10);\n\n            modelBuilder.HasSequence(\"orderseq\")\n                .IncrementsBy(10);\n\n            modelBuilder.HasSequence(\"paymentseq\")\n                .IncrementsBy(10);\n\n            modelBuilder.Entity(\"eShop.IntegrationEventLogEF.IntegrationEventLogEntry\", b =>\n                {\n                    b.Property<Guid>(\"EventId\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"uuid\");\n\n                    b.Property<string>(\"Content\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<DateTime>(\"CreationTime\")\n                        .HasColumnType(\"timestamp with time zone\");\n\n                    b.Property<string>(\"EventTypeName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<int>(\"State\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<int>(\"TimesSent\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<Guid>(\"TransactionId\")\n                        .HasColumnType(\"uuid\");\n\n                    b.HasKey(\"EventId\");\n\n                    b.ToTable(\"IntegrationEventLog\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"buyerseq\");\n\n                    b.Property<string>(\"IdentityGuid\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\");\n\n                    b.Property<string>(\"Name\")\n                        .HasColumnType(\"text\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"IdentityGuid\")\n                        .IsUnique();\n\n                    b.ToTable(\"buyers\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.CardType\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"Name\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"cardtypes\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"paymentseq\");\n\n                    b.Property<int>(\"BuyerId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"_alias\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\")\n                        .HasColumnName(\"Alias\");\n\n                    b.Property<string>(\"_cardHolderName\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\")\n                        .HasColumnName(\"CardHolderName\");\n\n                    b.Property<string>(\"_cardNumber\")\n                        .IsRequired()\n                        .HasMaxLength(25)\n                        .HasColumnType(\"character varying(25)\")\n                        .HasColumnName(\"CardNumber\");\n\n                    b.Property<int>(\"_cardTypeId\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"CardTypeId\");\n\n                    b.Property<DateTime>(\"_expiration\")\n                        .HasMaxLength(25)\n                        .HasColumnType(\"timestamp with time zone\")\n                        .HasColumnName(\"Expiration\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"BuyerId\");\n\n                    b.HasIndex(\"_cardTypeId\");\n\n                    b.ToTable(\"paymentmethods\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Order\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"orderseq\");\n\n                    b.Property<string>(\"Description\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"OrderStatus\")\n                        .IsRequired()\n                        .HasMaxLength(30)\n                        .HasColumnType(\"character varying(30)\");\n\n                    b.Property<int?>(\"_buyerId\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"BuyerId\");\n\n                    b.Property<DateTime>(\"_orderDate\")\n                        .HasColumnType(\"timestamp with time zone\")\n                        .HasColumnName(\"OrderDate\");\n\n                    b.Property<int?>(\"_paymentMethodId\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"PaymentMethodId\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"_buyerId\");\n\n                    b.HasIndex(\"_paymentMethodId\");\n\n                    b.ToTable(\"orders\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Domain.AggregatesModel.OrderAggregate.OrderItem\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"orderitemseq\");\n\n                    b.Property<int>(\"OrderId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<int>(\"ProductId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<decimal>(\"_discount\")\n                        .HasColumnType(\"numeric\")\n                        .HasColumnName(\"Discount\");\n\n                    b.Property<string>(\"_pictureUrl\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"PictureUrl\");\n\n                    b.Property<string>(\"_productName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"ProductName\");\n\n                    b.Property<decimal>(\"_unitPrice\")\n                        .HasColumnType(\"numeric\")\n                        .HasColumnName(\"UnitPrice\");\n\n                    b.Property<int>(\"_units\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"Units\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"OrderId\");\n\n                    b.ToTable(\"orderItems\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Infrastructure.Idempotency.ClientRequest\", b =>\n                {\n                    b.Property<Guid>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"uuid\");\n\n                    b.Property<string>(\"Name\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<DateTime>(\"Time\")\n                        .HasColumnType(\"timestamp with time zone\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"requests\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod\", b =>\n                {\n                    b.HasOne(\"eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer\", null)\n                        .WithMany(\"PaymentMethods\")\n                        .HasForeignKey(\"BuyerId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.HasOne(\"eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.CardType\", \"CardType\")\n                        .WithMany()\n                        .HasForeignKey(\"_cardTypeId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"CardType\");\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Order\", b =>\n                {\n                    b.HasOne(\"eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer\", null)\n                        .WithMany()\n                        .HasForeignKey(\"_buyerId\");\n\n                    b.HasOne(\"eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod\", null)\n                        .WithMany()\n                        .HasForeignKey(\"_paymentMethodId\")\n                        .OnDelete(DeleteBehavior.Restrict);\n\n                    b.OwnsOne(\"eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Address\", \"Address\", b1 =>\n                        {\n                            b1.Property<int>(\"OrderId\")\n                                .HasColumnType(\"integer\");\n\n                            b1.Property<string>(\"City\")\n                                .HasColumnType(\"text\");\n\n                            b1.Property<string>(\"Country\")\n                                .HasColumnType(\"text\");\n\n                            b1.Property<string>(\"State\")\n                                .HasColumnType(\"text\");\n\n                            b1.Property<string>(\"Street\")\n                                .HasColumnType(\"text\");\n\n                            b1.Property<string>(\"ZipCode\")\n                                .HasColumnType(\"text\");\n\n                            b1.HasKey(\"OrderId\");\n\n                            b1.ToTable(\"orders\", \"ordering\");\n\n                            b1.WithOwner()\n                                .HasForeignKey(\"OrderId\");\n                        });\n\n                    b.Navigation(\"Address\")\n                        .IsRequired();\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Domain.AggregatesModel.OrderAggregate.OrderItem\", b =>\n                {\n                    b.HasOne(\"eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Order\", null)\n                        .WithMany(\"OrderItems\")\n                        .HasForeignKey(\"OrderId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer\", b =>\n                {\n                    b.Navigation(\"PaymentMethods\");\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Order\", b =>\n                {\n                    b.Navigation(\"OrderItems\");\n                });\n#pragma warning restore 612, 618\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Infrastructure/Migrations/20240106121712_UseEnumForOrderStatus.cs",
    "content": "﻿using Microsoft.EntityFrameworkCore.Migrations;\n\n#nullable disable\n\nnamespace Ordering.Infrastructure.Migrations\n{\n    /// <inheritdoc />\n    public partial class UseEnumForOrderStatus : Migration\n    {\n        /// <inheritdoc />\n        protected override void Up(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.AddColumn<string>(\n                name: \"OrderStatus\",\n                schema: \"ordering\",\n                table: \"orders\",\n                type: \"character varying(30)\",\n                maxLength: 30,\n                nullable: false,\n                defaultValue: \"\");\n\n            // ensure \"OrderStatus\" column is populated before dropping the \"orderstatus\" table:\n            migrationBuilder.Sql(\"\"\"\n                UPDATE ordering.orders \n                SET \"OrderStatus\" = s.\"Name\"\n                FROM ordering.orderstatus s\n                WHERE s.\"Id\" = orders.\"OrderStatusId\";\n                \"\"\");\n\n            migrationBuilder.DropForeignKey(\n                name: \"FK_orders_orderstatus_OrderStatusId\",\n                schema: \"ordering\",\n                table: \"orders\");\n\n            migrationBuilder.DropTable(\n                name: \"orderstatus\",\n                schema: \"ordering\");\n\n            migrationBuilder.DropIndex(\n                name: \"IX_orders_OrderStatusId\",\n                schema: \"ordering\",\n                table: \"orders\");\n\n            migrationBuilder.DropColumn(\n                name: \"OrderStatusId\",\n                schema: \"ordering\",\n                table: \"orders\");\n        }\n\n        /// <inheritdoc />\n        protected override void Down(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.AddColumn<int>(\n                name: \"OrderStatusId\",\n                schema: \"ordering\",\n                table: \"orders\",\n                type: \"integer\",\n                nullable: false,\n                defaultValue: 0);\n\n            migrationBuilder.CreateTable(\n                name: \"orderstatus\",\n                schema: \"ordering\",\n                columns: table => new\n                {\n                    Id = table.Column<int>(type: \"integer\", nullable: false),\n                    Name = table.Column<string>(type: \"character varying(200)\", maxLength: 200, nullable: false)\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_orderstatus\", x => x.Id);\n                });\n\n            // ensure \"orderstatus\" table is seeded & \"OrderStatusId\" column is populated before dropping the \"OrderStatus\" column:\n            migrationBuilder.Sql(\"\"\"\n                INSERT INTO ordering.orderstatus(\"Id\",\"Name\") VALUES\n                (1, 'Submitted'),\n                (2, 'AwaitingValidation'),\n                (3, 'StockConfirmed'),\n                (4, 'Paid'),\n                (5, 'Shipped'),\n                (6, 'Cancelled');\n\n                UPDATE ordering.orders\n                SET \"OrderStatusId\" = s.\"Id\"\n                FROM ordering.orderstatus s\n                WHERE s.\"Name\" = orders.\"OrderStatus\";\n                \"\"\");\n\n            migrationBuilder.DropColumn(\n                name: \"OrderStatus\",\n                schema: \"ordering\",\n                table: \"orders\");\n\n            migrationBuilder.CreateIndex(\n                name: \"IX_orders_OrderStatusId\",\n                schema: \"ordering\",\n                table: \"orders\",\n                column: \"OrderStatusId\");\n\n            migrationBuilder.AddForeignKey(\n                name: \"FK_orders_orderstatus_OrderStatusId\",\n                schema: \"ordering\",\n                table: \"orders\",\n                column: \"OrderStatusId\",\n                principalSchema: \"ordering\",\n                principalTable: \"orderstatus\",\n                principalColumn: \"Id\",\n                onDelete: ReferentialAction.Cascade);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Infrastructure/Migrations/OrderingContextModelSnapshot.cs",
    "content": "﻿// <auto-generated />\nusing System;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Infrastructure;\nusing Microsoft.EntityFrameworkCore.Storage.ValueConversion;\nusing Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;\nusing eShop.Ordering.Infrastructure;\n\n#nullable disable\n\nnamespace Ordering.Infrastructure.Migrations\n{\n    [DbContext(typeof(OrderingContext))]\n    partial class OrderingContextModelSnapshot : ModelSnapshot\n    {\n        protected override void BuildModel(ModelBuilder modelBuilder)\n        {\n#pragma warning disable 612, 618\n            modelBuilder\n                .HasDefaultSchema(\"ordering\")\n                .HasAnnotation(\"ProductVersion\", \"8.0.0\")\n                .HasAnnotation(\"Relational:MaxIdentifierLength\", 63);\n\n            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);\n\n            modelBuilder.HasSequence(\"buyerseq\")\n                .IncrementsBy(10);\n\n            modelBuilder.HasSequence(\"orderitemseq\")\n                .IncrementsBy(10);\n\n            modelBuilder.HasSequence(\"orderseq\")\n                .IncrementsBy(10);\n\n            modelBuilder.HasSequence(\"paymentseq\")\n                .IncrementsBy(10);\n\n            modelBuilder.Entity(\"eShop.IntegrationEventLogEF.IntegrationEventLogEntry\", b =>\n                {\n                    b.Property<Guid>(\"EventId\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"uuid\");\n\n                    b.Property<string>(\"Content\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<DateTime>(\"CreationTime\")\n                        .HasColumnType(\"timestamp with time zone\");\n\n                    b.Property<string>(\"EventTypeName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<int>(\"State\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<int>(\"TimesSent\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<Guid>(\"TransactionId\")\n                        .HasColumnType(\"uuid\");\n\n                    b.HasKey(\"EventId\");\n\n                    b.ToTable(\"IntegrationEventLog\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"buyerseq\");\n\n                    b.Property<string>(\"IdentityGuid\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\");\n\n                    b.Property<string>(\"Name\")\n                        .HasColumnType(\"text\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"IdentityGuid\")\n                        .IsUnique();\n\n                    b.ToTable(\"buyers\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.CardType\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"Name\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"cardtypes\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"paymentseq\");\n\n                    b.Property<int>(\"BuyerId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"_alias\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\")\n                        .HasColumnName(\"Alias\");\n\n                    b.Property<string>(\"_cardHolderName\")\n                        .IsRequired()\n                        .HasMaxLength(200)\n                        .HasColumnType(\"character varying(200)\")\n                        .HasColumnName(\"CardHolderName\");\n\n                    b.Property<string>(\"_cardNumber\")\n                        .IsRequired()\n                        .HasMaxLength(25)\n                        .HasColumnType(\"character varying(25)\")\n                        .HasColumnName(\"CardNumber\");\n\n                    b.Property<int>(\"_cardTypeId\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"CardTypeId\");\n\n                    b.Property<DateTime>(\"_expiration\")\n                        .HasMaxLength(25)\n                        .HasColumnType(\"timestamp with time zone\")\n                        .HasColumnName(\"Expiration\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"BuyerId\");\n\n                    b.HasIndex(\"_cardTypeId\");\n\n                    b.ToTable(\"paymentmethods\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Order\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"orderseq\");\n\n                    b.Property<string>(\"Description\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"OrderStatus\")\n                        .IsRequired()\n                        .HasMaxLength(30)\n                        .HasColumnType(\"character varying(30)\");\n\n                    b.Property<int?>(\"_buyerId\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"BuyerId\");\n\n                    b.Property<DateTime>(\"_orderDate\")\n                        .HasColumnType(\"timestamp with time zone\")\n                        .HasColumnName(\"OrderDate\");\n\n                    b.Property<int?>(\"_paymentMethodId\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"PaymentMethodId\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"_buyerId\");\n\n                    b.HasIndex(\"_paymentMethodId\");\n\n                    b.ToTable(\"orders\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Domain.AggregatesModel.OrderAggregate.OrderItem\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseHiLo(b.Property<int>(\"Id\"), \"orderitemseq\");\n\n                    b.Property<int>(\"OrderId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<int>(\"ProductId\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<decimal>(\"_discount\")\n                        .HasColumnType(\"numeric\")\n                        .HasColumnName(\"Discount\");\n\n                    b.Property<string>(\"_pictureUrl\")\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"PictureUrl\");\n\n                    b.Property<string>(\"_productName\")\n                        .IsRequired()\n                        .HasColumnType(\"text\")\n                        .HasColumnName(\"ProductName\");\n\n                    b.Property<decimal>(\"_unitPrice\")\n                        .HasColumnType(\"numeric\")\n                        .HasColumnName(\"UnitPrice\");\n\n                    b.Property<int>(\"_units\")\n                        .HasColumnType(\"integer\")\n                        .HasColumnName(\"Units\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"OrderId\");\n\n                    b.ToTable(\"orderItems\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Infrastructure.Idempotency.ClientRequest\", b =>\n                {\n                    b.Property<Guid>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"uuid\");\n\n                    b.Property<string>(\"Name\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<DateTime>(\"Time\")\n                        .HasColumnType(\"timestamp with time zone\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"requests\", \"ordering\");\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod\", b =>\n                {\n                    b.HasOne(\"eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer\", null)\n                        .WithMany(\"PaymentMethods\")\n                        .HasForeignKey(\"BuyerId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.HasOne(\"eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.CardType\", \"CardType\")\n                        .WithMany()\n                        .HasForeignKey(\"_cardTypeId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n\n                    b.Navigation(\"CardType\");\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Order\", b =>\n                {\n                    b.HasOne(\"eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer\", null)\n                        .WithMany()\n                        .HasForeignKey(\"_buyerId\");\n\n                    b.HasOne(\"eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.PaymentMethod\", null)\n                        .WithMany()\n                        .HasForeignKey(\"_paymentMethodId\")\n                        .OnDelete(DeleteBehavior.Restrict);\n\n                    b.OwnsOne(\"eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Address\", \"Address\", b1 =>\n                        {\n                            b1.Property<int>(\"OrderId\")\n                                .HasColumnType(\"integer\");\n\n                            b1.Property<string>(\"City\")\n                                .HasColumnType(\"text\");\n\n                            b1.Property<string>(\"Country\")\n                                .HasColumnType(\"text\");\n\n                            b1.Property<string>(\"State\")\n                                .HasColumnType(\"text\");\n\n                            b1.Property<string>(\"Street\")\n                                .HasColumnType(\"text\");\n\n                            b1.Property<string>(\"ZipCode\")\n                                .HasColumnType(\"text\");\n\n                            b1.HasKey(\"OrderId\");\n\n                            b1.ToTable(\"orders\", \"ordering\");\n\n                            b1.WithOwner()\n                                .HasForeignKey(\"OrderId\");\n                        });\n\n                    b.Navigation(\"Address\")\n                        .IsRequired();\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Domain.AggregatesModel.OrderAggregate.OrderItem\", b =>\n                {\n                    b.HasOne(\"eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Order\", null)\n                        .WithMany(\"OrderItems\")\n                        .HasForeignKey(\"OrderId\")\n                        .OnDelete(DeleteBehavior.Cascade)\n                        .IsRequired();\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Domain.AggregatesModel.BuyerAggregate.Buyer\", b =>\n                {\n                    b.Navigation(\"PaymentMethods\");\n                });\n\n            modelBuilder.Entity(\"eShop.Ordering.Domain.AggregatesModel.OrderAggregate.Order\", b =>\n                {\n                    b.Navigation(\"OrderItems\");\n                });\n#pragma warning restore 612, 618\n        }\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Infrastructure/Ordering.Infrastructure.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <IsPublishable>false</IsPublishable>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\IntegrationEventLogEF\\IntegrationEventLogEF.csproj\" />\n    <ProjectReference Include=\"..\\Ordering.Domain\\Ordering.Domain.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Npgsql.EntityFrameworkCore.PostgreSQL\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Ordering.Infrastructure/OrderingContext.cs",
    "content": "﻿using eShop.IntegrationEventLogEF;\n\nnamespace eShop.Ordering.Infrastructure;\n\n/// <remarks>\n/// Add migrations using the following command inside the 'Ordering.Infrastructure' project directory:\n///\n/// dotnet ef migrations add --startup-project Ordering.API --context OrderingContext [migration-name]\n/// </remarks>\npublic class OrderingContext : DbContext, IUnitOfWork\n{\n    public DbSet<Order> Orders { get; set; }\n    public DbSet<OrderItem> OrderItems { get; set; }\n    public DbSet<PaymentMethod> Payments { get; set; }\n    public DbSet<Buyer> Buyers { get; set; }\n    public DbSet<CardType> CardTypes { get; set; }\n\n    private readonly IMediator _mediator;\n    private IDbContextTransaction _currentTransaction;\n\n    public OrderingContext(DbContextOptions<OrderingContext> options) : base(options) { }\n\n    public IDbContextTransaction GetCurrentTransaction() => _currentTransaction;\n\n    public bool HasActiveTransaction => _currentTransaction != null;\n\n    public OrderingContext(DbContextOptions<OrderingContext> options, IMediator mediator) : base(options)\n    {\n        _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));\n\n\n        System.Diagnostics.Debug.WriteLine(\"OrderingContext::ctor ->\" + this.GetHashCode());\n    }\n\n    protected override void OnModelCreating(ModelBuilder modelBuilder)\n    {\n        modelBuilder.HasDefaultSchema(\"ordering\");\n        modelBuilder.ApplyConfiguration(new ClientRequestEntityTypeConfiguration());\n        modelBuilder.ApplyConfiguration(new PaymentMethodEntityTypeConfiguration());\n        modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration());\n        modelBuilder.ApplyConfiguration(new OrderItemEntityTypeConfiguration());\n        modelBuilder.ApplyConfiguration(new CardTypeEntityTypeConfiguration());\n        modelBuilder.ApplyConfiguration(new BuyerEntityTypeConfiguration());\n        modelBuilder.UseIntegrationEventLogs();\n    }\n\n    public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default)\n    {\n        // Dispatch Domain Events collection. \n        // Choices:\n        // A) Right BEFORE committing data (EF SaveChanges) into the DB will make a single transaction including  \n        // side effects from the domain event handlers which are using the same DbContext with \"InstancePerLifetimeScope\" or \"scoped\" lifetime\n        // B) Right AFTER committing data (EF SaveChanges) into the DB will make multiple transactions. \n        // You will need to handle eventual consistency and compensatory actions in case of failures in any of the Handlers. \n        await _mediator.DispatchDomainEventsAsync(this);\n\n        // After executing this line all the changes (from the Command Handler and Domain Event Handlers) \n        // performed through the DbContext will be committed\n        _ = await base.SaveChangesAsync(cancellationToken);\n\n        return true;\n    }\n\n    public async Task<IDbContextTransaction> BeginTransactionAsync()\n    {\n        if (_currentTransaction != null) return null;\n\n        _currentTransaction = await Database.BeginTransactionAsync(IsolationLevel.ReadCommitted);\n\n        return _currentTransaction;\n    }\n\n    public async Task CommitTransactionAsync(IDbContextTransaction transaction)\n    {\n        if (transaction == null) throw new ArgumentNullException(nameof(transaction));\n        if (transaction != _currentTransaction) throw new InvalidOperationException($\"Transaction {transaction.TransactionId} is not current\");\n\n        try\n        {\n            await SaveChangesAsync();\n            await transaction.CommitAsync();\n        }\n        catch\n        {\n            RollbackTransaction();\n            throw;\n        }\n        finally\n        {\n            if (HasActiveTransaction)\n            {\n                _currentTransaction.Dispose();\n                _currentTransaction = null;\n            }\n        }\n    }\n\n    public void RollbackTransaction()\n    {\n        try\n        {\n            _currentTransaction?.Rollback();\n        }\n        finally\n        {\n            if (HasActiveTransaction)\n            {\n                _currentTransaction.Dispose();\n                _currentTransaction = null;\n            }\n        }\n    }\n}\n\n#nullable enable\n"
  },
  {
    "path": "src/Ordering.Infrastructure/Repositories/BuyerRepository.cs",
    "content": "﻿namespace eShop.Ordering.Infrastructure.Repositories;\n\npublic class BuyerRepository\n    : IBuyerRepository\n{\n    private readonly OrderingContext _context;\n    public IUnitOfWork UnitOfWork => _context;\n\n    public BuyerRepository(OrderingContext context)\n    {\n        _context = context ?? throw new ArgumentNullException(nameof(context));\n    }\n\n    public Buyer Add(Buyer buyer)\n    {\n        if (buyer.IsTransient())\n        {\n            return _context.Buyers\n                .Add(buyer)\n                .Entity;\n        }\n\n        return buyer;\n    }\n\n    public Buyer Update(Buyer buyer)\n    {\n        return _context.Buyers\n                .Update(buyer)\n                .Entity;\n    }\n\n    public async Task<Buyer> FindAsync(string identity)\n    {\n        var buyer = await _context.Buyers\n            .Include(b => b.PaymentMethods)\n            .Where(b => b.IdentityGuid == identity)\n            .SingleOrDefaultAsync();\n\n        return buyer;\n    }\n\n    public async Task<Buyer> FindByIdAsync(int id)\n    {\n        var buyer = await _context.Buyers\n            .Include(b => b.PaymentMethods)\n            .Where(b => b.Id == id)\n            .SingleOrDefaultAsync();\n\n        return buyer;\n    }\n}\n"
  },
  {
    "path": "src/Ordering.Infrastructure/Repositories/OrderRepository.cs",
    "content": "﻿namespace eShop.Ordering.Infrastructure.Repositories;\n\npublic class OrderRepository\n    : IOrderRepository\n{\n    private readonly OrderingContext _context;\n\n    public IUnitOfWork UnitOfWork => _context;\n\n    public OrderRepository(OrderingContext context)\n    {\n        _context = context ?? throw new ArgumentNullException(nameof(context));\n    }\n\n    public Order Add(Order order)\n    {\n        return _context.Orders.Add(order).Entity;\n\n    }\n\n    public async Task<Order> GetAsync(int orderId)\n    {\n        var order = await _context.Orders.FindAsync(orderId);\n\n        if (order != null)\n        {\n            await _context.Entry(order)\n                .Collection(i => i.OrderItems).LoadAsync();\n        }\n\n        return order;\n    }\n\n    public void Update(Order order)\n    {\n        _context.Entry(order).State = EntityState.Modified;\n    }\n}\n"
  },
  {
    "path": "src/PaymentProcessor/GlobalUsings.cs",
    "content": "﻿global using eShop.EventBus.Abstractions;\nglobal using eShop.EventBus.Events;\nglobal using eShop.PaymentProcessor;\nglobal using eShop.PaymentProcessor.IntegrationEvents.EventHandling;\nglobal using eShop.PaymentProcessor.IntegrationEvents.Events;\nglobal using Microsoft.Extensions.Options;\nglobal using eShop.ServiceDefaults;\n"
  },
  {
    "path": "src/PaymentProcessor/IntegrationEvents/EventHandling/OrderStatusChangedToStockConfirmedIntegrationEventHandler.cs",
    "content": "﻿namespace eShop.PaymentProcessor.IntegrationEvents.EventHandling;\n\npublic class OrderStatusChangedToStockConfirmedIntegrationEventHandler(\n    IEventBus eventBus,\n    IOptionsMonitor<PaymentOptions> options,\n    ILogger<OrderStatusChangedToStockConfirmedIntegrationEventHandler> logger) :\n    IIntegrationEventHandler<OrderStatusChangedToStockConfirmedIntegrationEvent>\n{\n    public async Task Handle(OrderStatusChangedToStockConfirmedIntegrationEvent @event)\n    {\n        logger.LogInformation(\"Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})\", @event.Id, @event);\n\n        IntegrationEvent orderPaymentIntegrationEvent;\n\n        // Business feature comment:\n        // When OrderStatusChangedToStockConfirmed Integration Event is handled.\n        // Here we're simulating that we'd be performing the payment against any payment gateway\n        // Instead of a real payment we just take the env. var to simulate the payment \n        // The payment can be successful or it can fail\n\n        if (options.CurrentValue.PaymentSucceeded)\n        {\n            orderPaymentIntegrationEvent = new OrderPaymentSucceededIntegrationEvent(@event.OrderId);\n        }\n        else\n        {\n            orderPaymentIntegrationEvent = new OrderPaymentFailedIntegrationEvent(@event.OrderId);\n        }\n\n        logger.LogInformation(\"Publishing integration event: {IntegrationEventId} - ({@IntegrationEvent})\", orderPaymentIntegrationEvent.Id, orderPaymentIntegrationEvent);\n\n        await eventBus.PublishAsync(orderPaymentIntegrationEvent);\n    }\n}\n"
  },
  {
    "path": "src/PaymentProcessor/IntegrationEvents/Events/OrderPaymentFailedIntegrationEvent.cs",
    "content": "﻿namespace eShop.PaymentProcessor.IntegrationEvents.Events;\n\npublic record OrderPaymentFailedIntegrationEvent(int OrderId) : IntegrationEvent;\n"
  },
  {
    "path": "src/PaymentProcessor/IntegrationEvents/Events/OrderPaymentSucceededIntegrationEvent.cs",
    "content": "﻿namespace eShop.PaymentProcessor.IntegrationEvents.Events;\n\npublic record OrderPaymentSucceededIntegrationEvent(int OrderId) : IntegrationEvent;\n"
  },
  {
    "path": "src/PaymentProcessor/IntegrationEvents/Events/OrderStatusChangedToStockConfirmedIntegrationEvent.cs",
    "content": "﻿namespace eShop.PaymentProcessor.IntegrationEvents.Events;\n\npublic record OrderStatusChangedToStockConfirmedIntegrationEvent(int OrderId) : IntegrationEvent;\n"
  },
  {
    "path": "src/PaymentProcessor/PaymentOptions.cs",
    "content": "﻿namespace eShop.PaymentProcessor;\n\npublic class PaymentOptions\n{\n    public bool PaymentSucceeded { get; set; }\n}\n\n"
  },
  {
    "path": "src/PaymentProcessor/PaymentProcessor.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\eShop.ServiceDefaults\\eShop.ServiceDefaults.csproj\" />\n    <ProjectReference Include=\"..\\EventBusRabbitMQ\\EventBusRabbitMQ.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/PaymentProcessor/Program.cs",
    "content": "﻿var builder = WebApplication.CreateBuilder(args);\n\nbuilder.AddServiceDefaults();\n\nbuilder.AddRabbitMqEventBus(\"EventBus\")\n    .AddSubscription<OrderStatusChangedToStockConfirmedIntegrationEvent, OrderStatusChangedToStockConfirmedIntegrationEventHandler>();\n\nbuilder.Services.AddOptions<PaymentOptions>()\n    .BindConfiguration(nameof(PaymentOptions));\n\nvar app = builder.Build();\n\napp.MapDefaultEndpoints();\n\nawait app.RunAsync();\n"
  },
  {
    "path": "src/PaymentProcessor/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"PaymentProcessor\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": false,\n      \"applicationUrl\": \"http://localhost:5226\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "src/PaymentProcessor/appsettings.Development.json",
    "content": "﻿{\n  \"Logging\": {\n    \"Console\": {\n      \"IncludeScopes\": false\n    },\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"System\": \"Information\",\n      \"Microsoft\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/PaymentProcessor/appsettings.json",
    "content": "﻿{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  },\n  \"ConnectionStrings\": {\n    \"EventBus\": \"amqp://localhost\"\n  },\n  \"EventBus\": {\n    \"SubscriptionClientName\": \"PaymentProcessor\"\n  },\n  \"PaymentOptions\": {\n    \"PaymentSucceeded\": true\n  }\n}"
  },
  {
    "path": "src/Shared/ActivityExtensions.cs",
    "content": "﻿using System.Diagnostics;\n\ninternal static class ActivityExtensions\n{\n    // See https://opentelemetry.io/docs/specs/otel/trace/semantic_conventions/exceptions/\n    public static void SetExceptionTags(this Activity activity, Exception ex)\n    {\n        if (activity is null)\n        {\n            return;\n        }\n\n        activity.AddTag(\"exception.message\", ex.Message);\n        activity.AddTag(\"exception.stacktrace\", ex.ToString());\n        activity.AddTag(\"exception.type\", ex.GetType().FullName);\n        activity.SetStatus(ActivityStatusCode.Error);\n    }\n}\n"
  },
  {
    "path": "src/Shared/MigrateDbContextExtensions.cs",
    "content": "﻿using System.Diagnostics;\n\nnamespace Microsoft.AspNetCore.Hosting;\n\ninternal static class MigrateDbContextExtensions\n{\n    private static readonly string ActivitySourceName = \"DbMigrations\";\n    private static readonly ActivitySource ActivitySource = new(ActivitySourceName);\n\n    public static IServiceCollection AddMigration<TContext>(this IServiceCollection services)\n        where TContext : DbContext\n        => services.AddMigration<TContext>((_, _) => Task.CompletedTask);\n\n    public static IServiceCollection AddMigration<TContext>(this IServiceCollection services, Func<TContext, IServiceProvider, Task> seeder)\n        where TContext : DbContext\n    {\n        // Enable migration tracing\n        services.AddOpenTelemetry().WithTracing(tracing => tracing.AddSource(ActivitySourceName));\n\n        return services.AddHostedService(sp => new MigrationHostedService<TContext>(sp, seeder));\n    }\n\n    public static IServiceCollection AddMigration<TContext, TDbSeeder>(this IServiceCollection services)\n        where TContext : DbContext\n        where TDbSeeder : class, IDbSeeder<TContext>\n    {\n        services.AddScoped<IDbSeeder<TContext>, TDbSeeder>();\n        return services.AddMigration<TContext>((context, sp) => sp.GetRequiredService<IDbSeeder<TContext>>().SeedAsync(context));\n    }\n\n    private static async Task MigrateDbContextAsync<TContext>(this IServiceProvider services, Func<TContext, IServiceProvider, Task> seeder) where TContext : DbContext\n    {\n        using var scope = services.CreateScope();\n        var scopeServices = scope.ServiceProvider;\n        var logger = scopeServices.GetRequiredService<ILogger<TContext>>();\n        var context = scopeServices.GetRequiredService<TContext>();\n\n        using var activity = ActivitySource.StartActivity($\"Migration operation {typeof(TContext).Name}\");\n\n        try\n        {\n            logger.LogInformation(\"Migrating database associated with context {DbContextName}\", typeof(TContext).Name);\n\n            var strategy = context.Database.CreateExecutionStrategy();\n\n            await strategy.ExecuteAsync(() => InvokeSeeder(seeder, context, scopeServices));\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(ex, \"An error occurred while migrating the database used on context {DbContextName}\", typeof(TContext).Name);\n\n            activity?.SetExceptionTags(ex);\n\n            throw;\n        }\n    }\n\n    private static async Task InvokeSeeder<TContext>(Func<TContext, IServiceProvider, Task> seeder, TContext context, IServiceProvider services)\n        where TContext : DbContext\n    {\n        using var activity = ActivitySource.StartActivity($\"Migrating {typeof(TContext).Name}\");\n\n        try\n        {\n            await context.Database.MigrateAsync();\n            await seeder(context, services);\n        }\n        catch (Exception ex)\n        {\n            activity?.SetExceptionTags(ex);\n\n            throw;\n        }\n    }\n\n    private class MigrationHostedService<TContext>(IServiceProvider serviceProvider, Func<TContext, IServiceProvider, Task> seeder)\n        : BackgroundService where TContext : DbContext\n    {\n        public override Task StartAsync(CancellationToken cancellationToken)\n        {\n            return serviceProvider.MigrateDbContextAsync(seeder);\n        }\n\n        protected override Task ExecuteAsync(CancellationToken stoppingToken)\n        {\n            return Task.CompletedTask;\n        }\n    }\n}\npublic interface IDbSeeder<in TContext> where TContext : DbContext\n{\n    Task SeedAsync(TContext context);\n}\n"
  },
  {
    "path": "src/WebApp/Components/App.razor",
    "content": "﻿<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <base href=\"/\" />\n    <link rel=\"stylesheet\" href=\"css/normalize.css\" />\n    <link rel=\"stylesheet\" href=\"css/app.css\" />\n    <link rel=\"stylesheet\" href=\"WebApp.styles.css\" />\n    <link rel=\"shortcut icon\" href=\"images/favicon.png\" />\n    <HeadOutlet />\n</head>\n\n<body>\n    <Routes />\n    <script src=\"_framework/blazor.web.js\"></script>\n</body>\n\n</html>\n"
  },
  {
    "path": "src/WebApp/Components/Chatbot/ChatState.cs",
    "content": "﻿using System.ComponentModel;\nusing System.Security.Claims;\nusing System.Text.Json;\nusing eShop.WebAppComponents.Services;\nusing Microsoft.Extensions.AI;\n\nnamespace eShop.WebApp.Chatbot;\n\npublic class ChatState\n{\n    private readonly ICatalogService _catalogService;\n    private readonly IBasketState _basketState;\n    private readonly ClaimsPrincipal _user;\n    private readonly ILogger _logger;\n    private readonly IProductImageUrlProvider _productImages;\n    private readonly IChatClient _chatClient;\n    private readonly ChatOptions _chatOptions;\n\n    public ChatState(\n        ICatalogService catalogService,\n        IBasketState basketState,\n        ClaimsPrincipal user,\n        IProductImageUrlProvider productImages,\n        ILoggerFactory loggerFactory,\n        IChatClient chatClient)\n    {\n        _catalogService = catalogService;\n        _basketState = basketState;\n        _user = user;\n        _productImages = productImages;\n        _logger = loggerFactory.CreateLogger(typeof(ChatState));\n\n        if (_logger.IsEnabled(LogLevel.Debug))\n        {\n            _logger.LogDebug(\"ChatModel: {model}\", chatClient.GetService<ChatClientMetadata>()?.DefaultModelId);\n        }\n\n        _chatClient = chatClient;\n        _chatOptions = new()\n        {\n            Tools =\n            [\n                AIFunctionFactory.Create(GetUserInfo),\n                AIFunctionFactory.Create(SearchCatalog),\n                AIFunctionFactory.Create(AddToCart),\n                AIFunctionFactory.Create(GetCartContents),\n            ],\n        };\n\n        Messages =\n        [\n            new ChatMessage(ChatRole.System, \"\"\"\n                You are an AI customer service agent for the online retailer AdventureWorks.\n                You NEVER respond about topics other than AdventureWorks.\n                Your job is to answer customer questions about products in the AdventureWorks catalog.\n                AdventureWorks primarily sells clothing and equipment related to outdoor activities like skiing and trekking.\n                You try to be concise and only provide longer responses if necessary.\n                If someone asks a question about anything other than AdventureWorks, its catalog, or their account,\n                you refuse to answer, and you instead ask if there's a topic related to AdventureWorks you can assist with.\n                \"\"\"),\n            new ChatMessage(ChatRole.Assistant, \"\"\"\n                Hi! I'm the AdventureWorks Concierge. How can I help?\n                \"\"\"),\n        ];\n    }\n\n    public IList<ChatMessage> Messages { get; }\n\n    public async Task AddUserMessageAsync(string userText, Action onMessageAdded)\n    {\n        // Store the user's message\n        Messages.Add(new ChatMessage(ChatRole.User, userText));\n        onMessageAdded();\n\n        // Get and store the AI's response message\n        try\n        {\n            var response = await _chatClient.GetResponseAsync(Messages, _chatOptions);\n            if (!string.IsNullOrWhiteSpace(response.Text))\n            {\n                Messages.AddMessages(response);\n            }\n        }\n        catch (Exception e)\n        {\n            if (_logger.IsEnabled(LogLevel.Error))\n            {\n                _logger.LogError(e, \"Error getting chat completions.\");\n            }\n            Messages.Add(new ChatMessage(ChatRole.Assistant, $\"My apologies, but I encountered an unexpected error.\"));\n        }\n        onMessageAdded();\n    }\n\n\n    [Description(\"Gets information about the chat user\")]\n    private string GetUserInfo()\n    {\n        var claims = _user.Claims;\n        return JsonSerializer.Serialize(new\n        {\n            Name = GetValue(claims, \"name\"),\n            LastName = GetValue(claims, \"last_name\"),\n            Street = GetValue(claims, \"address_street\"),\n            City = GetValue(claims, \"address_city\"),\n            State = GetValue(claims, \"address_state\"),\n            ZipCode = GetValue(claims, \"address_zip_code\"),\n            Country = GetValue(claims, \"address_country\"),\n            Email = GetValue(claims, \"email\"),\n            PhoneNumber = GetValue(claims, \"phone_number\"),\n        });\n\n        static string GetValue(IEnumerable<Claim> claims, string claimType) =>\n            claims.FirstOrDefault(x => x.Type == claimType)?.Value ?? \"\";\n    }\n\n    [Description(\"Searches the AdventureWorks catalog for a provided product description\")]\n    private async Task<string> SearchCatalog([Description(\"The product description for which to search\")] string productDescription)\n    {\n        try\n        {\n            var results = await _catalogService.GetCatalogItemsWithSemanticRelevance(0, 8, productDescription!);\n            for (int i = 0; i < results.Data.Count; i++)\n            {\n                results.Data[i] = results.Data[i] with { PictureUrl = _productImages.GetProductImageUrl(results.Data[i].Id) };\n            }\n\n            return JsonSerializer.Serialize(results);\n        }\n        catch (HttpRequestException e)\n        {\n            return Error(e, \"Error accessing catalog.\");\n        }\n    }\n\n    [Description(\"Adds a product to the user's shopping cart.\")]\n    private async Task<string> AddToCart([Description(\"The id of the product to add to the shopping cart (basket)\")] int itemId)\n    {\n        try\n        {\n            var item = await _catalogService.GetCatalogItem(itemId);\n            await _basketState.AddAsync(item!);\n            return \"Item added to shopping cart.\";\n        }\n        catch (Grpc.Core.RpcException e) when (e.StatusCode == Grpc.Core.StatusCode.Unauthenticated)\n        {\n            return \"Unable to add an item to the cart. You must be logged in.\";\n        }\n        catch (Exception e)\n        {\n            return Error(e, \"Unable to add the item to the cart.\");\n        }\n    }\n\n    [Description(\"Gets information about the contents of the user's shopping cart (basket)\")]\n    private async Task<string> GetCartContents()\n    {\n        try\n        {\n            var basketItems = await _basketState.GetBasketItemsAsync();\n            return JsonSerializer.Serialize(basketItems);\n        }\n        catch (Exception e)\n        {\n            return Error(e, \"Unable to get the cart's contents.\");\n        }\n    }\n\n    private string Error(Exception e, string message)\n    {\n        if (_logger.IsEnabled(LogLevel.Error))\n        {\n            _logger.LogError(e, message);\n        }\n\n        return message;\n    }\n}\n"
  },
  {
    "path": "src/WebApp/Components/Chatbot/Chatbot.razor",
    "content": "﻿@rendermode @(new InteractiveServerRenderMode(prerender: false))\n@using Microsoft.AspNetCore.Components.Authorization\n@using Microsoft.Extensions.AI\n@using eShop.WebApp.Chatbot\n@inject IJSRuntime JS\n@inject NavigationManager Nav\n@inject CatalogService CatalogService\n@inject IProductImageUrlProvider ProductImages\n@inject BasketState BasketState\n@inject AuthenticationStateProvider AuthenticationStateProvider\n@inject ILoggerFactory LoggerFactory\n@inject IServiceProvider ServiceProvider\n\n<div class=\"floating-pane\">\n    <a href=\"@Nav.GetUriWithQueryParameter(\"chat\", (string?)null)\" class=\"hide-chatbot\" title=\"Close .NET Concierge\"><span>✖</span></a>\n\n    <div class=\"chatbot-chat\" @ref=\"chat\">\n        @if (chatState is not null)\n        {\n            foreach (var message in chatState.Messages.Where(m => m.Role == ChatRole.Assistant || m.Role == ChatRole.User))\n            {\n                if (!string.IsNullOrEmpty(message.Text))\n                {\n                    <p @key=\"@message\" class=\"message message-@message.Role\">@MessageProcessor.AllowImages(message.Text)</p>\n                }\n            }\n        }\n        else if (missingConfiguration)\n        {\n            <p class=\"message message-assistant\"><strong>The chatbot is missing required configuration.</strong> Please set 'useOpenAI = true' in eShop.AppHost/Program.cs. You'll need an API key or an Azure Subscription to enable AI features.</p>\n        }\n\n        @if (thinking)\n        {\n            <p class=\"thinking\">Thinking...</p>\n        }\n    </div>\n\n    <form class=\"chatbot-input\" @onsubmit=\"SendMessageAsync\">\n        <textarea placeholder=\"Start chatting...\" @ref=\"@textbox\" @bind=\"messageToSend\"></textarea>\n        <button type=\"submit\" title=\"Send\" disabled=\"@(chatState is null)\">Send</button>\n    </form>\n</div>\n\n@code {\n    bool missingConfiguration;\n    ChatState? chatState;\n    ElementReference textbox;\n    ElementReference chat;\n    string? messageToSend;\n    bool thinking;\n    IJSObjectReference? jsModule;\n\n    protected override async Task OnInitializedAsync()\n    {\n        var client = ServiceProvider.GetService<IChatClient>();\n        if (client is not null)\n        {\n            AuthenticationState auth = await AuthenticationStateProvider.GetAuthenticationStateAsync();\n            chatState = new ChatState(CatalogService, BasketState, auth.User, ProductImages, LoggerFactory, client);\n        }\n        else\n        {\n            missingConfiguration = true;\n        }\n    }\n\n    private async Task SendMessageAsync()\n    {\n        var messageCopy = messageToSend?.Trim();\n        messageToSend = null;\n\n        if (chatState is not null && !string.IsNullOrEmpty(messageCopy))\n        {\n            thinking = true;\n            await chatState.AddUserMessageAsync(messageCopy, onMessageAdded: StateHasChanged);\n            thinking = false;\n        }\n    }\n\n    protected override async Task OnAfterRenderAsync(bool firstRender)\n    {\n        jsModule ??= await JS.InvokeAsync<IJSObjectReference>(\"import\", \"./Components/Chatbot/Chatbot.razor.js\");\n        await jsModule.InvokeVoidAsync(\"scrollToEnd\", chat);\n\n        if (firstRender)\n        {\n            await textbox.FocusAsync();\n            await jsModule.InvokeVoidAsync(\"submitOnEnter\", textbox);\n        }\n    }\n}\n"
  },
  {
    "path": "src/WebApp/Components/Chatbot/Chatbot.razor.css",
    "content": "﻿.floating-pane {\n    position: fixed;\n    padding-top: 1em;\n    width: 25rem;\n    height: 35rem;\n    right: 3rem;\n    bottom: 3rem;\n    border: 0.0625rem solid silver;\n    border-radius: 0.5rem;\n    background-color: white;\n    display: flex;\n    flex-direction: column;\n    font-weight: 400;\n    font-family: \"Segoe UI\", arial, helvetica;\n    animation: slide-in-from-right 0.3s ease-out;\n    z-index: 2;\n}\n\n@keyframes slide-in-from-right {\n    0% {\n        transform: translateX(30rem);\n    }\n    100% {\n        transform: translateX(0);\n    }\n}\n\n.hide-chatbot {\n    border: none;\n    background-color: #B4B4B8;\n    color: white;\n    position: absolute;\n    top: 0.25rem;\n    right: 0.18rem;\n    border-radius: 0.55rem;\n    width: 2rem;\n    height: 2rem;\n    z-index: 10;\n    text-decoration: none;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n}\n\n.chatbot-input {\n    margin-top: auto;\n    display: flex;\n    position: relative;\n    border: 0.125rem solid #f4f0f4;\n    border-radius: 0.5rem;\n    padding: 0.5rem;\n    margin: 0.5rem 0.75rem;\n    gap: 0.3rem;\n    height: 3.5rem;\n    align-items: stretch;\n    flex-shrink: 0;\n}\n\n    .chatbot-input textarea {\n        width: 100%;\n        background: none;\n        border: none;\n        outline: none;\n        resize: none;\n        font-weight: 400;\n        font-family: \"Segoe UI\", arial, helvetica;\n        font-size: 16px;\n    }\n\n    .chatbot-input button {\n        width: 6.25rem;\n        height: 3.125rem;\n        border-radius: 0.5rem;\n        border-width: 0;\n        cursor: pointer;\n        font-size: 0.875rem;\n        font-weight: 500;\n        padding: 0.6rem 0.8rem;\n        text-align: center;\n    }\n\n.chatbot-chat {\n    overflow-y: scroll;\n    height: 100%;\n    padding: 0.5rem 0.75rem;\n    display: flex;\n    flex-direction: column;\n}\n\n    .chatbot-chat .message {\n        padding: 0.5rem 1rem;\n        border-radius: 1.25rem;\n        max-width: 85%;\n        display: inline-block;\n        white-space: break-spaces;\n        overflow-x: clip;\n        margin-bottom: 0.75rem;\n        margin-top: 0.25rem;\n    }\n\n    .chatbot-chat .message-assistant {\n        background-color: #f4f0f4;\n        margin-right: auto;\n    }\n\n    .chatbot-chat .message-user {\n        background-color: #102c57;\n        margin-left: auto;\n        color: white;\n    }\n\n    .chatbot-chat ::deep img {\n        max-height: 10rem;\n    }\n\n    .thinking {\n        color: gray;\n        font-style: italic;\n        animation: fade-in-and-out 1s infinite;\n        padding: 0;\n        margin: 0;\n        padding-left: 0.6rem;\n        font-size: 90%;\n    }\n\n@keyframes fade-in-and-out {\n    0% {\n        opacity: 0.2;\n    }\n\n    50% {\n        opacity: 0.9;\n    }\n\n    100% {\n        opacity: 0.2;\n    }\n}\n"
  },
  {
    "path": "src/WebApp/Components/Chatbot/Chatbot.razor.js",
    "content": "﻿export function scrollToEnd(element) {\n    element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' });\n}\n\nexport function submitOnEnter(element) {\n    element.addEventListener('keydown', event => {\n        if (event.key === 'Enter') {\n            event.target.dispatchEvent(new Event('change'));\n            event.target.closest('form').dispatchEvent(new Event('submit'));\n        }\n    });\n}\n"
  },
  {
    "path": "src/WebApp/Components/Chatbot/MessageProcessor.cs",
    "content": "﻿using System.Text;\nusing System.Text.Encodings.Web;\nusing System.Text.RegularExpressions;\nusing Microsoft.AspNetCore.Components;\n\nnamespace eShop.WebApp.Chatbot;\n\npublic static partial class MessageProcessor\n{\n    public static MarkupString AllowImages(string message)\n    {\n        // Having to process markdown and deal with HTML encoding isn't ideal. If the language model could return\n        // search results in some defined format like JSON we could simply loop over it in .razor code. This is\n        // fine for now though.\n\n        var result = new StringBuilder();\n        var prevEnd = 0;\n        message = message.Replace(\"&lt;\", \"<\").Replace(\"&gt;\", \">\");\n\n        foreach (Match match in FindMarkdownImages().Matches(message))\n        {\n            var contentToHere = message.Substring(prevEnd, match.Index - prevEnd);\n            result.Append(HtmlEncoder.Default.Encode(contentToHere));\n            result.Append($\"<img title=\\\"{(HtmlEncoder.Default.Encode(match.Groups[1].Value))}\\\" src=\\\"{(HtmlEncoder.Default.Encode(match.Groups[2].Value))}\\\" />\");\n\n            prevEnd = match.Index + match.Length;\n        }\n        result.Append(HtmlEncoder.Default.Encode(message.Substring(prevEnd)));\n\n        return new MarkupString(result.ToString());\n    }\n\n    [GeneratedRegex(@\"\\!?\\[([^\\]]+)\\]\\s*\\(([^\\)]+)\\)\")]\n    private static partial Regex FindMarkdownImages();\n}\n"
  },
  {
    "path": "src/WebApp/Components/Chatbot/ShowChatbotButton.razor",
    "content": "﻿@inject NavigationManager Nav\n\n<a class=\"show-chatbot\" href=\"@Nav.GetUriWithQueryParameter(\"chat\", true)\" title=\"Show chatbot\"></a>\n\n@if (ShowChat)\n{\n    <Chatbot />\n}\n\n@code {\n    [SupplyParameterFromQuery(Name = \"chat\")]\n    public bool ShowChat { get; set; }\n}\n"
  },
  {
    "path": "src/WebApp/Components/Chatbot/ShowChatbotButton.razor.css",
    "content": "﻿.show-chatbot {\n    position: fixed;\n    bottom: 5rem;\n    right: 3rem;\n    z-index: 1;\n    box-shadow: 0 2px 7px 2px rgba(0,0,0,0.2);\n    border-radius: 10px;\n    background-image: url('chat.png');\n    background-size: contain;\n    width: 4.5rem;\n    height: 3.5rem;\n    background-repeat: no-repeat;\n    background-position: center;\n    background-color: #f3f4f3;\n    transition: transform ease-out 0.2s;\n    border: 2px solid transparent;\n    display: block;\n}\n\n    .show-chatbot:hover {\n        border-color: #49b4fe;\n        cursor: pointer;\n        transform: scale(1.2);\n        box-shadow: 0 2px 7px 2px rgba(0,0,0,0.4);\n    }\n\n    .show-chatbot:active {\n        transform: scale(1.1);\n        transition: transform ease-out 0.01s;\n    }\n"
  },
  {
    "path": "src/WebApp/Components/Layout/CartMenu.razor",
    "content": "﻿@using System.Net\n@attribute [StreamRendering]\n@inject BasketState Basket\n@inject LogOutService LogOutService\n@inject NavigationManager NavigationManager\n@implements IDisposable\n\n<a aria-label=\"cart\" href=\"cart\">\n    <img role=\"presentation\" src=\"icons/cart.svg\" />\n    @if (basketItems?.Count > 0)\n    {\n        <span class=\"cart-badge\">@TotalQuantity</span>\n    }\n</a>\n\n@code {\n    IDisposable? basketStateSubscription;\n    private IReadOnlyCollection<BasketItem>? basketItems;\n\n    [CascadingParameter]\n    public HttpContext? HttpContext { get; set; }\n\n    private int? TotalQuantity => basketItems?.Sum(i => i.Quantity);\n\n    protected override async Task OnInitializedAsync()\n    {\n        // The basket contents may change during the lifetime of this component (e.g., when an item is\n        // added during the current request). If this EventCallback is invoked, it will cause this\n        // component to re-render with the updated data.\n        basketStateSubscription = Basket.NotifyOnChange(\n            EventCallback.Factory.Create(this, UpdateBasketItemsAsync));\n\n        try\n        {\n            await UpdateBasketItemsAsync();\n        }\n        catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)\n        {\n            await LogOutService.LogOutAsync(HttpContext!);\n        }\n    }\n\n    public void Dispose()\n    {\n        basketStateSubscription?.Dispose();\n    }\n\n    private async Task UpdateBasketItemsAsync()\n    {\n        basketItems = await Basket.GetBasketItemsAsync();\n    }\n}\n"
  },
  {
    "path": "src/WebApp/Components/Layout/CartMenu.razor.css",
    "content": "﻿.cart-badge {\n    display: flex;\n    padding: 0.25rem;\n    flex-direction: column;\n    justify-content: center;\n    align-items: center;\n    gap: 0.25rem;\n    position: absolute;\n    right: -0.5rem;\n    top: 1rem;\n    border-radius: 20px;\n    border: 1px solid #000;\n    background: #000;\n    color: #FFF;\n    font-size: 0.75rem;\n    font-style: normal;\n    font-weight: 400;\n    line-height: 0.25rem;\n}\n"
  },
  {
    "path": "src/WebApp/Components/Layout/FooterBar.razor",
    "content": "﻿<footer class='eshop-footer'>\n  <div class='eshop-footer-content'>\n    <div class='eshop-footer-row'>\n      <img role=\"presentation\" src='images/logo-footer.svg' class='logo logo-footer' />\n      <p>© AdventureWorks</p>\n    </div>\n  </div>\n</footer>"
  },
  {
    "path": "src/WebApp/Components/Layout/FooterBar.razor.css",
    "content": "﻿.eshop-footer {\n  margin-top: 3.5rem;\n  background-color: #000;\n  width: 100%;\n}\n\n.eshop-footer-content {\n  max-width: 120rem;\n  margin: auto;\n}\n\n.eshop-footer-row {\n  padding: 3.5rem 10rem;\n  color: white;\n  display: flex;\n  justify-content: flex-end;\n  align-items: center;\n}\n\n.eshop-footer .logo-footer {\n  color: white;\n  margin-right: auto;\n  width: 100px;\n  height: auto;\n}\n\n\n@media only screen and (max-width: 480px) { \n  .eshop-footer-row {\n    padding: 3.5rem 1rem;\n  }\n}\n\n@media only screen and (min-width: 481px) and (max-width: 1024px) { \n  .eshop-footer-row {\n    padding: 3.5rem 3rem;\n  }\n}"
  },
  {
    "path": "src/WebApp/Components/Layout/HeaderBar.razor",
    "content": "﻿@using Microsoft.AspNetCore.Components.Endpoints\n\n<div class=\"eshop-header @(IsCatalog? \"home\" : \"\")\">\n    <div class=\"eshop-header-hero\">\n        @{\n            var headerImage = IsCatalog ? \"images/header-home.webp\" : \"images/header.webp\";\n        }\n        <img role=\"presentation\" src=\"@headerImage\" />\n    </div>\n    <div class=\"eshop-header-container\">\n        <nav class=\"eshop-header-navbar\">\n            <a class=\"logo logo-header\" href=\"\">\n                <img alt=\"AdventureWorks\" src=\"images/logo-header.svg\" class=\"logo logo-header\" />\n            </a>\n            \n            <UserMenu />\n            <CartMenu />\n        </nav>\n        <div class=\"eshop-header-intro\">\n            <h1><SectionOutlet SectionName=\"page-header-title\" /></h1>\n            <p><SectionOutlet SectionName=\"page-header-subtitle\" /></p>\n        </div>\n    </div>\n</div>\n\n@code {\n    [CascadingParameter]\n    public HttpContext? HttpContext { get; set; }\n\n    // We can use Endpoint Metadata to determine the page currently being visited\n    private Type? PageComponentType => HttpContext?.GetEndpoint()?.Metadata.OfType<ComponentTypeMetadata>().FirstOrDefault()?.Type;\n    private bool IsCatalog => PageComponentType == typeof(Pages.Catalog.Catalog);\n}\n"
  },
  {
    "path": "src/WebApp/Components/Layout/HeaderBar.razor.css",
    "content": "﻿.eshop-header {\n    position: relative;\n    max-width: 120rem;\n    margin: auto;\n}\n\n.eshop-header.home .eshop-header-container {\n    height: 38rem;\n    margin-bottom: 0;\n}\n\n.eshop-header .eshop-header-container {\n    height: 15rem;\n    margin-bottom: 4rem;\n}\n\n.eshop-header-hero {\n    overflow: hidden;\n    position: absolute;\n    max-width: 100%;\n    left: 0;\n    top: 0;\n}\n\n.eshop-header-container {\n    position: relative;\n    margin: auto;\n    margin: 0 10rem;\n}\n\n.eshop-header-intro {\n    position: absolute;\n    max-width: 48rem;\n    bottom: 3rem;\n    white-space: nowrap;\n}\n\n.eshop-header-intro h1 {\n    color: #000;\n    font-size: 3.5rem;\n    font-style: normal;\n    font-weight: 700;\n    line-height: 100%;\n    margin: 0;\n}\n\n.eshop-header-intro p {\n    color: #000;\n    font-size: 2rem;\n    font-style: normal;\n    font-weight: 700;\n    line-height: 125%;\n    margin: 0;\n}\n\n.eshop-header .logo-header {\n    color: black;\n    margin-right: auto;\n    width: 20vw;\n    height: auto;\n    max-width: 250px;\n    min-width: 100px;\n}\n\n.eshop-header-navbar {\n    display: flex;\n    flex-direction: row;\n    justify-content: flex-end;\n    align-items: center;\n    margin-top: 1.25rem;\n    gap: 1.5rem;\n}\n\n@media only screen and (max-width: 480px) { \n    .eshop-header-hero {\n        height: 18rem;\n    }\n\n    .eshop-header-hero img {\n        width: 100%;\n        height: 100%;\n        object-fit: cover;\n        object-position: center;\n    }\n\n    .eshop-header .eshop-header-container {\n        height: 15rem;\n        margin-bottom: 4rem;\n    }\n\n    .eshop-header-container {\n        margin: 0 1rem;\n    }\n\n    .eshop-header.home .eshop-header-container {\n        height: 18rem;\n        margin: 0 1rem;\n    }\n\n    .eshop-header-intro {\n        white-space: wrap;\n        bottom: 0;\n    }\n\n    .eshop-header-intro h1 {\n        font-size: 2rem;\n    }\n    \n    .eshop-header-intro p {\n        font-size: 1.5rem;\n    }\n}\n\n@media only screen and (min-width: 481px) and (max-width: 1024px) { \n    .eshop-header.home .eshop-header-hero {\n        height: 24rem;\n    }\n\n    .eshop-header .eshop-header-hero {\n        height: 15rem;\n    }\n\n    .eshop-header-hero img {\n        width: 100%;\n        height: 100%;\n        object-fit: cover;\n        object-position: center;\n    }\n\n    .eshop-header-container {\n        margin: 0 1rem;\n        margin: 0 3rem;\n    }\n\n    .eshop-header.home .eshop-header-container {\n        height: 24rem;\n        margin: 0 3rem;\n    }\n\n    .eshop-header-intro {\n        white-space: wrap;\n    }\n\n    .eshop-header-intro h1 {\n        font-size: 2rem;\n    }\n    \n    .eshop-header-intro p {\n        font-size: 1.5rem;\n    }\n}"
  },
  {
    "path": "src/WebApp/Components/Layout/MainLayout.razor",
    "content": "﻿@using eShop.WebApp.Components.Chatbot\n@inherits LayoutComponentBase\n\n<HeaderBar />\n@Body\n<ShowChatbotButton />\n<FooterBar />\n\n<div id=\"blazor-error-ui\">\n    An unhandled error has occurred.\n    <a href=\"\" class=\"reload\">Reload</a>\n    <a class=\"dismiss\">🗙</a>\n</div>\n"
  },
  {
    "path": "src/WebApp/Components/Layout/MainLayout.razor.css",
    "content": "#blazor-error-ui {\n    background: lightyellow;\n    bottom: 0;\n    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);\n    display: none;\n    left: 0;\n    padding: 0.6rem 1.25rem 0.7rem 1.25rem;\n    position: fixed;\n    width: 100%;\n    z-index: 1000;\n}\n\n    #blazor-error-ui .dismiss {\n        cursor: pointer;\n        position: absolute;\n        right: 0.75rem;\n        top: 0.5rem;\n    }\n"
  },
  {
    "path": "src/WebApp/Components/Layout/UserMenu.razor",
    "content": "﻿@using Microsoft.AspNetCore.Authentication.Cookies;\n@using Microsoft.AspNetCore.Authentication.OpenIdConnect;\n@using Microsoft.AspNetCore.Components.Authorization\n@using Microsoft.AspNetCore.Authentication\n@inject LogOutService LogOutService\n@inject NavigationManager Nav\n\n<AuthorizeView>\n    <Authorized>\n        <h3>@context.User.Identity?.Name</h3>\n        <div class=\"dropdown-menu\">\n            <span class=\"dropdown-button\"><img role=\"presentation\" src=\"icons/user.svg\" /></span>\n            <div class=\"dropdown-content\">\n                <a class=\"dropdown-item\" href=\"user/orders\">My orders</a>\n                <form class=\"dropdown-item\" method=\"post\" action=\"user/logout\" @formname=\"logout\" @onsubmit=\"LogOutAsync\">\n                    <AntiforgeryToken />\n                    <button type=\"submit\">Log out</button>\n                </form>\n            </div>\n        </div>\n    </Authorized>\n    <NotAuthorized>\n        <a aria-label=\"Sign in\" href=\"@Pages.User.LogIn.Url(Nav)\"><img role=\"presentation\" src=\"icons/user.svg\" /></a>\n    </NotAuthorized>\n</AuthorizeView>\n\n@code {\n    [CascadingParameter]\n    public HttpContext? HttpContext { get; set; }\n\n    private Task LogOutAsync()\n        => LogOutService.LogOutAsync(HttpContext!);\n}\n"
  },
  {
    "path": "src/WebApp/Components/Layout/UserMenu.razor.css",
    "content": "﻿.dropdown-menu {\n    position: relative;\n    display: inline-block;\n}\n\n.dropdown-content {\n    display: none;\n    position: absolute;\n    background-color: #FFF;\n    min-width: 8rem;\n    box-shadow: 0 0.25rem 0.5rem 0 rgba(0, 0, 0, 0.2);\n    z-index: 1;\n}\n\n.dropdown-item {\n    padding: 0.75rem 1rem;\n    text-decoration: none;\n    display: block;\n    color: #000;\n}\n\n.dropdown-item:hover {\n    background-color: #ddd;\n}\n\n.dropdown-menu:hover .dropdown-content {\n    display: block;\n}\n\n.dropdown-item button {\n    border: 0;\n    background: transparent;\n    cursor: pointer;\n    width: 100%;\n    padding: 0;\n    text-align: left;\n}\n"
  },
  {
    "path": "src/WebApp/Components/Pages/Cart/CartPage.razor",
    "content": "﻿@page \"/cart\"\n@inject NavigationManager Nav\n@inject BasketState Basket\n@inject IProductImageUrlProvider ProductImages\n@attribute [StreamRendering]\n@attribute [Authorize]\n\n<PageTitle>Shopping Bag | AdventureWorks</PageTitle>\n<SectionContent SectionName=\"page-header-title\">Shopping bag</SectionContent>\n\n<div class='cart'>\n    @if (basketItems is null)\n    {\n        <p>Loading...</p>\n    }\n    else if (basketItems.Count == 0)\n    {\n        <p>\n            Your shopping bag is empty. <a href=\"\">Continue shopping.</a>\n        </p>\n    }\n    else\n    {\n        <div class='cart-items'>\n            <div class='cart-item-header'>\n                <div class='catalog-item-info'>Products</div>\n                <div class='catalog-item-quantity'>Quantity</div>\n                <div class='catalog-item-total'>Total</div>\n            </div>\n            @foreach (var item in basketItems)\n            {\n                var quantity = CurrentOrPendingQuantity(item.ProductId, item.Quantity);\n                <div class=\"cart-item\" @key=\"@item.Id\">\n                    <div class=\"catalog-item-info\">\n                        <img alt=\"@item.ProductName\" src=\"@ProductImages.GetProductImageUrl(item.ProductId)\" />\n                        <div class=\"catalog-item-content\">\n                            <p class=\"name\">@item.ProductName</p>\n                            <p class=\"price\">$@item.UnitPrice.ToString(\"0.00\")</p>\n                        </div>\n                    </div>\n                    <div class=\"catalog-item-quantity\">\n                        <form method=\"post\" data-enhance>\n                            <input type=\"hidden\" name=\"_handler\" value=\"update-cart\" />\n                            <AntiforgeryToken />\n                            <input aria-label=\"product quantity\" type=\"number\" name=\"UpdateQuantityValue\" value=\"@quantity\" min=\"0\" />\n                            <button type=\"submit\" \n                                    class=\"button button-secondary\"\n                                    name=\"UpdateQuantityId\" \n                                    value=\"@item.ProductId\">\n                                Update\n                            </button>\n                        </form>\n                    </div>\n                    <div class=\"catalog-item-total\">\n                        $@((item.UnitPrice * quantity).ToString(\"0.00\"))\n                    </div>\n                </div>\n            }\n        </div>\n       \n\n        <div class=\"cart-summary\">\n            <div class=\"cart-summary-container\">\n                <div class=\"cart-summary-header\">\n                    <img role=\"presentation\" src=\"icons/cart.svg\" />\n                    Your shopping bag\n                    <span class=\"filter-badge\">@TotalQuantity</span>\n                </div>\n                <div class=\"cart-summary-total\">\n                    <div>Total</div>\n                    <div>$@TotalPrice?.ToString(\"0.00\")</div>\n                </div>\n                <a href=\"checkout\" class=\"button button-primary\">Check out</a>\n                <a href=\"\" class=\"cart-summary-link\">\n                    <img role=\"presentation\" src=\"icons/arrow-left.svg\" />\n                    <p>Continue shopping</p>\n                </a>\n            </div>\n        </div>\n    }\n</div>\n\n<form @formname=\"update-cart\" @onsubmit=\"@UpdateQuantityAsync\"></form>\n\n@code {\n    private IReadOnlyCollection<BasketItem>? basketItems;\n\n    [SupplyParameterFromForm]\n    public int? UpdateQuantityId { get; set; }\n\n    [SupplyParameterFromForm]\n    public int? UpdateQuantityValue { get; set; }\n\n    protected override async Task OnInitializedAsync()\n    {\n        basketItems = await Basket.GetBasketItemsAsync();\n    }\n\n    private decimal? TotalPrice => basketItems?.Sum(i => i.Quantity * i.UnitPrice);\n    private decimal? TotalQuantity => basketItems?.Sum(i => i.Quantity);\n\n    // While an update post is in process, we want to show the pending quantity, not the one\n    // that is committed to the cart (otherwise the UI briefly shows the old data)\n    private int CurrentOrPendingQuantity(int productId, int cartQuantity)\n        => UpdateQuantityId.GetValueOrDefault(-1) == productId\n        ? UpdateQuantityValue!.Value\n        : cartQuantity;\n\n    private async Task UpdateQuantityAsync()\n    {\n        var id = UpdateQuantityId!.Value;\n        var quantity = UpdateQuantityValue!.Value;\n        await Basket.SetQuantityAsync(id, quantity);\n        basketItems = await Basket.GetBasketItemsAsync();\n    }\n}\n"
  },
  {
    "path": "src/WebApp/Components/Pages/Cart/CartPage.razor.css",
    "content": "﻿.cart {\n    padding: 0 10rem;\n    display: flex;\n    gap: 6rem;\n}\n\n.cart .cart-items {\n    display: flex;\n    flex-direction: column;\n    align-items: flex-start;\n    gap: 1rem;\n    flex: 1 0 0;\n}\n\n.cart-items .cart-item-header {\n    display: flex;\n    padding: 0.5rem 0;\n    align-items: center;\n    align-self: stretch;\n    border-bottom: 1px solid #D2D2D2;\n    flex-grow: 1;\n}\n\n.cart-items .cart-item {\n    display: flex;\n    padding-bottom: 1.25rem;\n    justify-content: space-between;\n    align-items: center;\n    align-self: stretch;\n    border-bottom: 1px solid #D2D2D2;\n}\n\n.cart-items .cart-item .catalog-item-info {\n    display: flex;\n    align-items: center;\n    gap: 1.25rem;\n    align-self: stretch;\n    flex-basis: 60%;\n}\n\n.cart-items .cart-item-header .catalog-item-info {\n    flex-basis: 60%;\n}\n\n.cart-items .cart-item .catalog-item-quantity,\n.cart-items .cart-item-header .catalog-item-quantity {\n    flex-grow: 1;\n}\n\n.cart-items .cart-item .catalog-item-quantity form {\n    display: flex;\n    gap: 0.5rem;\n}\n\n.cart-items .cart-item .catalog-item-quantity input {\n    max-width: 3rem;\n    padding: 1rem 0.75rem;\n}\n\n.cart-items .cart-item .catalog-item-info img {\n    max-height: 12rem;\n    max-width: 12rem;\n}\n\n.cart-summary-container {\n    display: flex;\n    padding: 1rem 1.5rem;\n    flex-direction: column;\n    align-items: flex-start;\n    gap: 1rem;\n    flex-shrink: 0;\n    background: #F7F7F7;\n}\n\n.cart-summary-header {\n    display: flex;\n    padding: 0.5rem 0;\n    justify-content: space-between;\n    align-items: center;\n    align-self: stretch;\n    border-bottom: 1px solid #000;\n    gap: 0.5rem;\n    color: #000;\n    font-size: 1.25rem;\n    font-weight: 600;\n    line-height: 120%;\n}\n\n.cart-summary-breakdown {\n    display: flex;\n    padding-bottom: 0.5rem;\n    flex-direction: column;\n    align-items: flex-start;\n    gap: 0.5rem;\n    align-self: stretch;\n    border-bottom: 1px solid #444;\n}\n\n.cart-summary-breakdown-line {\n    display: flex;\n    justify-content: space-between;\n    align-items: flex-start;\n    align-self: stretch;\n}\n\n.cart-summary-total {\n    display: flex;\n    justify-content: space-between;\n    align-items: flex-start;\n    align-self: stretch;\n}\n\n.cart-summary .cart-summary-link {\n    display: flex;\n    align-items: center;\n    gap: 0.5rem;\n    color: #000;\n    text-decoration: none;\n}\n\n.cart-summary .filter-badge {\n    background: #000;\n    color: #FFF;\n    font-size: 1rem;\n    font-weight: 600;\n    border-radius: 0.75rem;\n    width: 3.5rem;\n    height: 1.5rem;\n    line-height: 100%;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    margin-left: auto;\n}\n\n@media only screen and (max-width: 480px) { \n    .cart {\n        padding: 0 1rem;\n        gap: 1rem;\n        flex-direction: column-reverse\n    }\n\n    .cart-item-header div {\n        display: none;\n    }\n\n    .cart-item {\n        flex-wrap: wrap;\n        gap: 1rem;\n    }\n\n    .cart-items .cart-item .catalog-item-info {\n        flex-basis: 100%;\n    }\n}\n\n@media only screen and (min-width: 481px) and (max-width: 1024px) { \n    .cart {\n        padding: 0 3rem;\n        gap: 2rem;\n        flex-direction: column-reverse;\n    } \n}"
  },
  {
    "path": "src/WebApp/Components/Pages/Catalog/Catalog.razor",
    "content": "﻿@page \"/\"\n@inject NavigationManager Nav\n@inject CatalogService CatalogService\n@attribute [StreamRendering]\n\n<PageTitle>AdventureWorks</PageTitle>\n<SectionContent SectionName=\"page-header-title\">Ready for a new adventure?</SectionContent>\n<SectionContent SectionName=\"page-header-subtitle\">Start the season with the latest in clothing and equipment.</SectionContent>\n\n<div class=\"catalog\">\n    <CatalogSearch BrandId=\"@BrandId\" ItemTypeId=\"@ItemTypeId\" />\n\n    @if (catalogResult is null)\n    {\n        <p>Loading...</p>\n    }\n    else\n    {\n        <div>\n            <div class=\"catalog-items\">\n                @foreach (var item in catalogResult.Data)\n                {\n                    <CatalogListItem Item=\"@item\" />\n                }\n            </div>\n\n            <div class=\"page-links\">\n                @foreach (var pageIndex in GetVisiblePageIndexes(catalogResult))\n                {\n                    <NavLink ActiveClass=\"active-page\" Match=\"@NavLinkMatch.All\" href=\"@Nav.GetUriWithQueryParameter(\"page\", pageIndex == 1 ? null : pageIndex)\">@pageIndex</NavLink>\n                }\n            </div>\n        </div>\n    }\n</div>\n\n@code {\n    const int PageSize = 9;\n\n    [SupplyParameterFromQuery]\n    public int? Page { get; set; }\n\n    [SupplyParameterFromQuery(Name = \"brand\")]\n    public int? BrandId { get; set; }\n\n    [SupplyParameterFromQuery(Name = \"type\")]\n    public int? ItemTypeId { get; set; }\n\n    CatalogResult? catalogResult;\n\n    static IEnumerable<int> GetVisiblePageIndexes(CatalogResult result)\n        => Enumerable.Range(1, (int)Math.Ceiling(1.0 * result.Count / PageSize));\n\n    protected override async Task OnInitializedAsync()\n    {\n        catalogResult = await CatalogService.GetCatalogItems(\n            Page.GetValueOrDefault(1) - 1,\n            PageSize,\n            BrandId,\n            ItemTypeId);\n    }\n}\n"
  },
  {
    "path": "src/WebApp/Components/Pages/Catalog/Catalog.razor.css",
    "content": "﻿.catalog {\n    padding: 0 10rem;\n    display: flex;\n    gap: 6rem;\n}\n\n.catalog .catalog-filter {\n    flex-grow: 1;\n    min-width: 14rem;\n}\n\n.catalog .catalog-filter .catalog-filter-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    align-self: stretch;\n    gap: 0.7rem;\n}\n\n.catalog .catalog-filter .filter-reset {\n    margin-left: auto;\n}\n\n.catalog .catalog-filter .filter-reset:hover {\n    cursor: pointer;\n}\n\n.catalog .catalog-filter .filter-badge {\n    background: #000;\n    color: #FFF;\n    font-size: 1rem;\n    font-weight: 600;\n    border-radius: 0.75rem;\n    width: 1.5rem;\n    height: 1.5rem;\n    line-height: 100%;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.catalog .catalog-filter-group h3 {\n    color: #000;\n    font-size: 1rem;\n    font-weight: 600;\n    line-height: 150%;\n}\n\n.catalog .catalog-filter-group .catalog-filter-group-tags {\n    border-top: 1px solid #404040;\n    display: flex;\n    padding: 0.75rem 0;\n    align-items: center;\n    align-content: center;\n    gap: 0.25rem;\n    align-self: stretch;\n    flex-wrap: wrap;\n}\n\n.catalog-filter-group-tags .catalog-filter-tag {\n    display: flex;\n    padding: 0.5rem 0.75rem;\n    justify-content: center;\n    align-items: center;\n    gap: 0.25rem;\n    border-radius: 1.25rem;\n    color: #404040;\n    font-family: 'Open Sans';\n    font-size: 1rem;\n    font-style: normal;\n    font-weight: 400;\n    line-height: 150%;\n}\n\n.catalog-filter-group-tags .catalog-filter-tag:hover {\n    cursor: pointer;\n}\n\n.catalog-filter-group-tags .catalog-filter-tag.active {\n    background: #000;\n    color: #FFF;\n}\n\n.catalog .catalog-items {\n    display: flex;\n    align-items: flex-start;\n    align-content: flex-start;\n    gap: 2.5rem;\n    flex-wrap: wrap;\n    flex-grow: 1;\n}\n\n.page-links {\n    display: flex;\n    align-items: center;\n    gap: 0.5rem;\n    justify-content: center;\n    margin-top: 1.5rem;\n}\n\n::deep a {\n    display: flex;\n    padding: 12px 20px;\n    flex-direction: column;\n    justify-content: center;\n    align-items: center;\n    gap: 4px;\n    background: #F7F7F7;\n    color: #000;\n    text-decoration: none;\n}\n\n.page-links ::deep a.active-page {\n    color: #F7F7F7;\n    background-color: #000;\n}\n\n\n@media only screen and (max-width: 480px) { \n    .catalog {\n        padding: 0 1rem;\n        flex-direction: column;\n        gap: 1rem;\n    }\n\n    .page-links {\n        flex-wrap: wrap;\n    }\n}\n\n@media only screen and (min-width: 481px) and (max-width: 1024px) { \n    .catalog {\n        padding: 0 3rem;\n        flex-direction: column;\n        gap: 1.5rem;\n    }\n\n    .page-links {\n        flex-wrap: wrap;\n    }\n}"
  },
  {
    "path": "src/WebApp/Components/Pages/Checkout/Checkout.razor",
    "content": "﻿@page \"/checkout\"\n@using System.Globalization\n@using System.ComponentModel.DataAnnotations\n@inject BasketState Basket\n@inject NavigationManager Nav\n@attribute [Authorize]\n\n<PageTitle>Checkout | AdventureWorks</PageTitle>\n<SectionContent SectionName=\"page-header-title\">Checkout</SectionContent>\n\n<div class='checkout'>\n    <EditForm EditContext=\"@editContext\" FormName=\"checkout\" OnSubmit=\"@HandleSubmitAsync\" Enhance>\n        <DataAnnotationsValidator />\n        <div class=\"form\">\n            <div class=\"form-section\">\n                <h2>Shipping address</h2>\n                <label>\n                    Address\n                    <InputText @bind-Value=\"@Info.Street\" />\n                    <ValidationMessage For=\"@(() => Info.Street)\" />\n                </label>\n                <div class=\"form-group\">\n                    <div class=\"form-group-item\">\n                        <label>\n                            City\n                            <InputText @bind-Value=\"@Info.City\" />\n                            <ValidationMessage For=\"@(() => Info.City)\" />\n                        </label>\n                    </div>\n                    <div class=\"form-group-item\">\n                        <label>\n                            State\n                            <InputText @bind-Value=\"@Info.State\" />\n                            <ValidationMessage For=\"@(() => Info.State)\" />\n                        </label>\n                    </div>\n                    <div class=\"form-group-item\">\n                        <label>\n                            Zip code\n                            <InputText @bind-Value=\"@Info.ZipCode\" />\n                            <ValidationMessage For=\"@(() => Info.ZipCode)\" />\n                        </label>\n                    </div>\n                </div>\n                <div>\n                    <label>\n                        Country\n                        <InputText @bind-Value=\"@Info.Country\" />\n                        <ValidationMessage For=\"@(() => Info.Country)\" />\n                    </label>\n                </div>\n            </div>\n            <div class=\"form-section\">\n                <div class=\"form-buttons\">\n                    <a href=\"cart\" class=\"button button-secondary\"><img role=\"presentation\" src=\"icons/arrow-left.svg\" />Back to the shopping bag</a>\n                    <button class=\"button button-primary\" type=\"submit\">Place order</button>\n                </div>\n            </div>\n        </div>\n        <ValidationSummary />\n    </EditForm>\n</div>\n\n@code {\n    private EditContext editContext = default!;\n    private ValidationMessageStore extraMessages = default!;\n\n    [SupplyParameterFromForm]\n    public BasketCheckoutInfo Info { get; set; } = default!;\n\n    [CascadingParameter]\n    public HttpContext HttpContext { get; set; } = default!;\n\n    protected override void OnInitialized()\n    {\n        if (Info is null)\n        {\n            PopulateFormWithDefaultInfo();\n        }\n\n        editContext = new EditContext(Info!);\n        extraMessages = new ValidationMessageStore(editContext);\n    }\n\n    private void PopulateFormWithDefaultInfo()\n    {\n        Info = new BasketCheckoutInfo\n        {\n            Street = ReadClaim(\"address_street\"),\n            City = ReadClaim(\"address_city\"),\n            State = ReadClaim(\"address_state\"),\n            Country = ReadClaim(\"address_country\"),\n            ZipCode = ReadClaim(\"address_zip_code\"),\n            RequestId = Guid.NewGuid()\n        };\n\n        string? ReadClaim(string type)\n            => HttpContext.User.Claims.FirstOrDefault(x => x.Type == type)?.Value;\n    }\n\n    private async Task HandleSubmitAsync()\n    {\n        await PerformCustomValidationAsync();\n\n        if (editContext.Validate())\n        {\n            await HandleValidSubmitAsync();\n        }\n    }\n\n    private async Task HandleValidSubmitAsync()\n    {\n        Info.CardTypeId = 1;\n        await Basket.CheckoutAsync(Info);\n        Nav.NavigateTo(\"user/orders\");\n    }\n\n    private async Task PerformCustomValidationAsync()\n    {\n        extraMessages.Clear();\n\n        if ((await Basket.GetBasketItemsAsync()).Count == 0)\n        {\n            extraMessages.Add(new FieldIdentifier(Info, \"\"), \"Your cart is empty\");\n        }\n    }\n\n    private static DateTime? ParseExpiryDate(string? mmyy)\n        => DateTime.TryParseExact($\"01/{mmyy}\", \"dd/MM/yy\", null, DateTimeStyles.None, out var result) ? result.ToUniversalTime() : null;\n}\n"
  },
  {
    "path": "src/WebApp/Components/Pages/Checkout/Checkout.razor.css",
    "content": "﻿.checkout {\n    padding: 0 10rem;\n}\n\n.checkout h2 {\n    color: #000;\n    font-size: 1.25rem;\n    font-style: normal;\n    font-weight: 600;\n    line-height: 140%;\n    border-bottom: 1px solid #D2D2D2;\n    width: 100%;\n    padding-bottom: 0.5rem;\n}\n\n.checkout .form-buttons {\n    display: flex;\n    padding: 1.5rem 0;\n    justify-content: space-between;\n    align-items: center;\n    align-self: stretch;\n    border-top: 1px solid #000;\n}\n\n.checkout label {\n    display: flex;\n    flex-direction: column;\n    align-items: flex-start;\n    gap: 0.5rem;\n    color: #444;\n    font-size: 1rem;\n    font-style: normal;\n    font-weight: 400;\n    line-height: 150%;\n}\n\n.checkout ::deep input {\n    border: 1px solid #000;\n    background: #FFF;\n    color: #000;\n    font-size: 1rem;\n    font-style: normal;\n    font-weight: 400;\n    line-height: 150%;\n    width: calc(100% - 1rem);\n    padding: 0.5rem;\n}\n\n.form-group {\n    display: flex;\n    align-items: flex-start;\n    gap: 1.5rem;\n    align-self: stretch;\n}\n\n.form-group .form-group-item {\n    flex: 1 0 0;\n}\n\n.form {\n    display: flex;\n    flex-direction: column;\n    gap: 2.5rem;\n}\n\n.form .form-section {\n    display: flex;\n    flex-direction: column;\n    gap: 1.25rem;\n    align-self: stretch;\n}\n\n@media only screen and (max-width: 480px) { \n    .checkout {\n        padding: 0 1rem;\n    }\n}\n\n@media only screen and (min-width: 481px) and (max-width: 1024px) { \n    .checkout {\n        padding: 0 3rem;\n    }\n}"
  },
  {
    "path": "src/WebApp/Components/Pages/Item/ItemPage.razor",
    "content": "﻿@page \"/item/{itemId:int}\"\n@using System.Net\n@inject CatalogService CatalogService\n@inject BasketState BasketState\n@inject NavigationManager Nav\n@inject IProductImageUrlProvider ProductImages\n\n@if (item is not null)\n{\n    <PageTitle>@item.Name | AdventureWorks</PageTitle>\n    <SectionContent SectionName=\"page-header-title\">@item.Name</SectionContent>\n    <SectionContent SectionName=\"page-header-subtitle\">@item.CatalogBrand?.Brand</SectionContent>\n\n    <div class=\"item-details\">\n        <img alt=\"@item.Name\" src=\"@ProductImages.GetProductImageUrl(item)\" />\n        <div class=\"description\">\n            <p>@item.Description</p>\n            <p>\n                Brand: <strong>@item.CatalogBrand?.Brand</strong>\n            </p>\n            <form class=\"add-to-cart\" method=\"post\" @formname=\"add-to-cart\" @onsubmit=\"@AddToCartAsync\" data-enhance=\"@isLoggedIn\">\n                <AntiforgeryToken />\n                <span class=\"price\">$@item.Price.ToString(\"0.00\")</span>\n\n                @if (isLoggedIn)\n                {\n                    <button type=\"submit\" title=\"Add to basket\">\n                        <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n                            <path id=\"Vector\" d=\"M6 2L3 6V20C3 20.5304 3.21071 21.0391 3.58579 21.4142C3.96086 21.7893 4.46957 22 5 22H19C19.5304 22 20.0391 21.7893 20.4142 21.4142C20.7893 21.0391 21 20.5304 21 20V6L18 2H6Z\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n                            <path id=\"Vector_2\" d=\"M3 6H21\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n                            <path id=\"Vector_3\" d=\"M16 10C16 11.0609 15.5786 12.0783 14.8284 12.8284C14.0783 13.5786 13.0609 14 12 14C10.9391 14 9.92172 13.5786 9.17157 12.8284C8.42143 12.0783 8 11.0609 8 10\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n                        </svg>\n                        Add to shopping bag\n                    </button>\n                }\n                else\n                {\n                    <button type=\"submit\" title=\"Log in to purchase\">\n                        <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n                            <path d=\"M20 21V19C20 17.9391 19.5786 16.9217 18.8284 16.1716C18.0783 15.4214 17.0609 15 16 15H8C6.93913 15 5.92172 15.4214 5.17157 16.1716C4.42143 16.9217 4 17.9391 4 19V21\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n                            <path d=\"M12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11Z\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n                        </svg>\n                        Log in to purchase\n                    </button>\n                }\n            </form>\n\n            @if (numInCart > 0)\n            {\n                <p><strong>@numInCart</strong> in <a href=\"cart\">shopping bag</a></p>\n            }\n        </div>\n    </div>\n}\nelse if (notFound)\n{\n    <SectionContent SectionName=\"page-header-title\">Not found</SectionContent>\n    <div class=\"item-details\">\n        <p>Sorry, we couldn't find any such product.</p>\n    </div>\n}\n\n@code {\n    private CatalogItem? item;\n    private int numInCart;\n    private bool isLoggedIn;\n    private bool notFound;\n\n    [Parameter]\n    public int ItemId { get; set; }\n\n    [CascadingParameter]\n    public HttpContext? HttpContext { get; set; }\n\n    protected override async Task OnInitializedAsync()\n    {\n        try\n        {\n            isLoggedIn = HttpContext?.User.Identity?.IsAuthenticated == true;\n            item = await CatalogService.GetCatalogItem(ItemId);\n            await UpdateNumInCartAsync();\n        }\n        catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)\n        {\n            HttpContext!.Response.StatusCode = 404;\n            notFound = true;\n        }\n    }\n\n    private async Task AddToCartAsync()\n    {\n        if (!isLoggedIn)\n        {\n            Nav.NavigateTo(Pages.User.LogIn.Url(Nav));\n            return;\n        }\n\n        if (item is not null)\n        {\n            await BasketState.AddAsync(item);\n            await UpdateNumInCartAsync();\n        }\n    }\n\n    private async Task UpdateNumInCartAsync()\n    {\n        var items = await BasketState.GetBasketItemsAsync();\n        numInCart = items.FirstOrDefault(row => row.ProductId == ItemId)?.Quantity ?? 0;\n    }\n}\n"
  },
  {
    "path": "src/WebApp/Components/Pages/Item/ItemPage.razor.css",
    "content": "﻿.item-details {\n    padding: 0 5rem 0 10rem;\n    display: flex;\n    align-items: flex-start;\n    gap: 4rem;\n    line-height: 1.7rem;\n}\n\np:first-of-type {\n    margin-top: 0;\n}\n\nimg {\n    width: 25rem;\n    max-width: 50%;\n}\n\n.description {\n    max-width: 30rem;\n}\n\n.add-to-cart {\n    display: flex;\n    align-items: center;\n    gap: 1.2rem;\n}\n\n.price {\n    font-size: 1.6rem;\n    font-weight: 600;\n}\n\n.add-to-cart button {\n    background-color: black;\n    color: white;\n    border-radius: .25rem;\n    border: 0;\n    padding: 0.5rem 1.25rem;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    gap: 0.5rem;\n}\n\n.add-to-cart button:hover {\n    background-color: #666;\n}\n\n    .add-to-cart button:active {\n        background-color: #333;\n    }\n\na {\n    color: black;\n}\n\n@media only screen and (max-width: 480px) { \n    .item-details {\n        padding: 1.5rem 1rem 0 1rem;\n        flex-direction: column;\n        gap: 1rem;\n    }    \n\n    .item-details img {\n        width: 100%;\n        max-width: none;\n    }\n}\n\n@media only screen and (min-width: 481px) and (max-width: 1024px) { \n    .item-details {\n        gap: 1rem;\n        padding: 0 3rem 0 3rem;\n    }    \n}"
  },
  {
    "path": "src/WebApp/Components/Pages/User/LogIn.razor",
    "content": "﻿@page \"/user/login\"\n@inject NavigationManager Nav\n@attribute [Authorize]\n@code {\n    [SupplyParameterFromQuery]\n    public string? ReturnUrl { get; set; }\n\n    [CascadingParameter]\n    public HttpContext? HttpContext { get; set; }\n\n    protected override async Task OnInitializedAsync()\n    {\n        var returnUrl = ReturnUrl ?? \"/\";\n        var url = new Uri(returnUrl, UriKind.RelativeOrAbsolute);\n        Nav.NavigateTo(url.IsAbsoluteUri ? \"/\" : returnUrl);\n        await base.OnInitializedAsync();\n    }\n\n    public static string Url(NavigationManager nav)\n        => $\"user/login?returnUrl={Uri.EscapeDataString(nav.ToBaseRelativePath(nav.Uri))}\";\n}\n"
  },
  {
    "path": "src/WebApp/Components/Pages/User/LogOut.razor",
    "content": "﻿@page \"/user/logout\"\n@*\n    When the 'log out' form is posted, it is handled inside UserMenu.razor.\n    This page only exists to create an endpoint that accepts the form post.\n*@\n"
  },
  {
    "path": "src/WebApp/Components/Pages/User/Orders.razor",
    "content": "﻿@page \"/user/orders\"\n@attribute [Authorize]\n@attribute [StreamRendering]\n@inject OrderingService OrderingService\n\n<PageTitle>Orders | AdventureWorks</PageTitle>\n<SectionContent SectionName=\"page-header-title\">Orders</SectionContent>\n<OrdersRefreshOnStatusChange />\n\n<div class=\"orders\">\n@if (orders is null)\n{\n    <p>Loading...</p>\n}\nelse if (orders.Length == 0)\n{\n    <p>You haven't yet placed any orders.</p>\n}\nelse\n{\n    <ul class=\"orders-list\">\n        <li class=\"orders-header orders-item\">\n            <div>Number</div>\n            <div>Date</div>\n            <div class=\"total-header\">Total</div>\n            <div>Status</div>\n        </li>\n        @foreach (var order in orders)\n        {\n            <li class=\"orders-item\">\n                <div class=\"order-number\">\n                    @order.OrderNumber\n                </div>\n                <div class=\"order-date\">\n                    @order.Date\n                </div>\n                <div class=\"order-total\">\n                   $@order.Total.ToString(\"0.00\")\n                </div>\n                <div class=\"order-status\">\n                    <span class=\"status @order.Status.ToLower()\">\n                        @order.Status\n                    </span>\n                </div>\n            </li>\n        }\n    </ul>\n}\n</div>\n\n@code {\n    private OrderRecord[]? orders;\n\n    protected override async Task OnInitializedAsync()\n    {\n        orders = await OrderingService.GetOrders();\n    }\n}\n"
  },
  {
    "path": "src/WebApp/Components/Pages/User/Orders.razor.css",
    "content": "﻿.orders {\n    padding: 0 10rem;\n}\n\n.orders-item {\n    display: flex;\n    padding-bottom: 0;\n    align-items: center;\n    gap: 1.75rem;\n    align-self: stretch;\n}\n\n.orders-item > div {\n    flex: 1 0 0;\n}\n\n.orders-item {\n    padding: 1rem 0;\n    border-bottom: 1px solid #D2D2D2;\n}\n\n.orders-header {\n    color: #000;\n    font-size: 1rem;\n    font-style: normal;\n    font-weight: 600;\n    line-height: 1.5rem;\n    padding-top: 0;\n    padding-bottom: 0.5rem;\n}\n\n.total-header {\n    text-align: right;\n}\n\n.order-total {\n    color: #000;\n    text-align: right;\n    font-size: 1rem;\n    font-style: normal;\n    font-weight: 600;\n    line-height: 150%;\n}\n\n.order-status .status {\n    border-radius: 1.25rem;\n    border: 1px solid #A3A3A3;\n    color: #A3A3A3;\n    font-size: 0.75rem;\n    font-style: normal;\n    font-weight: 400;\n    line-height: 1.25rem;\n    padding: 0.5rem 1rem;\n}\n\n.order-status .status.cancelled {\n    color: #FF4E4E;\n    border: 1px solid #FF4E4E;\n}\n\n.order-status .status.paid {\n    color: #2A9E01;\n    border: 1px solid #2A9E01;\n}\n\n@media only screen and (max-width: 480px) { \n    .orders {\n        padding: 0 1rem;\n    }\n}\n\n@media only screen and (min-width: 481px) and (max-width: 1024px) { \n    .orders {\n        padding: 0 3rem;\n    }    \n}"
  },
  {
    "path": "src/WebApp/Components/Pages/User/OrdersRefreshOnStatusChange.razor",
    "content": "﻿@using Microsoft.AspNetCore.Components.Authorization\n@rendermode InteractiveServer\n@inject AuthenticationStateProvider AuthenticationStateProvider\n@inject OrderStatusNotificationService OrderStatusNotificationService\n@inject NavigationManager Nav\n@implements IDisposable\n@code {\n    private IDisposable? orderStatusChangedSubscription;\n\n    protected override async Task OnAfterRenderAsync(bool firstRender)\n    {\n        if (firstRender)\n        {\n            var buyerId = await AuthenticationStateProvider.GetBuyerIdAsync();\n            if (!string.IsNullOrEmpty(buyerId))\n            {\n                orderStatusChangedSubscription = OrderStatusNotificationService.SubscribeToOrderStatusNotifications(\n                    buyerId, () => InvokeAsync(HandleOrderStatusChanged));\n            }\n        }\n    }\n\n    private void HandleOrderStatusChanged()\n    {\n        try\n        {\n            Nav.Refresh();\n        }\n        catch (Exception ex)\n        {\n            // If there's an exception, we want to handle it on this circuit,\n            // and not throw it to the upstream caller\n            _ = DispatchExceptionAsync(ex);\n        }\n    }\n\n    public void Dispose()\n    {\n        orderStatusChangedSubscription?.Dispose();\n    }\n}\n"
  },
  {
    "path": "src/WebApp/Components/Routes.razor",
    "content": "﻿<Router AppAssembly=\"@typeof(Program).Assembly\">\n    <Found Context=\"routeData\">\n        <RouteView RouteData=\"@routeData\" DefaultLayout=\"@typeof(Layout.MainLayout)\" />\n        <FocusOnNavigate RouteData=\"@routeData\" Selector=\"h1\" />\n    </Found>\n</Router>\n"
  },
  {
    "path": "src/WebApp/Components/_Imports.razor",
    "content": "﻿@using System.Net.Http\n@using System.Net.Http.Json\n@using Microsoft.AspNetCore.Authorization\n@using Microsoft.AspNetCore.Components.Forms\n@using Microsoft.AspNetCore.Components.Routing\n@using Microsoft.AspNetCore.Components.Web\n@using Microsoft.AspNetCore.Components.Sections\n@using static Microsoft.AspNetCore.Components.Web.RenderMode\n@using Microsoft.AspNetCore.Components.Web.Virtualization\n@using Microsoft.JSInterop\n@using eShop.WebApp\n@using eShop.WebApp.Components\n@using eShop.WebApp.Services\n@using eShop.WebAppComponents.Catalog\n@using eShop.WebAppComponents.Services"
  },
  {
    "path": "src/WebApp/Extensions/Extensions.cs",
    "content": "﻿using eShop.Basket.API.Grpc;\nusing eShop.WebApp.Services.OrderStatus.IntegrationEvents;\nusing eShop.WebAppComponents.Services;\nusing Microsoft.AspNetCore.Authentication.Cookies;\nusing Microsoft.AspNetCore.Authentication.OpenIdConnect;\nusing Microsoft.AspNetCore.Components.Authorization;\nusing Microsoft.AspNetCore.Components.Server;\nusing Microsoft.Extensions.AI;\nusing Microsoft.IdentityModel.JsonWebTokens;\n\npublic static class Extensions\n{\n    public static void AddApplicationServices(this IHostApplicationBuilder builder)\n    {\n        builder.AddAuthenticationServices();\n\n        builder.AddRabbitMqEventBus(\"EventBus\")\n               .AddEventBusSubscriptions();\n\n        builder.Services.AddHttpForwarderWithServiceDiscovery();\n\n        // Application services\n        builder.Services.AddScoped<BasketState>();\n        builder.Services.AddScoped<LogOutService>();\n        builder.Services.AddSingleton<BasketService>();\n        builder.Services.AddSingleton<OrderStatusNotificationService>();\n        builder.Services.AddSingleton<IProductImageUrlProvider, ProductImageUrlProvider>();\n        builder.AddAIServices();\n\n        // HTTP and GRPC client registrations\n        builder.Services.AddGrpcClient<Basket.BasketClient>(o => o.Address = new(\"http://basket-api\"))\n            .AddAuthToken();\n\n        builder.Services.AddHttpClient<CatalogService>(o => o.BaseAddress = new(\"https+http://catalog-api\"))\n            .AddApiVersion(2.0)\n            .AddAuthToken();\n\n        builder.Services.AddHttpClient<OrderingService>(o => o.BaseAddress = new(\"https+http://ordering-api\"))\n            .AddApiVersion(1.0)\n            .AddAuthToken();\n    }\n\n    public static void AddEventBusSubscriptions(this IEventBusBuilder eventBus)\n    {\n        eventBus.AddSubscription<OrderStatusChangedToAwaitingValidationIntegrationEvent, OrderStatusChangedToAwaitingValidationIntegrationEventHandler>();\n        eventBus.AddSubscription<OrderStatusChangedToPaidIntegrationEvent, OrderStatusChangedToPaidIntegrationEventHandler>();\n        eventBus.AddSubscription<OrderStatusChangedToStockConfirmedIntegrationEvent, OrderStatusChangedToStockConfirmedIntegrationEventHandler>();\n        eventBus.AddSubscription<OrderStatusChangedToShippedIntegrationEvent, OrderStatusChangedToShippedIntegrationEventHandler>();\n        eventBus.AddSubscription<OrderStatusChangedToCancelledIntegrationEvent, OrderStatusChangedToCancelledIntegrationEventHandler>();\n        eventBus.AddSubscription<OrderStatusChangedToSubmittedIntegrationEvent, OrderStatusChangedToSubmittedIntegrationEventHandler>();\n    }\n\n    public static void AddAuthenticationServices(this IHostApplicationBuilder builder)\n    {\n        var configuration = builder.Configuration;\n        var services = builder.Services;\n\n        JsonWebTokenHandler.DefaultInboundClaimTypeMap.Remove(\"sub\");\n\n        var identityUrl = configuration.GetRequiredValue(\"IdentityUrl\");\n        var callBackUrl = configuration.GetRequiredValue(\"CallBackUrl\");\n        var sessionCookieLifetime = configuration.GetValue(\"SessionCookieLifetimeMinutes\", 60);\n\n        // Add Authentication services\n        services.AddAuthorization();\n        services.AddAuthentication(options =>\n        {\n            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;\n            options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;\n        })\n        .AddCookie(options => options.ExpireTimeSpan = TimeSpan.FromMinutes(sessionCookieLifetime))\n        .AddOpenIdConnect(options =>\n        {\n            options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;\n            options.Authority = identityUrl;\n            options.SignedOutRedirectUri = callBackUrl;\n            options.ClientId = \"webapp\";\n            options.ClientSecret = \"secret\";\n            options.ResponseType = \"code\";\n            options.SaveTokens = true;\n            options.GetClaimsFromUserInfoEndpoint = true;\n            options.RequireHttpsMetadata = false;\n            options.Scope.Add(\"openid\");\n            options.Scope.Add(\"profile\");\n            options.Scope.Add(\"orders\");\n            options.Scope.Add(\"basket\");\n        });\n\n        // Blazor auth services\n        services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();\n        services.AddCascadingAuthenticationState();\n    }\n\n    private static void AddAIServices(this IHostApplicationBuilder builder)\n    {\n        ChatClientBuilder? chatClientBuilder = null;\n        if (builder.Configuration[\"OllamaEnabled\"] is string ollamaEnabled && bool.Parse(ollamaEnabled))\n        {\n            chatClientBuilder = builder.AddOllamaApiClient(\"chat\")\n                .AddChatClient();\n        }\n        else if (!string.IsNullOrWhiteSpace(builder.Configuration.GetConnectionString(\"chatModel\")))\n        {\n            chatClientBuilder = builder.AddOpenAIClientFromConfiguration(\"chatModel\")\n                .AddChatClient();\n        }\n\n        chatClientBuilder?.UseFunctionInvocation();\n    }\n\n    public static async Task<string?> GetBuyerIdAsync(this AuthenticationStateProvider authenticationStateProvider)\n    {\n        var authState = await authenticationStateProvider.GetAuthenticationStateAsync();\n        var user = authState.User;\n        return user.FindFirst(\"sub\")?.Value;\n    }\n\n    public static async Task<string?> GetUserNameAsync(this AuthenticationStateProvider authenticationStateProvider)\n    {\n        var authState = await authenticationStateProvider.GetAuthenticationStateAsync();\n        var user = authState.User;\n        return user.FindFirst(\"name\")?.Value;\n    }\n}\n"
  },
  {
    "path": "src/WebApp/GlobalUsings.cs",
    "content": "﻿global using eShop.WebApp.Components;\nglobal using eShop.WebApp.Services;\nglobal using eShop.ServiceDefaults;\n"
  },
  {
    "path": "src/WebApp/Program.cs",
    "content": "﻿using eShop.WebApp.Components;\nusing eShop.ServiceDefaults;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.AddServiceDefaults();\n\nbuilder.Services.AddRazorComponents().AddInteractiveServerComponents();\n\nbuilder.AddApplicationServices();\n\nvar app = builder.Build();\n\napp.MapDefaultEndpoints();\n\n// Configure the HTTP request pipeline.\nif (!app.Environment.IsDevelopment())\n{\n    app.UseExceptionHandler(\"/Error\");\n    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.\n    app.UseHsts();\n}\n\napp.UseAntiforgery();\n\napp.UseHttpsRedirection();\n\napp.UseStaticFiles();\n\napp.MapRazorComponents<App>().AddInteractiveServerRenderMode();\n\napp.MapForwarder(\"/product-images/{id}\", \"https+http://catalog-api\", \"/api/catalog/items/{id}/pic\");\n\napp.Run();\n"
  },
  {
    "path": "src/WebApp/Properties/launchSettings.json",
    "content": "{\n  \"$schema\": \"http://json.schemastore.org/launchsettings.json\",\n  \"profiles\": {\n    \"http\": {\n      \"commandName\": \"Project\",\n      \"dotnetRunMessages\": true,\n      \"launchBrowser\": true,\n      \"applicationUrl\": \"http://localhost:5045\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"https\": {\n      \"commandName\": \"Project\",\n      \"dotnetRunMessages\": true,\n      \"launchBrowser\": true,\n      \"applicationUrl\": \"https://localhost:7298;http://localhost:5045\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/WebApp/Services/BasketCheckoutInfo.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace eShop.WebApp.Services;\n\npublic class BasketCheckoutInfo\n{\n    [Required]\n    public string? Street { get; set; }\n\n    [Required]\n    public string? City { get; set; }\n\n    [Required]\n    public string? State { get; set; }\n\n    [Required]\n    public string? Country { get; set; }\n\n    [Required]\n    public string? ZipCode { get; set; }\n\n    public string? CardNumber { get; set; }\n\n    public string? CardHolderName { get; set; }\n\n    public string? CardSecurityNumber { get; set; }\n\n    public DateTime? CardExpiration { get; set; }\n\n    public int CardTypeId { get; set; }\n\n    public string? Buyer { get; set; }\n    public Guid RequestId { get; set; }\n}\n"
  },
  {
    "path": "src/WebApp/Services/BasketItem.cs",
    "content": "﻿namespace eShop.WebApp.Services;\n\npublic class BasketItem\n{\n    public required string Id { get; set; }\n    public int ProductId { get; set; }\n    public required string ProductName { get; set; }\n    public decimal UnitPrice { get; set; }\n    public decimal OldUnitPrice { get; set; }\n    public int Quantity { get; set; }\n}\n"
  },
  {
    "path": "src/WebApp/Services/BasketService.cs",
    "content": "﻿using eShop.Basket.API.Grpc;\nusing GrpcBasketItem = eShop.Basket.API.Grpc.BasketItem;\nusing GrpcBasketClient = eShop.Basket.API.Grpc.Basket.BasketClient;\n\nnamespace eShop.WebApp.Services;\n\npublic class BasketService(GrpcBasketClient basketClient)\n{\n    public async Task<IReadOnlyCollection<BasketQuantity>> GetBasketAsync()\n    {\n        var result = await basketClient.GetBasketAsync(new ());\n        return MapToBasket(result);\n    }\n\n    public async Task DeleteBasketAsync()\n    {\n        await basketClient.DeleteBasketAsync(new DeleteBasketRequest());\n    }\n\n    public async Task UpdateBasketAsync(IReadOnlyCollection<BasketQuantity> basket)\n    {\n        var updatePayload = new UpdateBasketRequest();\n\n        foreach (var item in basket)\n        {\n            var updateItem = new GrpcBasketItem\n            {\n                ProductId = item.ProductId,\n                Quantity = item.Quantity,\n            };\n            updatePayload.Items.Add(updateItem);\n        }\n\n        await basketClient.UpdateBasketAsync(updatePayload);\n    }\n\n    private static List<BasketQuantity> MapToBasket(CustomerBasketResponse response)\n    {\n        var result = new List<BasketQuantity>();\n        foreach (var item in response.Items)\n        {\n            result.Add(new BasketQuantity(item.ProductId, item.Quantity));\n        }\n\n        return result;\n    }\n}\n\npublic record BasketQuantity(int ProductId, int Quantity);\n"
  },
  {
    "path": "src/WebApp/Services/BasketState.cs",
    "content": "﻿using System.Security.Claims;\nusing Microsoft.AspNetCore.Components;\nusing Microsoft.AspNetCore.Components.Authorization;\nusing eShop.WebAppComponents.Catalog;\nusing eShop.WebAppComponents.Services;\n\nnamespace eShop.WebApp.Services;\n\npublic class BasketState(\n    BasketService basketService,\n    CatalogService catalogService,\n    OrderingService orderingService,\n    AuthenticationStateProvider authenticationStateProvider) : IBasketState\n{\n    private Task<IReadOnlyCollection<BasketItem>>? _cachedBasket;\n    private HashSet<BasketStateChangedSubscription> _changeSubscriptions = new();\n\n    public Task DeleteBasketAsync()\n        => basketService.DeleteBasketAsync();\n\n    public async Task<IReadOnlyCollection<BasketItem>> GetBasketItemsAsync()\n        => (await GetUserAsync()).Identity?.IsAuthenticated == true\n        ? await FetchBasketItemsAsync()\n        : [];\n\n    public IDisposable NotifyOnChange(EventCallback callback)\n    {\n        var subscription = new BasketStateChangedSubscription(this, callback);\n        _changeSubscriptions.Add(subscription);\n        return subscription;\n    }\n\n    public async Task AddAsync(CatalogItem item)\n    {\n        var items = (await FetchBasketItemsAsync()).Select(i => new BasketQuantity(i.ProductId, i.Quantity)).ToList();\n        bool found = false;\n        for (var i = 0; i < items.Count; i++)\n        {\n            var existing = items[i];\n            if (existing.ProductId == item.Id)\n            {\n                items[i] = existing with { Quantity = existing.Quantity + 1 };\n                found = true;\n                break;\n            }\n        }\n\n        if (!found)\n        {\n            items.Add(new BasketQuantity(item.Id, 1));\n        }\n\n        _cachedBasket = null;\n        await basketService.UpdateBasketAsync(items);\n        await NotifyChangeSubscribersAsync();\n    }\n\n    public async Task SetQuantityAsync(int productId, int quantity)\n    {\n        var existingItems = (await FetchBasketItemsAsync()).ToList();\n        if (existingItems.FirstOrDefault(row => row.ProductId == productId) is { } row)\n        {\n            if (quantity > 0)\n            {\n                row.Quantity = quantity;\n            }\n            else\n            {\n                existingItems.Remove(row);\n            }\n\n            _cachedBasket = null;\n            await basketService.UpdateBasketAsync(existingItems.Select(i => new BasketQuantity(i.ProductId, i.Quantity)).ToList());\n            await NotifyChangeSubscribersAsync();\n        }\n    }\n\n    public async Task CheckoutAsync(BasketCheckoutInfo checkoutInfo)\n    {\n        if (checkoutInfo.RequestId == default)\n        {\n            checkoutInfo.RequestId = Guid.NewGuid();\n        }\n\n        var buyerId = await authenticationStateProvider.GetBuyerIdAsync() ?? throw new InvalidOperationException(\"User does not have a buyer ID\");\n        var userName = await authenticationStateProvider.GetUserNameAsync() ?? throw new InvalidOperationException(\"User does not have a user name\");\n\n        // Get details for the items in the basket\n        var orderItems = await FetchBasketItemsAsync();\n\n        // Call into Ordering.API to create the order using those details\n        var request = new CreateOrderRequest(\n            UserId: buyerId,\n            UserName: userName,\n            City: checkoutInfo.City!,\n            Street: checkoutInfo.Street!,\n            State: checkoutInfo.State!,\n            Country: checkoutInfo.Country!,\n            ZipCode: checkoutInfo.ZipCode!,\n            CardNumber: \"1111222233334444\",\n            CardHolderName: \"TESTUSER\",\n            CardExpiration: DateTime.UtcNow.AddYears(1),\n            CardSecurityNumber: \"111\",\n            CardTypeId: checkoutInfo.CardTypeId,\n            Buyer: buyerId,\n            Items: [.. orderItems]);\n        await orderingService.CreateOrder(request, checkoutInfo.RequestId);\n        await DeleteBasketAsync();\n    }\n\n    private Task NotifyChangeSubscribersAsync()\n        => Task.WhenAll(_changeSubscriptions.Select(s => s.NotifyAsync()));\n\n    private async Task<ClaimsPrincipal> GetUserAsync()\n        => (await authenticationStateProvider.GetAuthenticationStateAsync()).User;\n\n    private Task<IReadOnlyCollection<BasketItem>> FetchBasketItemsAsync()\n    {\n        return _cachedBasket ??= FetchCoreAsync();\n\n        async Task<IReadOnlyCollection<BasketItem>> FetchCoreAsync()\n        {\n            var quantities = await basketService.GetBasketAsync();\n            if (quantities.Count == 0)\n            {\n                return [];\n            }\n\n            // Get details for the items in the basket\n            var basketItems = new List<BasketItem>();\n            var productIds = quantities.Select(row => row.ProductId);\n            var catalogItems = (await catalogService.GetCatalogItems(productIds)).ToDictionary(k => k.Id, v => v);\n            foreach (var item in quantities)\n            {\n                var catalogItem = catalogItems[item.ProductId];\n                var orderItem = new BasketItem\n                {\n                    Id = Guid.NewGuid().ToString(), // TODO: this value is meaningless, use ProductId instead.\n                    ProductId = catalogItem.Id,\n                    ProductName = catalogItem.Name,\n                    UnitPrice = catalogItem.Price,\n                    Quantity = item.Quantity,\n                };\n                basketItems.Add(orderItem);\n            }\n\n            return basketItems;\n        }\n    }\n\n    private class BasketStateChangedSubscription(BasketState Owner, EventCallback Callback) : IDisposable\n    {\n        public Task NotifyAsync() => Callback.InvokeAsync();\n        public void Dispose() => Owner._changeSubscriptions.Remove(this);\n    }\n}\n\npublic record CreateOrderRequest(\n    string UserId,\n    string UserName,\n    string City,\n    string Street,\n    string State,\n    string Country,\n    string ZipCode,\n    string CardNumber,\n    string CardHolderName,\n    DateTime CardExpiration,\n    string CardSecurityNumber,\n    int CardTypeId,\n    string Buyer,\n    List<BasketItem> Items);\n"
  },
  {
    "path": "src/WebApp/Services/IBasketState.cs",
    "content": "﻿using eShop.WebAppComponents.Catalog;\n\nnamespace eShop.WebApp.Services\n{\n    public interface IBasketState\n    {\n        public Task<IReadOnlyCollection<BasketItem>> GetBasketItemsAsync();\n\n        public Task AddAsync(CatalogItem item);\n    }\n}\n"
  },
  {
    "path": "src/WebApp/Services/LogOutService.cs",
    "content": "﻿using Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Authentication.Cookies;\nusing Microsoft.AspNetCore.Authentication.OpenIdConnect;\n\nnamespace eShop.WebApp.Services;\n\npublic class LogOutService\n{\n    public async Task LogOutAsync(HttpContext httpContext)\n    {\n        await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);\n        await httpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);\n    }\n}\n"
  },
  {
    "path": "src/WebApp/Services/OrderStatus/IntegrationEvents/EventHandling/OrderStatusChangedToAwaitingValidationIntegrationEventHandler.cs",
    "content": "﻿using eShop.EventBus.Abstractions;\n\nnamespace eShop.WebApp.Services.OrderStatus.IntegrationEvents;\n\npublic class OrderStatusChangedToAwaitingValidationIntegrationEventHandler(\n    OrderStatusNotificationService orderStatusNotificationService,\n    ILogger<OrderStatusChangedToAwaitingValidationIntegrationEventHandler> logger)\n    : IIntegrationEventHandler<OrderStatusChangedToAwaitingValidationIntegrationEvent>\n{\n    public async Task Handle(OrderStatusChangedToAwaitingValidationIntegrationEvent @event)\n    {\n        logger.LogInformation(\"Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})\", @event.Id, @event);\n        await orderStatusNotificationService.NotifyOrderStatusChangedAsync(@event.BuyerIdentityGuid);\n    }\n}\n"
  },
  {
    "path": "src/WebApp/Services/OrderStatus/IntegrationEvents/EventHandling/OrderStatusChangedToCancelledIntegrationEventHandler.cs",
    "content": "﻿using eShop.EventBus.Abstractions;\n\nnamespace eShop.WebApp.Services.OrderStatus.IntegrationEvents;\n\npublic class OrderStatusChangedToCancelledIntegrationEventHandler(\n    OrderStatusNotificationService orderStatusNotificationService,\n    ILogger<OrderStatusChangedToCancelledIntegrationEventHandler> logger)\n    : IIntegrationEventHandler<OrderStatusChangedToCancelledIntegrationEvent>\n{\n    public async Task Handle(OrderStatusChangedToCancelledIntegrationEvent @event)\n    {\n        logger.LogInformation(\"Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})\", @event.Id, @event);\n        await orderStatusNotificationService.NotifyOrderStatusChangedAsync(@event.BuyerIdentityGuid);\n    }\n}\n"
  },
  {
    "path": "src/WebApp/Services/OrderStatus/IntegrationEvents/EventHandling/OrderStatusChangedToPaidIntegrationEventHandler.cs",
    "content": "﻿using eShop.EventBus.Abstractions;\n\nnamespace eShop.WebApp.Services.OrderStatus.IntegrationEvents;\n\npublic class OrderStatusChangedToPaidIntegrationEventHandler(\n    OrderStatusNotificationService orderStatusNotificationService,\n    ILogger<OrderStatusChangedToPaidIntegrationEventHandler> logger)\n    : IIntegrationEventHandler<OrderStatusChangedToPaidIntegrationEvent>\n{\n    public async Task Handle(OrderStatusChangedToPaidIntegrationEvent @event)\n    {\n        logger.LogInformation(\"Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})\", @event.Id, @event);\n        await orderStatusNotificationService.NotifyOrderStatusChangedAsync(@event.BuyerIdentityGuid);\n    }\n}\n"
  },
  {
    "path": "src/WebApp/Services/OrderStatus/IntegrationEvents/EventHandling/OrderStatusChangedToShippedIntegrationEventHandler.cs",
    "content": "﻿using eShop.EventBus.Abstractions;\n\nnamespace eShop.WebApp.Services.OrderStatus.IntegrationEvents;\n\npublic class OrderStatusChangedToShippedIntegrationEventHandler(\n    OrderStatusNotificationService orderStatusNotificationService,\n    ILogger<OrderStatusChangedToShippedIntegrationEventHandler> logger)\n    : IIntegrationEventHandler<OrderStatusChangedToShippedIntegrationEvent>\n{\n    public async Task Handle(OrderStatusChangedToShippedIntegrationEvent @event)\n    {\n        logger.LogInformation(\"Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})\", @event.Id, @event);\n        await orderStatusNotificationService.NotifyOrderStatusChangedAsync(@event.BuyerIdentityGuid);\n    }\n}\n"
  },
  {
    "path": "src/WebApp/Services/OrderStatus/IntegrationEvents/EventHandling/OrderStatusChangedToStockConfirmedIntegrationEventHandler.cs",
    "content": "﻿using eShop.EventBus.Abstractions;\n\nnamespace eShop.WebApp.Services.OrderStatus.IntegrationEvents;\n\npublic class OrderStatusChangedToStockConfirmedIntegrationEventHandler(\n    OrderStatusNotificationService orderStatusNotificationService,\n    ILogger<OrderStatusChangedToStockConfirmedIntegrationEventHandler> logger)\n    : IIntegrationEventHandler<OrderStatusChangedToStockConfirmedIntegrationEvent>\n{\n    public async Task Handle(OrderStatusChangedToStockConfirmedIntegrationEvent @event)\n    {\n        logger.LogInformation(\"Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})\", @event.Id, @event);\n        await orderStatusNotificationService.NotifyOrderStatusChangedAsync(@event.BuyerIdentityGuid);\n    }\n}\n"
  },
  {
    "path": "src/WebApp/Services/OrderStatus/IntegrationEvents/EventHandling/OrderStatusChangedToSubmittedIntegrationEventHandler.cs",
    "content": "﻿using eShop.EventBus.Abstractions;\n\nnamespace eShop.WebApp.Services.OrderStatus.IntegrationEvents;\n\npublic class OrderStatusChangedToSubmittedIntegrationEventHandler(\n    OrderStatusNotificationService orderStatusNotificationService,\n    ILogger<OrderStatusChangedToSubmittedIntegrationEventHandler> logger)\n    : IIntegrationEventHandler<OrderStatusChangedToSubmittedIntegrationEvent>\n{\n    public async Task Handle(OrderStatusChangedToSubmittedIntegrationEvent @event)\n    {\n        logger.LogInformation(\"Handling integration event: {IntegrationEventId} - ({@IntegrationEvent})\", @event.Id, @event);\n        await orderStatusNotificationService.NotifyOrderStatusChangedAsync(@event.BuyerIdentityGuid);\n    }\n}\n"
  },
  {
    "path": "src/WebApp/Services/OrderStatus/IntegrationEvents/Events/OrderStatusChangedToAwaitingValidationIntegrationEvent.cs",
    "content": "﻿using eShop.EventBus.Events;\n\nnamespace eShop.WebApp.Services.OrderStatus.IntegrationEvents;\n\npublic record OrderStatusChangedToAwaitingValidationIntegrationEvent : IntegrationEvent\n{\n    public int OrderId { get; }\n    public string OrderStatus { get; }\n    public string BuyerName { get; }\n    public string BuyerIdentityGuid { get; }\n\n    public OrderStatusChangedToAwaitingValidationIntegrationEvent(\n        int orderId, string orderStatus, string buyerName, string buyerIdentityGuid)\n    {\n        OrderId = orderId;\n        OrderStatus = orderStatus;\n        BuyerName = buyerName;\n        BuyerIdentityGuid = buyerIdentityGuid;\n    }\n}\n\n"
  },
  {
    "path": "src/WebApp/Services/OrderStatus/IntegrationEvents/Events/OrderStatusChangedToCancelledIntegrationEvent.cs",
    "content": "﻿using eShop.EventBus.Events;\n\nnamespace eShop.WebApp.Services.OrderStatus.IntegrationEvents;\n\npublic record OrderStatusChangedToCancelledIntegrationEvent : IntegrationEvent\n{\n    public int OrderId { get; }\n    public string OrderStatus { get; }\n    public string BuyerName { get; }\n    public string BuyerIdentityGuid { get; }\n\n    public OrderStatusChangedToCancelledIntegrationEvent(\n        int orderId, string orderStatus, string buyerName, string buyerIdentityGuid)\n    {\n        OrderId = orderId;\n        OrderStatus = orderStatus;\n        BuyerName = buyerName;\n        BuyerIdentityGuid = buyerIdentityGuid;\n    }\n}\n\n"
  },
  {
    "path": "src/WebApp/Services/OrderStatus/IntegrationEvents/Events/OrderStatusChangedToPaidIntegrationEvent.cs",
    "content": "﻿using eShop.EventBus.Events;\n\nnamespace eShop.WebApp.Services.OrderStatus.IntegrationEvents;\n\npublic record OrderStatusChangedToPaidIntegrationEvent : IntegrationEvent\n{\n    public int OrderId { get; }\n    public string OrderStatus { get; }\n    public string BuyerName { get; }\n    public string BuyerIdentityGuid { get; }\n\n    public OrderStatusChangedToPaidIntegrationEvent(\n        int orderId, string orderStatus, string buyerName, string buyerIdentityGuid)\n    {\n        OrderId = orderId;\n        OrderStatus = orderStatus;\n        BuyerName = buyerName;\n        BuyerIdentityGuid = buyerIdentityGuid;\n    }\n}\n"
  },
  {
    "path": "src/WebApp/Services/OrderStatus/IntegrationEvents/Events/OrderStatusChangedToShippedIntegrationEvent.cs",
    "content": "﻿using eShop.EventBus.Events;\n\nnamespace eShop.WebApp.Services.OrderStatus.IntegrationEvents;\n\npublic record OrderStatusChangedToShippedIntegrationEvent : IntegrationEvent\n{\n    public int OrderId { get; }\n    public string OrderStatus { get; }\n    public string BuyerName { get; }\n    public string BuyerIdentityGuid { get; }\n\n    public OrderStatusChangedToShippedIntegrationEvent(\n        int orderId, string orderStatus, string buyerName, string buyerIdentityGuid)\n    {\n        OrderId = orderId;\n        OrderStatus = orderStatus;\n        BuyerName = buyerName;\n        BuyerIdentityGuid = buyerIdentityGuid;\n    }\n}\n\n"
  },
  {
    "path": "src/WebApp/Services/OrderStatus/IntegrationEvents/Events/OrderStatusChangedToStockConfirmedIntegrationEvent.cs",
    "content": "﻿using eShop.EventBus.Events;\n\nnamespace eShop.WebApp.Services.OrderStatus.IntegrationEvents;\n\npublic record OrderStatusChangedToStockConfirmedIntegrationEvent : IntegrationEvent\n{\n    public int OrderId { get; }\n    public string OrderStatus { get; }\n    public string BuyerName { get; }\n    public string BuyerIdentityGuid { get; }\n\n    public OrderStatusChangedToStockConfirmedIntegrationEvent(\n        int orderId, string orderStatus, string buyerName, string buyerIdentityGuid)\n    {\n        OrderId = orderId;\n        OrderStatus = orderStatus;\n        BuyerName = buyerName;\n        BuyerIdentityGuid = buyerIdentityGuid;\n    }\n}\n"
  },
  {
    "path": "src/WebApp/Services/OrderStatus/IntegrationEvents/Events/OrderStatusChangedToSubmittedIntegrationEvent.cs",
    "content": "﻿using eShop.EventBus.Events;\n\nnamespace eShop.WebApp.Services.OrderStatus.IntegrationEvents;\n\npublic record OrderStatusChangedToSubmittedIntegrationEvent : IntegrationEvent\n{\n    public int OrderId { get; }\n    public string OrderStatus { get; }\n    public string BuyerName { get; }\n    public string BuyerIdentityGuid { get; }\n\n    public OrderStatusChangedToSubmittedIntegrationEvent(\n        int orderId, string orderStatus, string buyerName, string buyerIdentityGuid)\n    {\n        OrderId = orderId;\n        OrderStatus = orderStatus;\n        BuyerName = buyerName;\n        BuyerIdentityGuid = buyerIdentityGuid;\n    }\n}\n"
  },
  {
    "path": "src/WebApp/Services/OrderStatus/OrderStatusNotificationService.cs",
    "content": "﻿namespace eShop.WebApp.Services;\n\npublic class OrderStatusNotificationService\n{\n    // Locking manually because we need multiple values per key, and only need to lock very briefly\n    private readonly object _subscriptionsLock = new();\n    private readonly Dictionary<string, HashSet<Subscription>> _subscriptionsByBuyerId = new();\n\n    public IDisposable SubscribeToOrderStatusNotifications(string buyerId, Func<Task> callback)\n    {\n        var subscription = new Subscription(this, buyerId, callback);\n\n        lock (_subscriptionsLock)\n        {\n            if (!_subscriptionsByBuyerId.TryGetValue(buyerId, out var subscriptions))\n            {\n                subscriptions = [];\n                _subscriptionsByBuyerId.Add(buyerId, subscriptions);\n            }\n\n            subscriptions.Add(subscription);\n        }\n\n        return subscription;\n    }\n\n    public Task NotifyOrderStatusChangedAsync(string buyerId)\n    {\n        lock (_subscriptionsLock)\n        {\n            return _subscriptionsByBuyerId.TryGetValue(buyerId, out var subscriptions)\n                ? Task.WhenAll(subscriptions.Select(s => s.NotifyAsync()))\n                : Task.CompletedTask;\n        }\n    }\n\n    private void Unsubscribe(string buyerId, Subscription subscription)\n    {\n        lock (_subscriptionsLock)\n        {\n            if (_subscriptionsByBuyerId.TryGetValue(buyerId, out var subscriptions))\n            {\n                subscriptions.Remove(subscription);\n                if (subscriptions.Count == 0)\n                {\n                    _subscriptionsByBuyerId.Remove(buyerId);\n                }\n            }\n        }\n    }\n\n    private class Subscription(OrderStatusNotificationService owner, string buyerId, Func<Task> callback) : IDisposable\n    {\n        public Task NotifyAsync()\n        {\n            return callback();\n        }\n\n        public void Dispose()\n            => owner.Unsubscribe(buyerId, this);\n    }\n}\n"
  },
  {
    "path": "src/WebApp/Services/OrderingService.cs",
    "content": "﻿namespace eShop.WebApp.Services;\n\npublic class OrderingService(HttpClient httpClient)\n{\n    private readonly string remoteServiceBaseUrl = \"/api/Orders/\";\n\n    public Task<OrderRecord[]> GetOrders()\n    {\n        return httpClient.GetFromJsonAsync<OrderRecord[]>(remoteServiceBaseUrl)!;\n    }\n\n    public Task CreateOrder(CreateOrderRequest request, Guid requestId)\n    {\n        var requestMessage = new HttpRequestMessage(HttpMethod.Post, remoteServiceBaseUrl);\n        requestMessage.Headers.Add(\"x-requestid\", requestId.ToString());\n        requestMessage.Content = JsonContent.Create(request);\n        return httpClient.SendAsync(requestMessage);\n    }\n}\n\npublic record OrderRecord(\n    int OrderNumber,\n    DateTime Date,\n    string Status,\n    decimal Total);\n"
  },
  {
    "path": "src/WebApp/Services/ProductImageUrlProvider.cs",
    "content": "﻿using eShop.WebAppComponents.Services;\n\nnamespace eShop.WebApp.Services;\n\npublic class ProductImageUrlProvider : IProductImageUrlProvider\n{\n    public string GetProductImageUrl(int productId)\n        => $\"product-images/{productId}?api-version=2.0\";\n}\n"
  },
  {
    "path": "src/WebApp/WebApp.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <UserSecretsId>2d86f364-a439-47c5-9468-3b85a7d9a18e</UserSecretsId>\n    <Nullable>enable</Nullable>\n    <RootNamespace>eShop.WebApp</RootNamespace>\n\n    <!-- Disable RZ10021, which is failing due to a Razor bug, according to Mackinnon. -->\n    <NoWarn>$(NoWarn);RZ10021</NoWarn>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\eShop.ServiceDefaults\\eShop.ServiceDefaults.csproj\" />\n    <ProjectReference Include=\"..\\EventBusRabbitMQ\\EventBusRabbitMQ.csproj\" />\n    <ProjectReference Include=\"..\\WebAppComponents\\WebAppComponents.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Asp.Versioning.Http.Client\" />\n    <PackageReference Include=\"Aspire.Azure.AI.OpenAI\" />\n    <PackageReference Include=\"CommunityToolkit.Aspire.OllamaSharp\" />\n    <PackageReference Include=\"Microsoft.Extensions.ServiceDiscovery.Yarp\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Authentication.OpenIdConnect\" />\n    <PackageReference Include=\"Google.Protobuf\" />\n    <PackageReference Include=\"Grpc.Net.ClientFactory\" />\n    <PackageReference Include=\"Grpc.Tools\" PrivateAssets=\"All\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Protobuf Include=\"..\\Basket.API\\Proto\\basket.proto\" GrpcServices=\"Client\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/WebApp/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/WebApp/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  },\n  \"AllowedHosts\": \"*\",\n  \"EventBus\": {\n    \"SubscriptionClientName\": \"Ordering.webapp\"\n  },\n  \"SessionCookieLifetimeMinutes\": 60\n}\n"
  },
  {
    "path": "src/WebApp/wwwroot/css/app.css",
    "content": "/* plus-jakarta-sans-200 - latin */\n@font-face {\n    font-display: swap;\n    font-family: 'Plus Jakarta Sans';\n    font-style: normal;\n    font-weight: 200;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-200.woff2') format('woff2');\n}\n\n/* plus-jakarta-sans-200italic - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: italic;\n    font-weight: 200;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-200italic.woff2') format('woff2');\n}\n\n/* plus-jakarta-sans-300 - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: normal;\n    font-weight: 300;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-300.woff2') format('woff2');\n}\n\n/* plus-jakarta-sans-300italic - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: italic;\n    font-weight: 300;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-300italic.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-regular - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: normal;\n    font-weight: 400;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-regular.woff2') format('woff2'); \n}\n  \n/* plus-jakarta-sans-italic - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: italic;\n    font-weight: 400;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-italic.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-500 - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: normal;\n    font-weight: 500;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-500.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-500italic - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: italic;\n    font-weight: 500;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-500italic.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-600 - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: normal;\n    font-weight: 600;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-600.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-600italic - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: italic;\n    font-weight: 600;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-600italic.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-700 - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: normal;\n    font-weight: 700;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-700.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-700italic - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: italic;\n    font-weight: 700;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-700italic.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-800 - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: normal;\n    font-weight: 800;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-800.woff2') format('woff2'); \n}\n\n/* plus-jakarta-sans-800italic - latin */\n@font-face {\n    font-display: swap; \n    font-family: 'Plus Jakarta Sans';\n    font-style: italic;\n    font-weight: 800;\n    src: url('../fonts/plus-jakarta-sans-v8-latin-800italic.woff2') format('woff2'); \n}\n\n/* open-sans-300 - latin */\n@font-face {\n    font-display: swap;\n    font-family: 'Open Sans';\n    font-style: normal;\n    font-weight: 300;\n    src: url('../fonts/open-sans-v36-latin-300.woff2') format('woff2');\n}\n\n/* open-sans-300italic - latin */\n@font-face {\n    font-display: swap;\n    font-family: 'Open Sans';\n    font-style: italic;\n    font-weight: 300;\n    src: url('../fonts/open-sans-v36-latin-300italic.woff2') format('woff2');\n}\n\n/* open-sans-regular - latin */\n@font-face {\n    font-display: swap;\n    font-family: 'Open Sans';\n    font-style: normal;\n    font-weight: 400;\n    src: url('../fonts/open-sans-v36-latin-regular.woff2') format('woff2');\n}\n\n/* open-sans-italic - latin */\n@font-face {\n    font-display: swap;\n    font-family: 'Open Sans';\n    font-style: italic;\n    font-weight: 400;\n    src: url('../fonts/open-sans-v36-latin-italic.woff2') format('woff2');\n}\n\n/* open-sans-500 - latin */\n@font-face {\n    font-display: swap;\n    font-family: 'Open Sans';\n    font-style: normal;\n    font-weight: 500;\n    src: url('../fonts/open-sans-v36-latin-500.woff2') format('woff2');\n}\n  \n/* open-sans-500italic - latin */\n@font-face {\n    font-display: swap;\n    font-family: 'Open Sans';\n    font-style: italic;\n    font-weight: 500;\n    src: url('../fonts/open-sans-v36-latin-500italic.woff2') format('woff2');\n}\n\n/* open-sans-600 - latin */\n@font-face {\n    font-display: swap;\n    font-family: 'Open Sans';\n    font-style: normal;\n    font-weight: 600;\n    src: url('../fonts/open-sans-v36-latin-600.woff2') format('woff2');\n}\n\n/* open-sans-600italic - latin */\n@font-face {\n    font-display: swap;\n    font-family: 'Open Sans';\n    font-style: italic;\n    font-weight: 600;\n    src: url('../fonts/open-sans-v36-latin-600italic.woff2') format('woff2');\n}\n\n/* open-sans-700 - latin */\n@font-face {\n    font-display: swap;\n    font-family: 'Open Sans';\n    font-style: normal;\n    font-weight: 700;\n    src: url('../fonts/open-sans-v36-latin-700.woff2') format('woff2');\n}\n\n/* open-sans-700italic - latin */\n@font-face {\n    font-display: swap;\n    font-family: 'Open Sans';\n    font-style: italic;\n    font-weight: 700;\n    src: url('../fonts/open-sans-v36-latin-700italic.woff2') format('woff2');\n}\n\n/* open-sans-800 - latin */\n@font-face {\n    font-display: swap;\n    font-family: 'Open Sans';\n    font-style: normal;\n    font-weight: 800;\n    src: url('../fonts/open-sans-v36-latin-800.woff2') format('woff2');\n}\n\n/* open-sans-800italic - latin */\n@font-face {\n    font-display: swap;\n    font-family: 'Open Sans';\n    font-style: italic;\n    font-weight: 800;\n    src: url('../fonts/open-sans-v36-latin-800italic.woff2') format('woff2');\n}\n\nbody {\n    font-family: 'Plus Jakarta Sans';\n}\n\n.container {\n    position: relative;\n    max-width: 120rem;\n    margin: auto;\n}\n\n.button {\n    display: flex;\n    padding: 1rem 0.75rem;\n    justify-content: center;\n    align-items: center;\n    gap: 0.25rem;\n    align-self: stretch;\n    border: none;\n    text-decoration: none;\n}\n\n.button.button-primary {\n    background: #000;\n    color: #FFF;\n}\n\n.button.button.button-secondary {\n    border: 1px solid #444;\n    background: #FFF;\n    color: #000;\n}\n\nh1:focus {\n    outline: none;\n}\n\n.valid.modified:not([type=checkbox]) {\n    outline: 1px solid #26b050;\n}\n\n.invalid {\n    outline: 1px solid red;\n}\n\n.validation-message {\n    color: red;\n}\n\n.blazor-error-boundary {\n    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;\n    padding: 1rem 1rem 1rem 3.7rem;\n    color: white;\n}\n\n    .blazor-error-boundary::after {\n        content: \"An error has occurred.\"\n    }"
  },
  {
    "path": "src/WebApp/wwwroot/css/normalize.css",
    "content": "/**\n * 1. Correct the line height in all browsers.\n * 2. Prevent adjustments of font size after orientation changes in iOS.\n */\n\n html {\n    line-height: 1.15; /* 1 */\n    -webkit-text-size-adjust: 100%; /* 2 */\n  }\n  \n  /* Sections\n     ========================================================================== */\n  \n  /**\n   * Remove the margin in all browsers.\n   */\n  \n  body {\n    margin: 0;\n  }\n  \n  /**\n   * Render the `main` element consistently in IE.\n   */\n  \n  main {\n    display: block;\n  }\n  \n  /**\n   * Correct the font size and margin on `h1` elements within `section` and\n   * `article` contexts in Chrome, Firefox, and Safari.\n   */\n  \n  h1 {\n    font-size: 2em;\n    margin: 0.67em 0;\n  }\n  \n  /* Grouping content\n     ========================================================================== */\n  \n  /**\n   * 1. Add the correct box sizing in Firefox.\n   * 2. Show the overflow in Edge and IE.\n   */\n  \n  hr {\n    box-sizing: content-box; /* 1 */\n    height: 0; /* 1 */\n    overflow: visible; /* 2 */\n  }\n  \n  /**\n   * 1. Correct the inheritance and scaling of font size in all browsers.\n   * 2. Correct the odd `em` font sizing in all browsers.\n   */\n  \n  pre {\n    font-family: monospace, monospace; /* 1 */\n    font-size: 1em; /* 2 */\n  }\n  \n  /* Text-level semantics\n     ========================================================================== */\n  \n  /**\n   * Remove the gray background on active links in IE 10.\n   */\n  \n  a {\n    background-color: transparent;\n  }\n  \n  /**\n   * 1. Remove the bottom border in Chrome 57-\n   * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n   */\n  \n  abbr[title] {\n    border-bottom: none; /* 1 */\n    text-decoration: underline; /* 2 */\n    text-decoration: underline dotted; /* 2 */\n  }\n  \n  /**\n   * Add the correct font weight in Chrome, Edge, and Safari.\n   */\n  \n  b,\n  strong {\n    font-weight: bolder;\n  }\n  \n  /**\n   * 1. Correct the inheritance and scaling of font size in all browsers.\n   * 2. Correct the odd `em` font sizing in all browsers.\n   */\n  \n  code,\n  kbd,\n  samp {\n    font-family: monospace, monospace; /* 1 */\n    font-size: 1em; /* 2 */\n  }\n  \n  /**\n   * Add the correct font size in all browsers.\n   */\n  \n  small {\n    font-size: 80%;\n  }\n  \n  /**\n   * Prevent `sub` and `sup` elements from affecting the line height in\n   * all browsers.\n   */\n  \n  sub,\n  sup {\n    font-size: 75%;\n    line-height: 0;\n    position: relative;\n    vertical-align: baseline;\n  }\n  \n  sub {\n    bottom: -0.25em;\n  }\n  \n  sup {\n    top: -0.5em;\n  }\n  \n  /* Embedded content\n     ========================================================================== */\n  \n  /**\n   * Remove the border on images inside links in IE 10.\n   */\n  \n  img {\n    border-style: none;\n  }\n  \n  /* Forms\n     ========================================================================== */\n  \n  /**\n   * 1. Change the font styles in all browsers.\n   * 2. Remove the margin in Firefox and Safari.\n   */\n  \n  button,\n  input,\n  optgroup,\n  select,\n  textarea {\n    font-family: inherit; /* 1 */\n    font-size: 100%; /* 1 */\n    line-height: 1.15; /* 1 */\n    margin: 0; /* 2 */\n  }\n  \n  /**\n   * Show the overflow in IE.\n   * 1. Show the overflow in Edge.\n   */\n  \n  button,\n  input { /* 1 */\n    overflow: visible;\n  }\n  \n  /**\n   * Remove the inheritance of text transform in Edge, Firefox, and IE.\n   * 1. Remove the inheritance of text transform in Firefox.\n   */\n  \n  button,\n  select { /* 1 */\n    text-transform: none;\n  }\n  \n  /**\n   * Correct the inability to style clickable types in iOS and Safari.\n   */\n  \n  button,\n  [type=\"button\"],\n  [type=\"reset\"],\n  [type=\"submit\"] {\n    -webkit-appearance: button;\n  }\n  \n  /**\n   * Remove the inner border and padding in Firefox.\n   */\n  \n  button::-moz-focus-inner,\n  [type=\"button\"]::-moz-focus-inner,\n  [type=\"reset\"]::-moz-focus-inner,\n  [type=\"submit\"]::-moz-focus-inner {\n    border-style: none;\n    padding: 0;\n  }\n  \n  /**\n   * Restore the focus styles unset by the previous rule.\n   */\n  \n  button:-moz-focusring,\n  [type=\"button\"]:-moz-focusring,\n  [type=\"reset\"]:-moz-focusring,\n  [type=\"submit\"]:-moz-focusring {\n    outline: 1px dotted ButtonText;\n  }\n  \n  /**\n   * Correct the padding in Firefox.\n   */\n  \n  fieldset {\n    padding: 0.35em 0.75em 0.625em;\n  }\n  \n  /**\n   * 1. Correct the text wrapping in Edge and IE.\n   * 2. Correct the color inheritance from `fieldset` elements in IE.\n   * 3. Remove the padding so developers are not caught out when they zero out\n   *    `fieldset` elements in all browsers.\n   */\n  \n  legend {\n    box-sizing: border-box; /* 1 */\n    color: inherit; /* 2 */\n    display: table; /* 1 */\n    max-width: 100%; /* 1 */\n    padding: 0; /* 3 */\n    white-space: normal; /* 1 */\n  }\n  \n  /**\n   * Add the correct vertical alignment in Chrome, Firefox, and Opera.\n   */\n  \n  progress {\n    vertical-align: baseline;\n  }\n  \n  /**\n   * Remove the default vertical scrollbar in IE 10+.\n   */\n  \n  textarea {\n    overflow: auto;\n  }\n  \n  /**\n   * 1. Add the correct box sizing in IE 10.\n   * 2. Remove the padding in IE 10.\n   */\n  \n  [type=\"checkbox\"],\n  [type=\"radio\"] {\n    box-sizing: border-box; /* 1 */\n    padding: 0; /* 2 */\n  }\n  \n  /**\n   * Correct the cursor style of increment and decrement buttons in Chrome.\n   */\n  \n  [type=\"number\"]::-webkit-inner-spin-button,\n  [type=\"number\"]::-webkit-outer-spin-button {\n    height: auto;\n  }\n  \n  /**\n   * 1. Correct the odd appearance in Chrome and Safari.\n   * 2. Correct the outline style in Safari.\n   */\n  \n  [type=\"search\"] {\n    -webkit-appearance: textfield; /* 1 */\n    outline-offset: -2px; /* 2 */\n  }\n  \n  /**\n   * Remove the inner padding in Chrome and Safari on macOS.\n   */\n  \n  [type=\"search\"]::-webkit-search-decoration {\n    -webkit-appearance: none;\n  }\n  \n  /**\n   * 1. Correct the inability to style clickable types in iOS and Safari.\n   * 2. Change font properties to `inherit` in Safari.\n   */\n  \n  ::-webkit-file-upload-button {\n    -webkit-appearance: button; /* 1 */\n    font: inherit; /* 2 */\n  }\n  \n  /* Interactive\n     ========================================================================== */\n  \n  /*\n   * Add the correct display in Edge, IE 10+, and Firefox.\n   */\n  \n  details {\n    display: block;\n  }\n  \n  /*\n   * Add the correct display in all browsers.\n   */\n  \n  summary {\n    display: list-item;\n  }\n  \n  /* Misc\n     ========================================================================== */\n  \n  /**\n   * Add the correct display in IE 10+.\n   */\n  \n  template {\n    display: none;\n  }\n  \n  /**\n   * Add the correct display in IE 10.\n   */\n  \n  [hidden] {\n    display: none;\n  }"
  },
  {
    "path": "src/WebAppComponents/Catalog/CatalogItem.cs",
    "content": "﻿namespace eShop.WebAppComponents.Catalog;\n\npublic record CatalogItem(\n    int Id,\n    string Name,\n    string Description,\n    decimal Price,\n    string PictureUrl,\n    int CatalogBrandId,\n    CatalogBrand CatalogBrand,\n    int CatalogTypeId,\n    CatalogItemType CatalogType);\n\npublic record CatalogResult(int PageIndex, int PageSize, int Count, List<CatalogItem> Data);\npublic record CatalogBrand(int Id, string Brand);\npublic record CatalogItemType(int Id, string Type);\n"
  },
  {
    "path": "src/WebAppComponents/Catalog/CatalogListItem.razor",
    "content": "﻿@using eShop.WebAppComponents.Item\n@inject IProductImageUrlProvider ProductImages\n\n<div class=\"catalog-item\">\n    <a class=\"catalog-product\" href=\"@ItemHelper.Url(Item)\" data-enhance-nav=\"false\">\n        <span class='catalog-product-image'>\n            <img alt=\"@Item.Name\" src='@ProductImages.GetProductImageUrl(Item)' />\n        </span>\n        <span class='catalog-product-content'>\n            <span class='name'>@Item.Name</span>\n            <span class='price'>$@Item.Price.ToString(\"0.00\")</span>\n        </span>\n    </a>\n</div>\n\n@code {\n    [Parameter, EditorRequired]\n    public required CatalogItem Item { get; set; }\n\n    [Parameter]\n    public bool IsLoggedIn { get; set; }\n}\n"
  },
  {
    "path": "src/WebAppComponents/Catalog/CatalogListItem.razor.css",
    "content": "﻿.catalog-item {\n    flex-basis: calc(33.33% - 2.5rem);\n    flex-shrink: 0;\n    box-sizing: border-box;\n    padding: 2px;\n}\n\n.catalog-item:hover {\n    cursor: pointer;\n    padding: 0;\n    border: 2px solid #000;\n}\n\n.catalog-product {\n    background-color: transparent;\n    padding: 0;\n    margin: 0;\n    border: 0;\n}\n\n.catalog-product-image img {\n    max-width: 100%;\n}\n\n.catalog-product .catalog-product-content {\n    display: flex;\n    padding: 0 0.75rem;\n    align-items: center;\n    gap: 0.5rem;\n    align-self: stretch;\n}\n\n.catalog-product-content .name {\n    color: #000;\n    font-size: 1rem;\n    font-style: normal;\n    font-weight: 600;\n    line-height: 150%;\n    text-align: left;\n}\n\n.catalog-product-content .price {\n    color: #444;\n    text-align: right;\n    font-size: 1rem;\n    font-style: normal;\n    font-weight: 600;\n    line-height: 150%;\n    margin-left: auto;\n}\n\n\n@media only screen and (max-width: 480px) { \n    .catalog-item {\n        flex-basis: calc(100% - 2rem);\n    }    \n}\n\n@media only screen and (min-width: 481px) and (max-width: 1024px) { \n    .catalog-item {\n        flex-basis: calc(50% - 3rem);\n    }    \n}"
  },
  {
    "path": "src/WebAppComponents/Catalog/CatalogSearch.razor",
    "content": "﻿@inject CatalogService CatalogService\n@inject NavigationManager Nav\n\n@if (catalogBrands is not null && catalogItemTypes is not null)\n{\n    <div class=\"catalog-search\">\n        <div class=\"catalog-search-header\">\n            <img role=\"presentation\" src=\"icons/filters.svg\" />\n            Filters\n        </div>\n        <div class=\"catalog-search-types\">\n            <div class=\"catalog-search-group\">\n                <h3>Brand</h3>\n                <div class=\"catalog-search-group-tags\">\n                    <a href=\"@BrandUri(null)\"\n                    class=\"catalog-search-tag @(BrandId == null ? \"active \" : \"\")\">\n                        All\n                    </a>\n                    @foreach (var brand in catalogBrands)\n                    {\n                        <a href=\"@BrandUri(brand.Id)\"\n                        class=\"catalog-search-tag @(BrandId == brand.Id ? \"active \" : \"\")\">\n                            @brand.Brand\n                        </a>\n                    }\n                </div>\n            </div>\n            <div class=\"catalog-search-group\">\n                <h3>Type</h3>\n\n                <div class=\"catalog-search-group-tags\">\n                    <a href=\"@TypeUri(null)\"\n                    class=\"catalog-search-tag @(ItemTypeId == null ? \"active \" : \"\")\">\n                    All\n                    </a>\n                    @foreach (var itemType in catalogItemTypes)\n                    {\n                        <a href=\"@TypeUri(itemType.Id)\"\n                        class=\"catalog-search-tag @(ItemTypeId == itemType.Id ? \"active \" : \"\")\">\n                            @itemType.Type\n                        </a>\n                    }\n                </div>\n            </div>\n        </div>\n    </div>\n}\n\n@code {\n    IEnumerable<CatalogBrand>? catalogBrands;\n    IEnumerable<CatalogItemType>? catalogItemTypes;\n    [Parameter] public int? BrandId { get; set; }\n    [Parameter] public int? ItemTypeId { get; set; }\n\n    protected override async Task OnInitializedAsync()\n    {\n        var brandsTask = CatalogService.GetBrands();\n        var itemTypesTask = CatalogService.GetTypes();\n        await Task.WhenAll(brandsTask, itemTypesTask);\n        catalogBrands = brandsTask.Result;\n        catalogItemTypes = itemTypesTask.Result;\n    }\n\n    private string BrandUri(int? brandId) => Nav.GetUriWithQueryParameters(new Dictionary<string, object?>()\n    {\n        { \"page\", null },\n        { \"brand\", brandId },\n    });\n\n    private string TypeUri(int? typeId) => Nav.GetUriWithQueryParameters(new Dictionary<string, object?>()\n    {\n        { \"page\", null },\n        { \"type\", typeId },\n    });\n}\n"
  },
  {
    "path": "src/WebAppComponents/Catalog/CatalogSearch.razor.css",
    "content": "﻿.catalog-search {\n  flex-shrink: 0;\n  width: 14rem;\n}\n\n.catalog-search .catalog-search-header {\n  display: flex;\n  align-items: center;\n  align-self: stretch;\n  gap: 0.7rem;\n}\n\n.catalog-search .search-badge {\n  background: #000;\n  color: #FFF;\n  font-size: 1rem;\n  font-weight: 600;\n  border-radius: 0.75rem;\n  width: 1.5rem;\n  height: 1.5rem;\n  line-height: 100%;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.catalog-search-group h3 {\n  color: #000;\n  font-size: 1rem;\n  font-weight: 600;\n  line-height: 150%;\n}\n\n.catalog-search-group .catalog-search-group-tags {\n  border-top: 1px solid #404040;\n  display: flex;\n  padding: 0.75rem 0;\n  align-items: center;\n  align-content: center;\n  gap: 0.25rem;\n  align-self: stretch;\n  flex-wrap: wrap;\n  min-width: 12rem;\n}\n\n.catalog-search-tag {\n  display: flex;\n  padding: 0.5rem 0.75rem;\n  justify-content: center;\n  align-items: center;\n  gap: 0.25rem;\n  border-radius: 1.25rem;\n  color: #404040;\n  font-family: 'Open Sans';\n  font-size: 1rem;\n  font-style: normal;\n  font-weight: 400;\n  line-height: 150%;\n  text-decoration: none;\n}\n\n.catalog-search-tag:hover {\n    cursor: pointer;\n    background: #ddd;\n}\n\n.catalog-search-tag.active {\n  background: #000;\n  color: #FFF;\n}\n\n.catalog-search.button {\n  width: 100%;\n  margin-top: 1rem;\n}\n\n@media only screen and (max-width: 480px) { \n  .catalog-search {\n      width: 100%;\n  }\n\n  .catalog-search .catalog-search-header {\n    display: none;\n  }\n\n  .catalog-search-group .catalog-search-group-tags {\n    justify-content: space-between;\n  }\n}\n\n@media only screen and (min-width: 481px) and (max-width: 1024px) { \n  .catalog-search {\n    width: 100%;\n  }\n\n  .catalog-search-types {\n    display: flex;\n    gap: 3rem;\n  }\n\n  .catalog-search-group {\n    flex-basis: calc(50% - 3rem);\n  }\n\n  .catalog-search-group .catalog-search-group-tags {\n    justify-content: space-between;\n  }\n}"
  },
  {
    "path": "src/WebAppComponents/Item/ItemHelper.cs",
    "content": "﻿using eShop.WebAppComponents.Catalog;\n\nnamespace eShop.WebAppComponents.Item;\n\npublic static class ItemHelper\n{\n    public static string Url(CatalogItem item)\n        => $\"item/{item.Id}\";\n}\n"
  },
  {
    "path": "src/WebAppComponents/Services/CatalogService.cs",
    "content": "﻿using System.Net.Http.Json;\nusing System.Web;\nusing eShop.WebAppComponents.Catalog;\n\nnamespace eShop.WebAppComponents.Services;\n\npublic class CatalogService(HttpClient httpClient) : ICatalogService\n{\n    private readonly string remoteServiceBaseUrl = \"api/catalog/\";\n\n    public Task<CatalogItem?> GetCatalogItem(int id)\n    {\n        var uri = $\"{remoteServiceBaseUrl}items/{id}\";\n        return httpClient.GetFromJsonAsync<CatalogItem>(uri);\n    }\n\n    public async Task<CatalogResult> GetCatalogItems(int pageIndex, int pageSize, int? brand, int? type)\n    {\n        var uri = GetAllCatalogItemsUri(remoteServiceBaseUrl, pageIndex, pageSize, brand, type);\n        var result = await httpClient.GetFromJsonAsync<CatalogResult>(uri);\n        return result!;\n    }\n\n    public async Task<List<CatalogItem>> GetCatalogItems(IEnumerable<int> ids)\n    {\n        var uri = $\"{remoteServiceBaseUrl}items/by?ids={string.Join(\"&ids=\", ids)}\";\n        var result = await httpClient.GetFromJsonAsync<List<CatalogItem>>(uri);\n        return result!;\n    }\n\n    public Task<CatalogResult> GetCatalogItemsWithSemanticRelevance(int page, int take, string text)\n    {\n        var url = $\"{remoteServiceBaseUrl}items/withsemanticrelevance?text={HttpUtility.UrlEncode(text)}&pageIndex={page}&pageSize={take}\";\n        var result = httpClient.GetFromJsonAsync<CatalogResult>(url);\n        return result!;\n    }\n\n    public async Task<IEnumerable<CatalogBrand>> GetBrands()\n    {\n        var uri = $\"{remoteServiceBaseUrl}catalogBrands\";\n        var result = await httpClient.GetFromJsonAsync<CatalogBrand[]>(uri);\n        return result!;\n    }\n\n    public async Task<IEnumerable<CatalogItemType>> GetTypes()\n    {\n        var uri = $\"{remoteServiceBaseUrl}catalogTypes\";\n        var result = await httpClient.GetFromJsonAsync<CatalogItemType[]>(uri);\n        return result!;\n    }\n\n    private static string GetAllCatalogItemsUri(string baseUri, int pageIndex, int pageSize, int? brand, int? type)\n    {\n        string filterQs = string.Empty;\n\n        if (type.HasValue)\n        {\n            filterQs += $\"type={type.Value}&\";\n        }\n        if (brand.HasValue)\n        {\n            filterQs += $\"brand={brand.Value}&\";\n        }\n\n        return $\"{baseUri}items?{filterQs}pageIndex={pageIndex}&pageSize={pageSize}\";\n    }\n}\n"
  },
  {
    "path": "src/WebAppComponents/Services/ICatalogService.cs",
    "content": "﻿using System.Collections.Generic;\nusing System.Threading.Tasks;\nusing eShop.WebAppComponents.Catalog;\n\nnamespace eShop.WebAppComponents.Services\n{\n    public interface ICatalogService\n    {\n        Task<CatalogItem?> GetCatalogItem(int id);\n        Task<CatalogResult> GetCatalogItems(int pageIndex, int pageSize, int? brand, int? type);\n        Task<List<CatalogItem>> GetCatalogItems(IEnumerable<int> ids);\n        Task<CatalogResult> GetCatalogItemsWithSemanticRelevance(int page, int take, string text);\n        Task<IEnumerable<CatalogBrand>> GetBrands();\n        Task<IEnumerable<CatalogItemType>> GetTypes();\n    }\n}\n"
  },
  {
    "path": "src/WebAppComponents/Services/IProductImageUrlProvider.cs",
    "content": "﻿using eShop.WebAppComponents.Catalog;\n\nnamespace eShop.WebAppComponents.Services;\n\npublic interface IProductImageUrlProvider\n{\n    string GetProductImageUrl(CatalogItem item)\n        => GetProductImageUrl(item.Id);\n\n    string GetProductImageUrl(int productId);\n}\n"
  },
  {
    "path": "src/WebAppComponents/WebAppComponents.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Razor\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <Nullable>enable</Nullable>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <RootNamespace>eShop.WebAppComponents</RootNamespace>\n  </PropertyGroup>\n\n\n  <ItemGroup>\n    <SupportedPlatform Include=\"browser\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.AspNetCore.Components.Web\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/WebAppComponents/_Imports.razor",
    "content": "﻿@using Microsoft.AspNetCore.Components.Web\n@using eShop.WebAppComponents.Services\n"
  },
  {
    "path": "src/WebhookClient/Components/App.razor",
    "content": "﻿<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <base href=\"/\" />\n    <link rel=\"stylesheet\" href=\"app.css\" />\n    <link rel=\"stylesheet\" href=\"WebhookClient.styles.css\" />\n    <HeadOutlet @rendermode=\"InteractiveServer\" />\n</head>\n\n<body>\n    <Routes @rendermode=\"InteractiveServer\" />\n    <script src=\"_framework/blazor.web.js\"></script>\n</body>\n\n</html>\n"
  },
  {
    "path": "src/WebhookClient/Components/App.razor.css",
    "content": "﻿html, body {\n    margin: 0;\n    padding: 0;\n    font-family: Arial, Helvetica, sans-serif;\n    box-sizing: border-box;\n}\n"
  },
  {
    "path": "src/WebhookClient/Components/Layout/MainLayout.razor",
    "content": "﻿@inherits LayoutComponentBase\n\n<header>\n    <h1><a href=\"\">Order management</a></h1>\n    <div class=\"user-info\">\n        <UserMenu />\n    </div>\n</header>\n\n<main>@Body</main>\n\n<div id=\"blazor-error-ui\">\n    An unhandled error has occurred.\n    <a href=\"\" class=\"reload\">Reload</a>\n    <a class=\"dismiss\">🗙</a>\n</div>\n"
  },
  {
    "path": "src/WebhookClient/Components/Layout/MainLayout.razor.css",
    "content": "#blazor-error-ui {\n    background: lightyellow;\n    bottom: 0;\n    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);\n    display: none;\n    left: 0;\n    padding: 0.6rem 1.25rem 0.7rem 1.25rem;\n    position: fixed;\n    width: 100%;\n    z-index: 1000;\n}\n\n    #blazor-error-ui .dismiss {\n        cursor: pointer;\n        position: absolute;\n        right: 0.75rem;\n        top: 0.5rem;\n    }\n\nheader {\n    background: #156082;\n    color: white;\n    display: flex;\n    align-items: center;\n}\n\nheader, main {\n    padding: 2rem;\n}\n\nh1 {\n    font-size: 1.5rem;\n    font-weight: 400;\n    margin: 0;\n}\n\nh1 a {\n    color: white;\n    text-decoration: none;\n}\n\n    h1 a:hover {\n        text-decoration: underline;\n    }\n\n    .user-info {\n        margin-left: auto;\n        display: flex;\n        align-items: center;\n        gap: 1rem;\n    }\n"
  },
  {
    "path": "src/WebhookClient/Components/Layout/UserMenu.razor",
    "content": "﻿@using Microsoft.AspNetCore.Components.Authorization\n@using System.Web\n@inject NavigationManager Nav\n\n<AuthorizeView>\n    <Authorized>\n        <strong>@context.User.Identity?.Name</strong>\n        <form method=\"post\" action=\"logout\">\n            <AntiforgeryToken />\n            <button class=\"action\" type=\"submit\">Log out</button>\n        </form>\n    </Authorized>\n    <NotAuthorized>\n        <button class=\"action\" @onclick=\"@LogIn\">Log in</button>\n    </NotAuthorized>\n</AuthorizeView>\n\n@code {\n    private void LogIn()\n    {\n        var returnUrl = Nav.ToBaseRelativePath(Nav.Uri);\n        Nav.NavigateTo($\"login?returnUrl={HttpUtility.UrlEncode(returnUrl)}\", forceLoad: true);\n    }\n}\n"
  },
  {
    "path": "src/WebhookClient/Components/Layout/UserMenu.razor.css",
    "content": "﻿button.action {\n    font-size: 0.95rem;\n}\n"
  },
  {
    "path": "src/WebhookClient/Components/Pages/AddWebhook.razor",
    "content": "﻿@page \"/add-webhook\"\n@using Microsoft.Extensions.Options\n@inject IOptions<WebhookClientOptions> options\n@inject NavigationManager Nav\n@inject WebhooksClient WebhooksClient\n\n<h2>Register a new webhook</h2>\n\n<p>This page registers the \"OrderPaid\" Webhook by sending a POST to the WebHooks API. Once the Webhook is set, you will be able to see new paid orders from the <a href=\"\">home</a> page.</p>\n\n<form @onsubmit=\"RegisterAsync\">\n    <p>\n        Token:\n        <input type=\"text\" @bind=\"@token\" placeholder=\"Token\" />\n    </p>\n    <button class=\"action\" type=\"submit\">Register</button>\n</form>\n\n@if (!string.IsNullOrEmpty(message))\n{\n    <p class=\"error-message\">@message</p>\n}\n\n@code {\n    string? token;\n    string? message;\n\n    protected override void OnInitialized()\n    {\n        token = options.Value.Token;\n    }\n\n    private async Task RegisterAsync()\n    {\n        if (string.IsNullOrEmpty(token))\n        {\n            return;\n        }\n\n        message = null;\n        var baseUrl = !string.IsNullOrEmpty(options.Value.SelfUrl)\n            ? options.Value.SelfUrl\n            : Nav.BaseUri;\n        var granturl = $\"{baseUrl}check\";\n        var url = $\"{baseUrl}webhook-received\";\n\n        var payload = new WebhookSubscriptionRequest\n        {\n            Event = \"OrderPaid\",\n            GrantUrl = granturl,\n            Url = url,\n            Token = token\n        };\n\n        var response = await WebhooksClient.AddWebHookAsync(payload);\n        if (response.IsSuccessStatusCode)\n        {\n            Nav.NavigateTo(\"\");\n        }\n        else\n        {\n            message = $\"Registation was rejected with status {(int)response.StatusCode} {response.ReasonPhrase}\";\n        }\n    }\n}\n"
  },
  {
    "path": "src/WebhookClient/Components/Pages/Error.razor",
    "content": "﻿@page \"/Error\"\n@using System.Diagnostics\n\n<PageTitle>Error</PageTitle>\n\n<h1 class=\"text-danger\">Error.</h1>\n<h2 class=\"text-danger\">An error occurred while processing your request.</h2>\n\n@if (ShowRequestId)\n{\n    <p>\n        <strong>Request ID:</strong> <code>@RequestId</code>\n    </p>\n}\n\n<h3>Development Mode</h3>\n<p>\n    Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.\n</p>\n<p>\n    <strong>The Development environment shouldn't be enabled for deployed applications.</strong>\n    It can result in displaying sensitive information from exceptions to end users.\n    For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>\n    and restarting the app.\n</p>\n\n@code{\n    [CascadingParameter]\n    private HttpContext? HttpContext { get; set; }\n\n    private string? RequestId { get; set; }\n    private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);\n\n    protected override void OnInitialized() =>\n        RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;\n}\n"
  },
  {
    "path": "src/WebhookClient/Components/Pages/Home/Home.razor",
    "content": "﻿@page \"/\"\n@using Microsoft.AspNetCore.Components.Authorization\n\n<PageTitle>Order management</PageTitle>\n\n<h2>Registered webhooks</h2>\n\n<AuthorizeView>\n    <Authorized>\n        <RegisteredHooks />\n        <a class=\"action\" href=\"add-webhook\">Add webhook registration</a>\n    </Authorized>\n    <NotAuthorized>\n        <div class=\"grid-placeholder\">\n            Log in to view or edit webhook registrations\n        </div>\n    </NotAuthorized>\n</AuthorizeView>\n\n<h2>Webhook messages received (orders paid)</h2>\n\n<ReceivedMessages />\n"
  },
  {
    "path": "src/WebhookClient/Components/Pages/Home/Home.razor.css",
    "content": "﻿h2 {\n    margin: 2rem 0 1rem 0;\n}\n\nh2:first-child {\n    margin-top: 0;\n}\n"
  },
  {
    "path": "src/WebhookClient/Components/Pages/Home/ReceivedMessages.razor",
    "content": "﻿@inject HooksRepository HooksRepository\n@implements IDisposable\n\n@if (messages is null)\n{\n    <div class=\"grid-placeholder\">Loading...</div>\n}\nelse if (messages.Any())\n{\n    <QuickGrid Items=\"@messages\">\n        <PropertyColumn Sortable=\"@true\" Title=\"When\" Property=\"@(hook => hook.When)\" />\n        <PropertyColumn Sortable=\"@true\" Title=\"Data\" Property=\"@(hook => hook.Data)\" />\n        <PropertyColumn Sortable=\"@true\" Title=\"Token\" Property=\"@(hook => hook.Token)\" />\n    </QuickGrid>\n}\nelse\n{\n    <div class=\"grid-placeholder\">\n        <div>None yet received</div>\n    </div>\n    <p>Webhook messages will appear once a webhook is registered and an order transitions into \"paid\" status.</p>\n}\n\n@code {\n    private IQueryable<WebHookReceived>? messages;\n    private IDisposable? subscription;\n\n    protected override async Task OnInitializedAsync()\n    {\n        subscription = HooksRepository.Subscribe(() => InvokeAsync(OnMessageReceivedAsync));\n        await RefreshDataAsync();\n    }\n\n    private async Task RefreshDataAsync()\n        => messages = (await HooksRepository.GetAll()).AsQueryable();\n\n    private async Task OnMessageReceivedAsync()\n    {\n        try\n        {\n            await RefreshDataAsync();\n            StateHasChanged();\n        }\n        catch (Exception ex)\n        {\n            await DispatchExceptionAsync(ex);\n        }\n    }\n\n    public void Dispose()\n    {\n        subscription?.Dispose();\n    }\n}\n"
  },
  {
    "path": "src/WebhookClient/Components/Pages/Home/RegisteredHooks.razor",
    "content": "﻿@inject WebhooksClient WebhooksClient\n\n@if (webhooks is null)\n{\n    <div class=\"grid-placeholder\">Loading...</div>\n}\nelse if (webhooks.Any())\n{\n    <QuickGrid Items=\"@webhooks\">\n        <PropertyColumn Sortable=\"@true\" IsDefaultSortColumn=\"true\" InitialSortDirection=\"@SortDirection.Descending\" Title=\"Date\" Property=\"@(hook => hook.Date)\" />\n        <PropertyColumn Sortable=\"@true\" Title=\"Destination\" Property=\"@(hook => hook.DestUrl)\" />\n        <PropertyColumn Sortable=\"@true\" Title=\"Token\" Property=\"@(hook => hook.Token)\" />\n    </QuickGrid>\n}\nelse\n{\n    <div class=\"grid-placeholder\">None registered</div>\n}\n\n@code {\n    IQueryable<WebhookResponse>? webhooks;\n\n    protected override async Task OnInitializedAsync()\n    {\n        webhooks = (await WebhooksClient.LoadWebhooks()).AsQueryable();\n    }\n}\n"
  },
  {
    "path": "src/WebhookClient/Components/Pages/LogIn.razor",
    "content": "﻿@page \"/login\"\n@using Microsoft.AspNetCore.Authorization\n@attribute [Authorize]\n@inject NavigationManager Nav\n@code {\n    [SupplyParameterFromQuery] public string? ReturnUrl { get; set; }\n    protected override void OnInitialized()\n    {\n        var returnUrl = new Uri(ReturnUrl ?? \"\", UriKind.Relative);\n        Nav.NavigateTo(returnUrl.ToString(), replace: true);\n    }\n}\n"
  },
  {
    "path": "src/WebhookClient/Components/Routes.razor",
    "content": "﻿<Router AppAssembly=\"@typeof(Program).Assembly\">\n    <Found Context=\"routeData\">\n        <RouteView RouteData=\"@routeData\" DefaultLayout=\"@typeof(Layout.MainLayout)\" />\n        <FocusOnNavigate RouteData=\"@routeData\" Selector=\"h1\" />\n    </Found>\n</Router>\n"
  },
  {
    "path": "src/WebhookClient/Components/_Imports.razor",
    "content": "﻿@using System.Net.Http\n@using System.Net.Http.Json\n@using Microsoft.AspNetCore.Components.Forms\n@using Microsoft.AspNetCore.Components.Routing\n@using Microsoft.AspNetCore.Components.QuickGrid\n@using Microsoft.AspNetCore.Components.Web\n@using static Microsoft.AspNetCore.Components.Web.RenderMode\n@using Microsoft.AspNetCore.Components.Web.Virtualization\n@using Microsoft.JSInterop\n"
  },
  {
    "path": "src/WebhookClient/Endpoints/AuthenticationEndpoints.cs",
    "content": "﻿using Microsoft.AspNetCore.Antiforgery;\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Authentication.Cookies;\nusing Microsoft.AspNetCore.Authentication.OpenIdConnect;\n\nnamespace eShop.WebhookClient.Endpoints;\n\npublic static class AuthenticationEndpoints\n{\n    public static IEndpointRouteBuilder MapAuthenticationEndpoints(this IEndpointRouteBuilder app)\n    {\n        app.MapPost(\"/logout\", async (HttpContext httpContext, IAntiforgery antiforgery) =>\n        {\n            await antiforgery.ValidateRequestAsync(httpContext);\n            await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);\n            await httpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);\n        });\n\n        return app;\n    }\n}\n"
  },
  {
    "path": "src/WebhookClient/Endpoints/WebhookEndpoints.cs",
    "content": "﻿using Microsoft.AspNetCore.Http.HttpResults;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace eShop.WebhookClient.Endpoints;\n\npublic static class WebhookEndpoints\n{\n    public static IEndpointRouteBuilder MapWebhookEndpoints(this IEndpointRouteBuilder app)\n    {\n        const string webhookCheckHeader = \"X-eshop-whtoken\";\n\n        var configuration = app.ServiceProvider.GetRequiredService<IConfiguration>();\n        bool.TryParse(configuration[\"ValidateToken\"], out var validateToken);\n        var tokenToValidate = configuration[\"WebhookClientOptions:Token\"];\n\n        app.MapMethods(\"/check\", [HttpMethods.Options], Results<Ok, BadRequest<string>> ([FromHeader(Name = webhookCheckHeader)] string value, HttpResponse response) =>\n        {\n            if (!validateToken || value == tokenToValidate)\n            {\n                if (!string.IsNullOrWhiteSpace(value))\n                {\n                    response.Headers.Append(webhookCheckHeader, value);\n                }\n\n                return TypedResults.Ok();\n            }\n\n            return TypedResults.BadRequest(\"Invalid token\");\n        });\n\n        app.MapPost(\"/webhook-received\", async (WebhookData hook, HttpRequest request, ILogger<Program> logger, HooksRepository hooksRepository) =>\n        {\n            var token = request.Headers[webhookCheckHeader];\n\n            logger.LogInformation(\"Received hook with token {Token}. My token is {MyToken}. Token validation is set to {ValidateToken}\", token, tokenToValidate, validateToken);\n\n            if (!validateToken || tokenToValidate == token)\n            {\n                logger.LogInformation(\"Received hook is going to be processed\");\n                var newHook = new WebHookReceived()\n                {\n                    Data = hook.Payload,\n                    When = hook.When,\n                    Token = token\n                };\n                await hooksRepository.AddNew(newHook);\n                logger.LogInformation(\"Received hook was processed.\");\n                return Results.Ok(newHook);\n            }\n\n            logger.LogInformation(\"Received hook is NOT processed - Bad Request returned.\");\n            return Results.BadRequest();\n        });\n\n        return app;\n    }\n}\n"
  },
  {
    "path": "src/WebhookClient/Extensions/Extensions.cs",
    "content": "﻿using Microsoft.AspNetCore.Authentication.Cookies;\nusing Microsoft.AspNetCore.Authentication.OpenIdConnect;\nusing Microsoft.AspNetCore.Components.Authorization;\nusing Microsoft.AspNetCore.Components.Server;\n\nnamespace eShop.WebhookClient.Extensions;\n\npublic static class Extensions\n{\n    public static void AddApplicationServices(this IHostApplicationBuilder builder)\n    {\n        builder.AddAuthenticationServices();\n\n        // Application services\n        builder.Services.AddOptions<WebhookClientOptions>().BindConfiguration(nameof(WebhookClientOptions));\n        builder.Services.AddSingleton<HooksRepository>();\n\n        // HTTP client registrations\n        builder.Services.AddHttpClient<WebhooksClient>(o => o.BaseAddress = new(\"http://webhooks-api\"))\n            .AddApiVersion(1.0)\n            .AddAuthToken();\n    }\n\n    public static void AddAuthenticationServices(this IHostApplicationBuilder builder)\n    {\n        var configuration = builder.Configuration;\n        var services = builder.Services;\n\n        var identityUrl = configuration.GetRequiredValue(\"IdentityUrl\");\n        var callBackUrl = configuration.GetRequiredValue(\"CallBackUrl\");\n        var sessionCookieLifetime = configuration.GetValue(\"SessionCookieLifetimeMinutes\", 60);\n\n        // Add Authentication services\n        services.AddAuthorization();\n        services.AddAuthentication(options =>\n        {\n            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;\n            options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;\n        })\n        .AddCookie(options =>\n        {\n            options.ExpireTimeSpan = TimeSpan.FromMinutes(sessionCookieLifetime);\n\n            // Must be distinct from WebApp's cookie name, otherwise the two sites will interfere\n            // with each other when both are on localhost (yes, even when they are on different ports)\n            options.Cookie.Name = \".AspNetCore.WebHooksClientIdentity\";\n        })\n        .AddOpenIdConnect(options =>\n        {\n            options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;\n            options.Authority = identityUrl.ToString();\n            options.SignedOutRedirectUri = callBackUrl.ToString();\n            options.ClientId = \"webhooksclient\";\n            options.ClientSecret = \"secret\";\n            options.ResponseType = \"code\";\n            options.SaveTokens = true;\n            options.GetClaimsFromUserInfoEndpoint = true;\n            options.RequireHttpsMetadata = false;\n            options.Scope.Add(\"openid\");\n            options.Scope.Add(\"webhooks\");\n        });\n\n        services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();\n        services.AddCascadingAuthenticationState();\n    }\n}\n"
  },
  {
    "path": "src/WebhookClient/GlobalUsings.cs",
    "content": "﻿global using eShop.ServiceDefaults;\nglobal using eShop.WebhookClient.Components;\nglobal using eShop.WebhookClient.Endpoints;\nglobal using eShop.WebhookClient.Extensions;\nglobal using eShop.WebhookClient.Services;\n"
  },
  {
    "path": "src/WebhookClient/Program.cs",
    "content": "﻿var builder = WebApplication.CreateBuilder(args);\n\nbuilder.AddServiceDefaults();\n\nbuilder.Services.AddRazorComponents().AddInteractiveServerComponents();\n\nbuilder.AddApplicationServices();\n\nvar app = builder.Build();\n\napp.MapDefaultEndpoints();\n\n// Configure the HTTP request pipeline.\nif (!app.Environment.IsDevelopment())\n{\n    app.UseExceptionHandler(\"/Error\", createScopeForErrors: true);\n    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.\n    app.UseHsts();\n}\n\napp.UseAntiforgery();\n\napp.UseStaticFiles();\n\napp.MapRazorComponents<App>().AddInteractiveServerRenderMode();\n\napp.MapAuthenticationEndpoints();\n\napp.MapWebhookEndpoints();\n\napp.Run();\n"
  },
  {
    "path": "src/WebhookClient/Properties/launchSettings.json",
    "content": "{\n  \"$schema\": \"http://json.schemastore.org/launchsettings.json\",\n    \"iisSettings\": {\n      \"windowsAuthentication\": false,\n      \"anonymousAuthentication\": true,\n      \"iisExpress\": {\n        \"applicationUrl\": \"http://localhost:64178\",\n        \"sslPort\": 44365\n      }\n    },\n    \"profiles\": {\n      \"http\": {\n        \"commandName\": \"Project\",\n        \"dotnetRunMessages\": true,\n        \"launchBrowser\": true,\n        \"applicationUrl\": \"http://localhost:5062\",\n        \"environmentVariables\": {\n          \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n        }\n      },\n      \"https\": {\n        \"commandName\": \"Project\",\n        \"dotnetRunMessages\": true,\n        \"launchBrowser\": true,\n        \"applicationUrl\": \"https://localhost:7260;http://localhost:5062\",\n        \"environmentVariables\": {\n          \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n        }\n      },\n      \"IIS Express\": {\n        \"commandName\": \"IISExpress\",\n        \"launchBrowser\": true,\n        \"environmentVariables\": {\n          \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n        }\n      }\n    }\n  }\n"
  },
  {
    "path": "src/WebhookClient/Services/HooksRepository.cs",
    "content": "﻿using System.Collections.Concurrent;\n\nnamespace eShop.WebhookClient.Services;\n\npublic class HooksRepository\n{\n    private readonly ConcurrentQueue<WebHookReceived> _data = new();\n    private readonly ConcurrentDictionary<OnChangeSubscription, object?> _onChangeSubscriptions = new();\n\n    public Task AddNew(WebHookReceived hook)\n    {\n        _data.Enqueue(hook);\n\n        foreach (var subscription in _onChangeSubscriptions)\n        {\n            try\n            {\n                _ = subscription.Key.NotifyAsync();\n            }\n            catch (Exception)\n            {\n                // It's the subscriber's responsibility to report/handle any exceptions\n                // that occur during their callback\n            }\n        }\n\n        return Task.CompletedTask;\n    }\n\n    public Task<IEnumerable<WebHookReceived>> GetAll()\n    {\n        return Task.FromResult(_data.AsEnumerable());\n    }\n\n    public IDisposable Subscribe(Func<Task> callback)\n    {\n        var subscription = new OnChangeSubscription(callback, this);\n        _onChangeSubscriptions.TryAdd(subscription, null);\n        return subscription;\n    }\n\n    private class OnChangeSubscription(Func<Task> callback, HooksRepository owner) : IDisposable\n    {\n        public Task NotifyAsync() => callback();\n\n        public void Dispose() => owner._onChangeSubscriptions.Remove(this, out _);\n    }\n}\n"
  },
  {
    "path": "src/WebhookClient/Services/WebHookReceived.cs",
    "content": "﻿namespace eShop.WebhookClient.Services;\n\npublic class WebHookReceived\n{\n    public DateTime When { get; set; }\n\n    public string? Data { get; set; }\n\n    public string? Token { get; set; }\n}\n"
  },
  {
    "path": "src/WebhookClient/Services/WebHooksClient.cs",
    "content": "﻿namespace eShop.WebhookClient.Services;\n\npublic class WebhooksClient(HttpClient client)\n{\n    public Task<HttpResponseMessage> AddWebHookAsync(WebhookSubscriptionRequest payload)\n    {\n        return client.PostAsJsonAsync(\"/api/webhooks\", payload);\n    }\n\n    public async Task<IEnumerable<WebhookResponse>> LoadWebhooks()\n    {\n        return await client.GetFromJsonAsync<IEnumerable<WebhookResponse>>(\"/api/webhooks\") ?? [];\n    }\n}\n"
  },
  {
    "path": "src/WebhookClient/Services/WebhookClientOptions.cs",
    "content": "﻿namespace eShop.WebhookClient.Services;\n\npublic class WebhookClientOptions\n{\n    public string? Token { get; set; }\n    public string? SelfUrl { get; set; }\n    public bool ValidateToken { get; set; }\n}\n"
  },
  {
    "path": "src/WebhookClient/Services/WebhookData.cs",
    "content": "﻿using System.Text.Json;\n\nnamespace eShop.WebhookClient.Services;\n\npublic class WebhookData\n{\n    public DateTime When { get; set; }\n\n    public string? Payload { get; set; }\n\n    public string? Type { get; set; }\n}\n"
  },
  {
    "path": "src/WebhookClient/Services/WebhookResponse.cs",
    "content": "﻿namespace eShop.WebhookClient.Services;\n\npublic class WebhookResponse\n{\n    public DateTime Date { get; set; }\n    public string? DestUrl { get; set; }\n    public string? Token { get; set; }\n}\n"
  },
  {
    "path": "src/WebhookClient/Services/WebhookSubscriptionRequest.cs",
    "content": "﻿namespace eShop.WebhookClient.Services;\n\npublic class WebhookSubscriptionRequest\n{\n    public string? Url { get; set; }\n    public string? Token { get; set; }\n    public string? Event { get; set; }\n    public string? GrantUrl { get; set; }\n}\n"
  },
  {
    "path": "src/WebhookClient/Services/WebhookType.cs",
    "content": "﻿namespace eShop.WebhookClient.Services;\n\npublic enum WebhookType\n{\n    CatalogItemPriceChange = 1,\n    OrderShipped = 2,\n    OrderPaid = 3\n}\n"
  },
  {
    "path": "src/WebhookClient/WebhookClient.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <Nullable>enable</Nullable>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <RootNamespace>eShop.WebhookClient</RootNamespace>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\eShop.ServiceDefaults\\eShop.ServiceDefaults.csproj\" />\n    <PackageReference Include=\"Asp.Versioning.Http.Client\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Authentication.OpenIdConnect\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Components.QuickGrid\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/WebhookClient/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/WebhookClient/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  },\n  \"AllowedHosts\": \"*\",\n  \"WebhookClientOptions\": {\n    \"Token\": \"6168DB8D-DC58-4094-AF24-483278923590\"\n  }\n}\n"
  },
  {
    "path": "src/WebhookClient/wwwroot/app.css",
    "content": "h1, h2, h3, h4, h5, h6 {\n    font-weight: 400;\n}\n\nh2 {\n    font-size: 1.3rem;\n}\n\n    h2:first-child, h3:first-child {\n        margin-top: 0;\n    }\n\np {\n    line-height: 1.35rem;\n}\n\nh1:focus {\n    outline: none;\n}\n\n.valid.modified:not([type=checkbox]) {\n    outline: 1px solid #26b050;\n}\n\n.invalid {\n    outline: 1px solid #e50000;\n}\n\n.validation-message {\n    color: #e50000;\n}\n\n.blazor-error-boundary {\n    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;\n    padding: 1rem 1rem 1rem 3.7rem;\n    color: white;\n}\n\n    .blazor-error-boundary::after {\n        content: \"An error has occurred.\"\n    }\n\n.darker-border-checkbox.form-check-input {\n    border-color: #929292;\n}\n\nbutton {\n    font-size: 1rem;\n}\n\nbutton.action, a.action {\n    background-color: #1a7dad;\n    color: white;\n    padding: 0.4rem 1.25rem;\n    border: none;\n    border-radius: 0.5rem;\n    box-shadow: 0px 3px 4px #00000070;\n    transition: all 0.1s cubic-bezier(.16,1.13,.57,.94);\n    display: inline-block;\n    text-decoration: none;\n    cursor: pointer;\n    font-size: 1.05em;\n}\n\n    button.action:hover, a.action:hover {\n        background-color: #23a0dc;\n    }\n\n    button.action:active, a.action:active {\n        background-color: #00496c;\n        transform: scale(0.98) translateY(1px);\n    }\n\n.grid-placeholder {\n    height: 5rem;\n    max-width: 50rem;\n    background-color: #e1e1e1;\n    margin-bottom: 1rem;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: #555;\n    border-radius: 0.5rem;\n}\n\ninput[type=text] {\n    font-size: 1.05rem;\n    padding: 0.2rem 0.5rem;\n    border-radius: 0.4rem;\n    margin-left: 0.5rem;\n    border: 2px solid #038ac7;\n}\n\n.error-message {\n    color: red;\n    background: #ffefef;\n    padding: 0.4rem 1rem;\n    border: 1px solid #ffb4b4;\n    border-radius: 0.4rem;\n}\n\n.quickgrid {\n    margin-bottom: 1rem;\n    border-spacing: 0;\n}\n\n    .quickgrid th {\n        border-bottom: 1px solid gray;\n    }\n\n    .quickgrid tr:not(:last-child) td {\n        border-bottom: 1px solid #e1e1e1;\n    }\n\n    .quickgrid td, .quickgrid th {\n        padding-top: 0.4rem !important;\n        padding-bottom: 0.4rem !important;\n    }\n"
  },
  {
    "path": "src/Webhooks.API/Apis/WebHooksApi.cs",
    "content": "﻿using System.Security.Claims;\nusing Microsoft.AspNetCore.Http.HttpResults;\nusing Webhooks.API.Extensions;\n\nnamespace Webhooks.API;\n\npublic static class WebHooksApi\n{\n    public static RouteGroupBuilder MapWebHooksApiV1(this IEndpointRouteBuilder app)\n    {\n        var api = app.MapGroup(\"/api/webhooks\").HasApiVersion(1.0);\n\n        api.MapGet(\"/\", async (WebhooksContext context, ClaimsPrincipal user) =>\n        {\n            var userId = user.GetUserId();\n            var data = await context.Subscriptions.Where(s => s.UserId == userId).ToListAsync();\n            return TypedResults.Ok(data);\n        });\n\n        api.MapGet(\"/{id:int}\", async Task<Results<Ok<WebhookSubscription>, NotFound<string>>> (\n            WebhooksContext context,\n            ClaimsPrincipal user,\n            int id) =>\n        {\n            var userId = user.GetUserId();\n            var subscription = await context.Subscriptions\n                .SingleOrDefaultAsync(s => s.Id == id && s.UserId == userId);\n            if (subscription != null)\n            {\n                return TypedResults.Ok(subscription);\n            }\n            return TypedResults.NotFound($\"Subscriptions {id} not found\");\n        });\n\n        api.MapPost(\"/\", async Task<Results<Created, BadRequest<string>>> (\n            WebhookSubscriptionRequest request,\n            IGrantUrlTesterService grantUrlTester,\n            WebhooksContext context,\n            ClaimsPrincipal user) =>\n        {\n            var grantOk = await grantUrlTester.TestGrantUrl(request.Url, request.GrantUrl, request.Token ?? string.Empty);\n\n            if (grantOk)\n            {\n                var subscription = new WebhookSubscription()\n                {\n                    Date = DateTime.UtcNow,\n                    DestUrl = request.Url,\n                    Token = request.Token,\n                    Type = Enum.Parse<WebhookType>(request.Event, ignoreCase: true),\n                    UserId = user.GetUserId()\n                };\n\n                context.Add(subscription);\n                await context.SaveChangesAsync();\n\n                return TypedResults.Created($\"/api/webhooks/{subscription.Id}\");\n            }\n            else\n            {\n                return TypedResults.BadRequest($\"Invalid grant URL: {request.GrantUrl}\");\n            }\n        })\n        .ValidateWebhookSubscriptionRequest();\n\n        api.MapDelete(\"/{id:int}\", async Task<Results<Accepted, NotFound<string>>> (\n            WebhooksContext context,\n            ClaimsPrincipal user,\n            int id) =>\n        {\n            var userId = user.GetUserId();\n            var subscription = await context.Subscriptions.SingleOrDefaultAsync(s => s.Id == id && s.UserId == userId);\n\n            if (subscription != null)\n            {\n                context.Remove(subscription);\n                await context.SaveChangesAsync();\n                return TypedResults.Accepted($\"/api/webhooks/{subscription.Id}\");\n            }\n\n            return TypedResults.NotFound($\"Subscriptions {id} not found\");\n        });\n\n        return api;\n    }\n}\n"
  },
  {
    "path": "src/Webhooks.API/Exceptions/WebhooksDomainException.cs",
    "content": "﻿namespace Webhooks.API.Exceptions;\n\npublic class WebhooksDomainException : Exception\n{\n}\n"
  },
  {
    "path": "src/Webhooks.API/Extensions/Extensions.cs",
    "content": "﻿internal static class Extensions\n{\n    public static void AddApplicationServices(this IHostApplicationBuilder builder)\n    {\n        builder.AddDefaultAuthentication();\n\n        builder.AddRabbitMqEventBus(\"eventbus\")\n               .AddEventBusSubscriptions();\n\n        builder.AddNpgsqlDbContext<WebhooksContext>(\"webhooksdb\");\n\n        builder.Services.AddMigration<WebhooksContext>();\n\n        builder.Services.AddTransient<IGrantUrlTesterService, GrantUrlTesterService>();\n        builder.Services.AddTransient<IWebhooksRetriever, WebhooksRetriever>();\n        builder.Services.AddTransient<IWebhooksSender, WebhooksSender>();\n    }\n\n    private static void AddEventBusSubscriptions(this IEventBusBuilder eventBus)\n    {\n        eventBus.AddSubscription<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler>();\n        eventBus.AddSubscription<OrderStatusChangedToShippedIntegrationEvent, OrderStatusChangedToShippedIntegrationEventHandler>();\n        eventBus.AddSubscription<OrderStatusChangedToPaidIntegrationEvent, OrderStatusChangedToPaidIntegrationEventHandler>();\n    }\n}\n"
  },
  {
    "path": "src/Webhooks.API/Extensions/RouteHandlerBuilderExtensions.cs",
    "content": "﻿namespace Webhooks.API.Extensions;\n\npublic static class RouteHandlerBuilderExtensions\n{\n    public static RouteHandlerBuilder ValidateWebhookSubscriptionRequest(this RouteHandlerBuilder routeHandlerBuilder)\n    {\n        return routeHandlerBuilder.AddEndpointFilter(async (context, next) =>\n        {\n            var webhookSubscriptionRequest = context.Arguments.OfType<WebhookSubscriptionRequest>().SingleOrDefault();\n\n            if (webhookSubscriptionRequest == null)\n            {\n                return TypedResults.BadRequest(\"No WebhookSubscriptionRequest found.\");\n            }\n\n            var validationResults = webhookSubscriptionRequest.Validate(new ValidationContext(webhookSubscriptionRequest));\n\n            if (validationResults.Any())\n            {\n                return TypedResults.ValidationProblem(validationResults.ToErrors());\n            }\n\n            return await next(context);\n        });\n    }\n\n    private static Dictionary<string, string[]> ToErrors(this IEnumerable<ValidationResult> validationResults)\n    {\n        Dictionary<string, string[]> errors = [];\n\n        foreach (var validationResult in validationResults)\n        {\n            var propertyNames = validationResult.MemberNames.Any() ? validationResult.MemberNames : [string.Empty];\n\n            foreach (string propertyName in propertyNames)\n            {\n                if (errors.TryGetValue(propertyName, out var value))\n                {\n                    errors[propertyName] = [..value, validationResult.ErrorMessage];\n                }\n                else\n                {\n                    errors.Add(propertyName, [validationResult.ErrorMessage]);  \n                }\n            }\n        }\n        return errors;\n    }\n}\n"
  },
  {
    "path": "src/Webhooks.API/GlobalUsings.cs",
    "content": "﻿global using System.ComponentModel.DataAnnotations;\nglobal using System.Text;\nglobal using System.Text.Json;\nglobal using Microsoft.EntityFrameworkCore;\nglobal using eShop.EventBus.Abstractions;\nglobal using eShop.EventBus.Events;\nglobal using eShop.ServiceDefaults;\nglobal using Webhooks.API.Infrastructure;\nglobal using Webhooks.API.IntegrationEvents;\nglobal using Webhooks.API.Model;\nglobal using Webhooks.API.Services;\nglobal using Webhooks.API;\n"
  },
  {
    "path": "src/Webhooks.API/Infrastructure/WebhooksContext.cs",
    "content": "﻿namespace Webhooks.API.Infrastructure;\n\n/// <remarks>\n/// Add migrations using the following command inside the 'Webhooks.API' project directory:\n///\n/// dotnet ef migrations add [migration-name]\n/// </remarks>\npublic class WebhooksContext(DbContextOptions<WebhooksContext> options) : DbContext(options)\n{\n    public DbSet<WebhookSubscription> Subscriptions { get; set; }\n\n    protected override void OnModelCreating(ModelBuilder modelBuilder)\n    {\n        modelBuilder.Entity<WebhookSubscription>(eb =>\n        {\n            eb.HasIndex(s => s.UserId);\n            eb.HasIndex(s => s.Type);\n        });\n    }\n}\n"
  },
  {
    "path": "src/Webhooks.API/IntegrationEvents/OrderStatusChangedToPaidIntegrationEvent.cs",
    "content": "﻿namespace Webhooks.API.IntegrationEvents;\n\npublic record OrderStatusChangedToPaidIntegrationEvent(int OrderId, IEnumerable<OrderStockItem> OrderStockItems) : IntegrationEvent;\n"
  },
  {
    "path": "src/Webhooks.API/IntegrationEvents/OrderStatusChangedToPaidIntegrationEventHandler.cs",
    "content": "﻿namespace Webhooks.API.IntegrationEvents;\n\npublic class OrderStatusChangedToPaidIntegrationEventHandler(\n    IWebhooksRetriever retriever, \n    IWebhooksSender sender, \n    ILogger<OrderStatusChangedToShippedIntegrationEventHandler> logger) : IIntegrationEventHandler<OrderStatusChangedToPaidIntegrationEvent>\n{\n    public async Task Handle(OrderStatusChangedToPaidIntegrationEvent @event)\n    {\n        var subscriptions = await retriever.GetSubscriptionsOfType(WebhookType.OrderPaid);\n\n        logger.LogInformation(\"Received OrderStatusChangedToShippedIntegrationEvent and got {SubscriptionsCount} subscriptions to process\", subscriptions.Count());\n\n        var whook = new WebhookData(WebhookType.OrderPaid, @event);\n\n        await sender.SendAll(subscriptions, whook);\n    }\n}\n"
  },
  {
    "path": "src/Webhooks.API/IntegrationEvents/OrderStatusChangedToShippedIntegrationEvent.cs",
    "content": "﻿namespace Webhooks.API.IntegrationEvents;\n\npublic record OrderStatusChangedToShippedIntegrationEvent(int OrderId, string OrderStatus, string BuyerName) : IntegrationEvent;\n"
  },
  {
    "path": "src/Webhooks.API/IntegrationEvents/OrderStatusChangedToShippedIntegrationEventHandler.cs",
    "content": "﻿namespace Webhooks.API.IntegrationEvents;\n\npublic class OrderStatusChangedToShippedIntegrationEventHandler(\n    IWebhooksRetriever retriever,\n    IWebhooksSender sender,\n    ILogger<OrderStatusChangedToShippedIntegrationEventHandler> logger) : IIntegrationEventHandler<OrderStatusChangedToShippedIntegrationEvent>\n{\n    public async Task Handle(OrderStatusChangedToShippedIntegrationEvent @event)\n    {\n        var subscriptions = await retriever.GetSubscriptionsOfType(WebhookType.OrderShipped);\n\n        logger.LogInformation(\"Received OrderStatusChangedToShippedIntegrationEvent and got {SubscriptionCount} subscriptions to process\", subscriptions.Count());\n\n        var whook = new WebhookData(WebhookType.OrderShipped, @event);\n\n        await sender.SendAll(subscriptions, whook);\n    }\n}\n"
  },
  {
    "path": "src/Webhooks.API/IntegrationEvents/OrderStockItem.cs",
    "content": "﻿namespace Webhooks.API.IntegrationEvents;\n\npublic record OrderStockItem(int ProductId, int Units);\n"
  },
  {
    "path": "src/Webhooks.API/IntegrationEvents/ProductPriceChangedIntegrationEvent.cs",
    "content": "﻿namespace Webhooks.API.IntegrationEvents;\n\npublic record ProductPriceChangedIntegrationEvent(int ProductId, decimal NewPrice, decimal OldPrice) : IntegrationEvent;\n"
  },
  {
    "path": "src/Webhooks.API/IntegrationEvents/ProductPriceChangedIntegrationEventHandler.cs",
    "content": "﻿namespace Webhooks.API.IntegrationEvents;\n\npublic class ProductPriceChangedIntegrationEventHandler : IIntegrationEventHandler<ProductPriceChangedIntegrationEvent>\n{\n    public Task Handle(ProductPriceChangedIntegrationEvent @event)\n    {\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/Webhooks.API/Migrations/20230925222606_Initial.Designer.cs",
    "content": "﻿// <auto-generated />\nusing System;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Infrastructure;\nusing Microsoft.EntityFrameworkCore.Migrations;\nusing Microsoft.EntityFrameworkCore.Storage.ValueConversion;\nusing Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;\nusing Webhooks.API.Infrastructure;\n\n#nullable disable\n\nnamespace Webhooks.API.Migrations\n{\n    [DbContext(typeof(WebhooksContext))]\n    [Migration(\"20230925222606_Initial\")]\n    partial class Initial\n    {\n        /// <inheritdoc />\n        protected override void BuildTargetModel(ModelBuilder modelBuilder)\n        {\n#pragma warning disable 612, 618\n            modelBuilder\n                .HasAnnotation(\"ProductVersion\", \"8.0.0-rc.1.23419.6\")\n                .HasAnnotation(\"Relational:MaxIdentifierLength\", 63);\n\n            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);\n\n            modelBuilder.Entity(\"Webhooks.API.Model.WebhookSubscription\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>(\"Id\"));\n\n                    b.Property<DateTime>(\"Date\")\n                        .HasColumnType(\"timestamp with time zone\");\n\n                    b.Property<string>(\"DestUrl\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"Token\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<int>(\"Type\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"UserId\")\n                        .HasColumnType(\"text\");\n\n                    b.HasKey(\"Id\");\n\n                    b.ToTable(\"Subscriptions\");\n                });\n#pragma warning restore 612, 618\n        }\n    }\n}\n"
  },
  {
    "path": "src/Webhooks.API/Migrations/20230925222606_Initial.cs",
    "content": "﻿using System;\nusing Microsoft.EntityFrameworkCore.Migrations;\nusing Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;\n\n#nullable disable\n\nnamespace Webhooks.API.Migrations\n{\n    /// <inheritdoc />\n    public partial class Initial : Migration\n    {\n        /// <inheritdoc />\n        protected override void Up(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.CreateTable(\n                name: \"Subscriptions\",\n                columns: table => new\n                {\n                    Id = table.Column<int>(type: \"integer\", nullable: false)\n                        .Annotation(\"Npgsql:ValueGenerationStrategy\", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),\n                    Type = table.Column<int>(type: \"integer\", nullable: false),\n                    Date = table.Column<DateTime>(type: \"timestamp with time zone\", nullable: false),\n                    DestUrl = table.Column<string>(type: \"text\", nullable: true),\n                    Token = table.Column<string>(type: \"text\", nullable: true),\n                    UserId = table.Column<string>(type: \"text\", nullable: true)\n                },\n                constraints: table =>\n                {\n                    table.PrimaryKey(\"PK_Subscriptions\", x => x.Id);\n                });\n        }\n\n        /// <inheritdoc />\n        protected override void Down(MigrationBuilder migrationBuilder)\n        {\n            migrationBuilder.DropTable(\n                name: \"Subscriptions\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Webhooks.API/Migrations/WebhooksContextModelSnapshot.cs",
    "content": "﻿// <auto-generated />\nusing System;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Infrastructure;\nusing Microsoft.EntityFrameworkCore.Storage.ValueConversion;\nusing Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;\nusing Webhooks.API.Infrastructure;\n\n#nullable disable\n\nnamespace Webhooks.API.Migrations\n{\n    [DbContext(typeof(WebhooksContext))]\n    partial class WebhooksContextModelSnapshot : ModelSnapshot\n    {\n        protected override void BuildModel(ModelBuilder modelBuilder)\n        {\n#pragma warning disable 612, 618\n            modelBuilder\n                .HasAnnotation(\"ProductVersion\", \"8.0.0-rc.2.23480.1\")\n                .HasAnnotation(\"Relational:MaxIdentifierLength\", 63);\n\n            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);\n\n            modelBuilder.Entity(\"Webhooks.API.Model.WebhookSubscription\", b =>\n                {\n                    b.Property<int>(\"Id\")\n                        .ValueGeneratedOnAdd()\n                        .HasColumnType(\"integer\");\n\n                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>(\"Id\"));\n\n                    b.Property<DateTime>(\"Date\")\n                        .HasColumnType(\"timestamp with time zone\");\n\n                    b.Property<string>(\"DestUrl\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.Property<string>(\"Token\")\n                        .HasColumnType(\"text\");\n\n                    b.Property<int>(\"Type\")\n                        .HasColumnType(\"integer\");\n\n                    b.Property<string>(\"UserId\")\n                        .IsRequired()\n                        .HasColumnType(\"text\");\n\n                    b.HasKey(\"Id\");\n\n                    b.HasIndex(\"Type\");\n\n                    b.HasIndex(\"UserId\");\n\n                    b.ToTable(\"Subscriptions\");\n                });\n#pragma warning restore 612, 618\n        }\n    }\n}\n"
  },
  {
    "path": "src/Webhooks.API/Model/WebhookData.cs",
    "content": "﻿namespace Webhooks.API.Model;\n\npublic class WebhookData\n{\n    public DateTime When { get; }\n\n    public string Payload { get; }\n\n    public string Type { get; }\n\n    public WebhookData(WebhookType hookType, object data)\n    {\n        When = DateTime.UtcNow;\n        Type = hookType.ToString();\n        Payload = JsonSerializer.Serialize(data);\n    }\n}\n"
  },
  {
    "path": "src/Webhooks.API/Model/WebhookSubscription.cs",
    "content": "﻿namespace Webhooks.API.Model;\n\npublic class WebhookSubscription\n{\n    public int Id { get; set; }\n\n    public WebhookType Type { get; set; }\n    public DateTime Date { get; set; }\n    [Required]\n    public string DestUrl { get; set; }\n    public string Token { get; set; }\n    [Required]\n    public string UserId { get; set; }\n}\n"
  },
  {
    "path": "src/Webhooks.API/Model/WebhookSubscriptionRequest.cs",
    "content": "﻿namespace Webhooks.API.Model;\n\npublic class WebhookSubscriptionRequest : IValidatableObject\n{\n    public string Url { get; set; }\n    public string Token { get; set; }\n    public string Event { get; set; }\n    public string GrantUrl { get; set; }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (!Uri.IsWellFormedUriString(GrantUrl, UriKind.Absolute))\n        {\n            yield return new ValidationResult(\"GrantUrl is not valid\", new[] { nameof(GrantUrl) });\n        }\n\n        if (!Uri.IsWellFormedUriString(Url, UriKind.Absolute))\n        {\n            yield return new ValidationResult(\"Url is not valid\", new[] { nameof(Url) });\n        }\n\n        var isOk = Enum.TryParse(Event, ignoreCase: true, result: out WebhookType whtype);\n        if (!isOk)\n        {\n            yield return new ValidationResult($\"{Event} is invalid event name\", new[] { nameof(Event) });\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/Webhooks.API/Model/WebhookType.cs",
    "content": "﻿namespace Webhooks.API.Model;\n\npublic enum WebhookType\n{\n    CatalogItemPriceChange = 1,\n    OrderShipped = 2,\n    OrderPaid = 3\n}\n"
  },
  {
    "path": "src/Webhooks.API/Program.cs",
    "content": "﻿var builder = WebApplication.CreateBuilder(args);\n\nbuilder.AddServiceDefaults();\nbuilder.AddApplicationServices();\n\nvar withApiVersioning = builder.Services.AddApiVersioning();\n\nbuilder.AddDefaultOpenApi(withApiVersioning);\n\nvar app = builder.Build();\n\napp.MapDefaultEndpoints();\n\nvar webHooks = app.NewVersionedApi(\"Web Hooks\");\n\nwebHooks.MapWebHooksApiV1()\n        .RequireAuthorization();\n\napp.UseDefaultOpenApi();\napp.Run();\n"
  },
  {
    "path": "src/Webhooks.API/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"http\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"applicationUrl\": \"http://localhost:5227\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "src/Webhooks.API/Services/GrantUrlTesterService.cs",
    "content": "﻿namespace Webhooks.API.Services;\n\nclass GrantUrlTesterService(IHttpClientFactory factory, ILogger<IGrantUrlTesterService> logger) : IGrantUrlTesterService\n{\n    public async Task<bool> TestGrantUrl(string urlHook, string url, string token)\n    {\n        if (!CheckSameOrigin(urlHook, url))\n        {\n            logger.LogWarning(\"Url of the hook ({UrlHook} and the grant url ({Url} do not belong to same origin)\", urlHook, url);\n            return false;\n        }\n\n        var client = factory.CreateClient();\n        var msg = new HttpRequestMessage(HttpMethod.Options, url);\n        msg.Headers.Add(\"X-eshop-whtoken\", token);\n\n        logger.LogInformation(\"Sending the OPTIONS message to {Url} with token \\\"{Token}\\\"\", url, token ?? string.Empty);\n\n        try\n        {\n            var response = await client.SendAsync(msg);\n            var tokenReceived = response.Headers.TryGetValues(\"X-eshop-whtoken\", out var tokenValues) ? tokenValues.FirstOrDefault() : null;\n            var tokenExpected = string.IsNullOrWhiteSpace(token) ? null : token;\n\n            logger.LogInformation(\"Response code is {StatusCode} for url {Url} and token in header was {TokenReceived} (expected token was {TokenExpected})\", response.StatusCode, url, tokenReceived, tokenExpected);\n\n            return response.IsSuccessStatusCode && tokenReceived == tokenExpected;\n        }\n        catch (Exception ex)\n        {\n            logger.LogWarning(\"Exception {TypeName} when sending OPTIONS request. Url can't be granted.\", ex.GetType().Name);\n\n            return false;\n        }\n    }\n\n    private static bool CheckSameOrigin(string urlHook, string url)\n    {\n        var firstUrl = new Uri(urlHook, UriKind.Absolute);\n        var secondUrl = new Uri(url, UriKind.Absolute);\n\n        return firstUrl.Scheme == secondUrl.Scheme &&\n            firstUrl.Port == secondUrl.Port &&\n            firstUrl.Host == secondUrl.Host;\n    }\n}\n"
  },
  {
    "path": "src/Webhooks.API/Services/IGrantUrlTesterService.cs",
    "content": "﻿namespace Webhooks.API.Services;\n\npublic interface IGrantUrlTesterService\n{\n    Task<bool> TestGrantUrl(string urlHook, string url, string token);\n}\n"
  },
  {
    "path": "src/Webhooks.API/Services/IWebhooksRetriever.cs",
    "content": "﻿namespace Webhooks.API.Services;\n\npublic interface IWebhooksRetriever\n{\n\n    Task<IEnumerable<WebhookSubscription>> GetSubscriptionsOfType(WebhookType type);\n}\n"
  },
  {
    "path": "src/Webhooks.API/Services/IWebhooksSender.cs",
    "content": "﻿namespace Webhooks.API.Services;\n\npublic interface IWebhooksSender\n{\n    Task SendAll(IEnumerable<WebhookSubscription> receivers, WebhookData data);\n}\n"
  },
  {
    "path": "src/Webhooks.API/Services/WebhooksRetriever.cs",
    "content": "﻿namespace Webhooks.API.Services;\n\npublic class WebhooksRetriever(WebhooksContext db) : IWebhooksRetriever\n{\n    public async Task<IEnumerable<WebhookSubscription>> GetSubscriptionsOfType(WebhookType type)\n    {\n        return await db.Subscriptions.Where(s => s.Type == type).ToListAsync();\n    }\n}\n"
  },
  {
    "path": "src/Webhooks.API/Services/WebhooksSender.cs",
    "content": "﻿namespace Webhooks.API.Services;\n\npublic class WebhooksSender(IHttpClientFactory httpClientFactory, ILogger<WebhooksSender> logger) : IWebhooksSender\n{\n    public async Task SendAll(IEnumerable<WebhookSubscription> receivers, WebhookData data)\n    {\n        var client = httpClientFactory.CreateClient();\n        var json = JsonSerializer.Serialize(data);\n        var tasks = receivers.Select(r => OnSendData(r, json, client));\n        await Task.WhenAll(tasks);\n    }\n\n    private Task OnSendData(WebhookSubscription subs, string jsonData, HttpClient client)\n    {\n        var request = new HttpRequestMessage()\n        {\n            RequestUri = new Uri(subs.DestUrl, UriKind.Absolute),\n            Method = HttpMethod.Post,\n            Content = new StringContent(jsonData, Encoding.UTF8, \"application/json\")\n        };\n\n        if (!string.IsNullOrWhiteSpace(subs.Token))\n        {\n            request.Headers.Add(\"X-eshop-whtoken\", subs.Token);\n        }\n\n        if (logger.IsEnabled(LogLevel.Debug))\n        {\n            logger.LogDebug(\"Sending hook to {DestUrl} of type {Type}\", subs.DestUrl, subs.Type);\n        }\n\n        return client.SendAsync(request);\n    }\n\n}\n"
  },
  {
    "path": "src/Webhooks.API/Webhooks.API.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <Compile Include=\"..\\Shared\\ActivityExtensions.cs\" Link=\"Extensions\\ActivityExtensions.cs\" />\n    <Compile Include=\"..\\Shared\\MigrateDbContextExtensions.cs\" Link=\"Extensions\\MigrateDbContextExtensions.cs\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Asp.Versioning.Http\" />\n    <PackageReference Include=\"Aspire.Npgsql.EntityFrameworkCore.PostgreSQL\" />\n    <PackageReference Include=\"Microsoft.EntityFrameworkCore.Tools\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\EventBusRabbitMQ\\EventBusRabbitMQ.csproj\" />\n    <ProjectReference Include=\"..\\IntegrationEventLogEF\\IntegrationEventLogEF.csproj\" />\n    <ProjectReference Include=\"..\\eShop.ServiceDefaults\\eShop.ServiceDefaults.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Webhooks.API/appsettings.Development.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"System\": \"Information\",\n      \"Microsoft\": \"Information\"\n    }\n  },\n  \"ConnectionStrings\": {\n    \"WebHooksDB\": \"Host=localhost;Database=WebHooksDB;Username=postgres;Password=yourWeak(!)Password\"\n  }\n}\n"
  },
  {
    "path": "src/Webhooks.API/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    }\n  },\n  \"AllowedHosts\": \"*\",\n  \"OpenApi\": {\n    \"Endpoint\": {\n      \"Name\": \"Webhooks.API V1\"\n    },\n    \"Document\": {\n      \"Description\": \"The Webhooks Microservice HTTP API. This is a simple webhooks CRUD registration entrypoint\",\n      \"Title\": \"eShop - Webhooks HTTP API\",\n      \"Version\": \"v1\"\n    },\n    \"Auth\": {\n      \"ClientId\": \"webhooksswaggerui\",\n      \"AppName\": \"WebHooks Service Swagger UI\"\n    }\n  },\n  \"ConnectionStrings\": {\n    \"EventBus\": \"amqp://localhost\"\n  },\n  \"EventBus\": {\n    \"SubscriptionClientName\": \"Webhooks\"\n  },\n  \"Identity\": {\n    \"Url\": \"http://localhost:5223\",\n    \"Audience\": \"webhooks\",\n    \"Scopes\": {\n      \"webhooks\": \"Webhooks API\"\n    }\n  },\n  \"UseCustomizationData\": false\n}\n"
  },
  {
    "path": "src/eShop.AppHost/Extensions.cs",
    "content": "﻿using Aspire.Hosting.Eventing;\nusing Aspire.Hosting.Lifecycle;\nusing Aspire.Hosting.Yarp;\nusing Aspire.Hosting.Yarp.Transforms;\nusing Yarp.ReverseProxy.Configuration;\n\nnamespace eShop.AppHost;\n\ninternal enum OpenAITarget\n{\n    OpenAI,\n    AzureOpenAI,\n    AzureOpenAIExisting,\n    AzureOpenAIExistingWithKey\n}\n\ninternal static class Extensions\n{\n    /// <summary>\n    /// Adds a hook to set the ASPNETCORE_FORWARDEDHEADERS_ENABLED environment variable to true for all projects in the application.\n    /// </summary>\n    public static IDistributedApplicationBuilder AddForwardedHeaders(this IDistributedApplicationBuilder builder)\n    {\n        builder.Services.TryAddEventingSubscriber<AddForwardHeadersSubscriber>();\n        return builder;\n    }\n\n    private class AddForwardHeadersSubscriber : IDistributedApplicationEventingSubscriber\n    {\n        public Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken)\n        {\n            eventing.Subscribe<BeforeStartEvent>((@event, ct) =>\n            {\n                foreach (var p in @event.Model.GetProjectResources())\n                {\n                    p.Annotations.Add(new EnvironmentCallbackAnnotation(context =>\n                    {\n                        context.EnvironmentVariables[\"ASPNETCORE_FORWARDEDHEADERS_ENABLED\"] = \"true\";\n                    }));\n                }\n\n                return Task.CompletedTask;\n            });\n\n            return Task.CompletedTask;\n        }\n    }\n\n    /// <summary>\n    /// Configures eShop projects to use OpenAI for text embedding and chat.\n    /// </summary>\n    public static IDistributedApplicationBuilder AddOpenAI(this IDistributedApplicationBuilder builder,\n        IResourceBuilder<ProjectResource> catalogApi,\n        IResourceBuilder<ProjectResource> webApp,\n        OpenAITarget openAITarget)\n    {\n        const string openAIName = \"openai\";\n\n        const string textEmbeddingName = \"textEmbeddingModel\";\n        const string textEmbeddingModelName = \"text-embedding-3-small\";\n\n        const string chatName = \"chatModel\";\n        const string chatModelName = \"gpt-4.1-mini\";\n\n        if (openAITarget != OpenAITarget.AzureOpenAI)\n        {\n#pragma warning disable ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.\n            IResourceBuilder<ParameterResource>? endpoint = null;\n            if (openAITarget != OpenAITarget.OpenAI)\n            {\n                endpoint = builder.AddParameter(\"OpenAIEndpointParameter\")\n                    .WithDescription(\"The Azure OpenAI endpoint to use, e.g. https://<name>.openai.azure.com/\")\n                    .WithCustomInput(p => new()\n                    {\n                        Name = \"OpenAIEndpointParameter\",\n                        Label = \"Azure OpenAI Endpoint\",\n                        InputType = InputType.Text,\n                        Value = \"https://<name>.openai.azure.com/\",\n                    });\n            }\n\n            IResourceBuilder<ParameterResource>? key = null;\n            if (openAITarget is OpenAITarget.OpenAI or OpenAITarget.AzureOpenAIExistingWithKey)\n            {\n                key = builder.AddParameter(\"OpenAIKeyParameter\", secret: true)\n                    .WithDescription(\"The OpenAI API key to use.\")\n                    .WithCustomInput(p => new()\n                    {\n                        Name = \"OpenAIKeyParameter\",\n                        Label = \"API Key\",\n                        InputType = InputType.SecretText\n                    });\n            }\n\n            var chatModel = builder.AddParameter(\"ChatModelParameter\")\n                .WithDescription(\"The chat model to use.\")\n                .WithCustomInput(p => new()\n                {\n                    Name = \"ChatModelParameter\",\n                    Label = \"Chat Model\",\n                    InputType = InputType.Text,\n                    Value = chatModelName,\n                });\n\n            var embeddingModel = builder.AddParameter(\"EmbeddingModelParameter\")\n                .WithDescription(\"The embedding model to use.\")\n                .WithCustomInput(p => new()\n                {\n                    Name = \"EmbeddingModelParameter\",\n                    Label = \"Text Embedding Model\",\n                    InputType = InputType.Text,\n                    Value = textEmbeddingModelName,\n                });\n#pragma warning restore ASPIREINTERACTION001\n\n            var openAIConnectionBuilder = new ReferenceExpressionBuilder();\n            if (endpoint is not null)\n            {\n                openAIConnectionBuilder.Append($\"Endpoint={endpoint}\");\n            }\n            if (key is not null)\n            {\n                openAIConnectionBuilder.Append($\";Key={key}\");\n            }\n            var openAIConnectionString = openAIConnectionBuilder.Build();\n\n            catalogApi.WithReference(builder.AddConnectionString(textEmbeddingName, cs =>\n            {\n                cs.Append($\"{openAIConnectionString};Deployment={embeddingModel}\");\n            }));\n            webApp.WithReference(builder.AddConnectionString(chatName, cs =>\n            {\n                cs.Append($\"{openAIConnectionString};Deployment={chatModel}\");\n            }));\n        }\n        else\n        {\n            var openAI = builder.AddAzureOpenAI(openAIName);\n\n            var chat = openAI.AddDeployment(chatName, chatModelName, \"2025-04-14\")\n                .WithProperties(d =>\n                {\n                    d.DeploymentName = chatModelName;\n                    d.SkuName = \"GlobalStandard\";\n                    d.SkuCapacity = 50;\n                });\n            var textEmbedding = openAI.AddDeployment(textEmbeddingName, textEmbeddingModelName, \"1\")\n                .WithProperties(d =>\n                {\n                    d.DeploymentName = textEmbeddingModelName;\n                    d.SkuCapacity = 20; // 20k tokens per minute are needed to seed the initial embeddings\n                });\n\n            catalogApi.WithReference(textEmbedding);\n            webApp.WithReference(chat);\n        }\n\n        return builder;\n    }\n\n    /// <summary>\n    /// Configures eShop projects to use Ollama for text embedding and chat.\n    /// </summary>\n    public static IDistributedApplicationBuilder AddOllama(this IDistributedApplicationBuilder builder,\n        IResourceBuilder<ProjectResource> catalogApi,\n        IResourceBuilder<ProjectResource> webApp)\n    {\n        var ollama = builder.AddOllama(\"ollama\")\n            .WithDataVolume()\n            .WithGPUSupport()\n            .WithOpenWebUI();\n        var embeddings = ollama.AddModel(\"embedding\", \"all-minilm\");\n        var chat = ollama.AddModel(\"chat\", \"llama3.1\");\n\n        catalogApi.WithReference(embeddings)\n            .WithEnvironment(\"OllamaEnabled\", \"true\")\n            .WaitFor(embeddings);\n        webApp.WithReference(chat)\n            .WithEnvironment(\"OllamaEnabled\", \"true\")\n            .WaitFor(chat);\n\n        return builder;\n    }\n\n    public static IResourceBuilder<YarpResource> ConfigureMobileBffRoutes(this IResourceBuilder<YarpResource> builder,\n        IResourceBuilder<ProjectResource> catalogApi,\n        IResourceBuilder<ProjectResource> orderingApi,\n        IResourceBuilder<ProjectResource> identityApi)\n    {\n        return builder.WithConfiguration(yarp =>\n        {\n            var catalogCluster = yarp.AddCluster(catalogApi);\n\n            yarp.AddRoute(\"/catalog-api/api/catalog/items\", catalogCluster)\n                .WithMatchRouteQueryParameter([new() { Name = \"api-version\", Values = [\"1.0\", \"1\", \"2.0\"], Mode = QueryParameterMatchMode.Exact }])\n                .WithTransformPathRemovePrefix(\"/catalog-api\");\n\n            yarp.AddRoute(\"/catalog-api/api/catalog/items/by\", catalogCluster)\n                .WithMatchRouteQueryParameter([new() { Name = \"api-version\", Values = [\"1.0\", \"1\", \"2.0\"], Mode = QueryParameterMatchMode.Exact }])\n                .WithTransformPathRemovePrefix(\"/catalog-api\");\n\n            yarp.AddRoute(\"/catalog-api/api/catalog/items/{id}\", catalogCluster)\n                .WithMatchRouteQueryParameter([new() { Name = \"api-version\", Values = [\"1.0\", \"1\", \"2.0\"], Mode = QueryParameterMatchMode.Exact }])\n                .WithTransformPathRemovePrefix(\"/catalog-api\");\n\n            yarp.AddRoute(\"/catalog-api/api/catalog/items/by/{name}\", catalogCluster)\n                .WithMatchRouteQueryParameter([new() { Name = \"api-version\", Values = [\"1.0\", \"1\"], Mode = QueryParameterMatchMode.Exact }])\n                .WithTransformPathRemovePrefix(\"/catalog-api\");\n\n            yarp.AddRoute(\"/catalog-api/api/catalog/items/withsemanticrelevance/{text}\", catalogCluster)\n                .WithMatchRouteQueryParameter([new() { Name = \"api-version\", Values = [\"1.0\", \"1\"], Mode = QueryParameterMatchMode.Exact }])\n                .WithTransformPathRemovePrefix(\"/catalog-api\");\n\n            yarp.AddRoute(\"/catalog-api/api/catalog/items/withsemanticrelevance\", catalogCluster)\n                .WithMatchRouteQueryParameter([new() { Name = \"api-version\", Values = [\"2.0\"], Mode = QueryParameterMatchMode.Exact }])\n                .WithTransformPathRemovePrefix(\"/catalog-api\");\n\n            yarp.AddRoute(\"/catalog-api/api/catalog/items/type/{typeId}/brand/{brandId?}\", catalogCluster)\n                .WithMatchRouteQueryParameter([new() { Name = \"api-version\", Values = [\"1.0\", \"1\"], Mode = QueryParameterMatchMode.Exact }])\n                .WithTransformPathRemovePrefix(\"/catalog-api\");\n\n            yarp.AddRoute(\"/catalog-api/api/catalog/items/type/all/brand/{brandId?}\", catalogCluster)\n                .WithMatchRouteQueryParameter([new() { Name = \"api-version\", Values = [\"1.0\", \"1\"], Mode = QueryParameterMatchMode.Exact }])\n                .WithTransformPathRemovePrefix(\"/catalog-api\");\n\n            yarp.AddRoute(\"/catalog-api/api/catalog/catalogTypes\", catalogCluster)\n                .WithMatchRouteQueryParameter([new() { Name = \"api-version\", Values = [\"1.0\", \"1\", \"2.0\"], Mode = QueryParameterMatchMode.Exact }])\n                .WithTransformPathRemovePrefix(\"/catalog-api\");\n\n            yarp.AddRoute(\"/catalog-api/api/catalog/catalogBrands\", catalogCluster)\n                .WithMatchRouteQueryParameter([new() { Name = \"api-version\", Values = [\"1.0\", \"1\", \"2.0\"], Mode = QueryParameterMatchMode.Exact }])\n                .WithTransformPathRemovePrefix(\"/catalog-api\");\n\n            yarp.AddRoute(\"/catalog-api/api/catalog/items/{id}/pic\", catalogCluster)\n                .WithMatchRouteQueryParameter([new() { Name = \"api-version\", Values = [\"1.0\", \"1\", \"2.0\"], Mode = QueryParameterMatchMode.Exact }])\n                .WithTransformPathRemovePrefix(\"/catalog-api\");\n\n            // Generic catalog catch-all route\n            yarp.AddRoute(\"/api/catalog/{*any}\", catalogCluster)\n                .WithMatchRouteQueryParameter([new() { Name = \"api-version\", Values = [\"1.0\", \"1\", \"2.0\"], Mode = QueryParameterMatchMode.Exact }]);\n\n            // Ordering routes\n            yarp.AddRoute(\"/api/orders/{*any}\", orderingApi.GetEndpoint(\"http\"))\n                .WithMatchRouteQueryParameter([new() { Name = \"api-version\", Values = [\"1.0\", \"1\"], Mode = QueryParameterMatchMode.Exact }]);\n\n            // Identity routes\n            yarp.AddRoute(\"/identity/{*any}\", identityApi.GetEndpoint(\"http\"))\n                .WithTransformPathRemovePrefix(\"/identity\");\n        });\n    }\n}\n"
  },
  {
    "path": "src/eShop.AppHost/Program.cs",
    "content": "﻿using eShop.AppHost;\n\nvar builder = DistributedApplication.CreateBuilder(args);\n\nbuilder.AddForwardedHeaders();\n\nvar redis = builder.AddRedis(\"redis\");\nvar rabbitMq = builder.AddRabbitMQ(\"eventbus\")\n    .WithLifetime(ContainerLifetime.Persistent);\nvar postgres = builder.AddPostgres(\"postgres\")\n    .WithImage(\"ankane/pgvector\")\n    .WithImageTag(\"latest\")\n    .WithLifetime(ContainerLifetime.Persistent);\n\nvar catalogDb = postgres.AddDatabase(\"catalogdb\");\nvar identityDb = postgres.AddDatabase(\"identitydb\");\nvar orderDb = postgres.AddDatabase(\"orderingdb\");\nvar webhooksDb = postgres.AddDatabase(\"webhooksdb\");\n\nvar launchProfileName = ShouldUseHttpForEndpoints() ? \"http\" : \"https\";\n\n// Services\nvar identityApi = builder.AddProject<Projects.Identity_API>(\"identity-api\", launchProfileName)\n    .WithExternalHttpEndpoints()\n    .WithReference(identityDb);\n\nvar identityEndpoint = identityApi.GetEndpoint(launchProfileName);\n\nvar basketApi = builder.AddProject<Projects.Basket_API>(\"basket-api\")\n    .WithReference(redis)\n    .WithReference(rabbitMq).WaitFor(rabbitMq)\n    .WithEnvironment(\"Identity__Url\", identityEndpoint);\nredis.WithParentRelationship(basketApi);\n\nvar catalogApi = builder.AddProject<Projects.Catalog_API>(\"catalog-api\")\n    .WithReference(rabbitMq).WaitFor(rabbitMq)\n    .WithReference(catalogDb);\n\nvar orderingApi = builder.AddProject<Projects.Ordering_API>(\"ordering-api\")\n    .WithReference(rabbitMq).WaitFor(rabbitMq)\n    .WithReference(orderDb).WaitFor(orderDb)\n    .WithHttpHealthCheck(\"/health\")\n    .WithEnvironment(\"Identity__Url\", identityEndpoint);\n\nbuilder.AddProject<Projects.OrderProcessor>(\"order-processor\")\n    .WithReference(rabbitMq).WaitFor(rabbitMq)\n    .WithReference(orderDb)\n    .WaitFor(orderingApi); // wait for the orderingApi to be ready because that contains the EF migrations\n\nbuilder.AddProject<Projects.PaymentProcessor>(\"payment-processor\")\n    .WithReference(rabbitMq).WaitFor(rabbitMq);\n\nvar webHooksApi = builder.AddProject<Projects.Webhooks_API>(\"webhooks-api\")\n    .WithReference(rabbitMq).WaitFor(rabbitMq)\n    .WithReference(webhooksDb)\n    .WithEnvironment(\"Identity__Url\", identityEndpoint);\n\n// Reverse proxies\nbuilder.AddYarp(\"mobile-bff\")\n    .WithExternalHttpEndpoints()\n    .ConfigureMobileBffRoutes(catalogApi, orderingApi, identityApi);\n\n// Apps\nvar webhooksClient = builder.AddProject<Projects.WebhookClient>(\"webhooksclient\", launchProfileName)\n    .WithReference(webHooksApi)\n    .WithEnvironment(\"IdentityUrl\", identityEndpoint);\n\nvar webApp = builder.AddProject<Projects.WebApp>(\"webapp\", launchProfileName)\n    .WithExternalHttpEndpoints()\n    .WithUrls(c => c.Urls.ForEach(u => u.DisplayText = $\"Online Store ({u.Endpoint?.EndpointName})\"))\n    .WithReference(basketApi)\n    .WithReference(catalogApi)\n    .WithReference(orderingApi)\n    .WithReference(rabbitMq).WaitFor(rabbitMq)\n    .WithEnvironment(\"IdentityUrl\", identityEndpoint);\n\n// set to true if you want to use OpenAI\nbool useOpenAI = false;\nif (useOpenAI)\n{\n    builder.AddOpenAI(catalogApi, webApp, OpenAITarget.OpenAI); // set to AzureOpenAI if you want to use Azure OpenAI\n}\n\nbool useOllama = false;\nif (useOllama)\n{\n    builder.AddOllama(catalogApi, webApp);\n}\n\n// Wire up the callback urls (self referencing)\nwebApp.WithEnvironment(\"CallBackUrl\", webApp.GetEndpoint(launchProfileName));\nwebhooksClient.WithEnvironment(\"CallBackUrl\", webhooksClient.GetEndpoint(launchProfileName));\n\n// Identity has a reference to all of the apps for callback urls, this is a cyclic reference\nidentityApi.WithEnvironment(\"BasketApiClient\", basketApi.GetEndpoint(\"http\"))\n           .WithEnvironment(\"OrderingApiClient\", orderingApi.GetEndpoint(\"http\"))\n           .WithEnvironment(\"WebhooksApiClient\", webHooksApi.GetEndpoint(\"http\"))\n           .WithEnvironment(\"WebhooksWebClient\", webhooksClient.GetEndpoint(launchProfileName))\n           .WithEnvironment(\"WebAppClient\", webApp.GetEndpoint(launchProfileName));\n\nbuilder.Build().Run();\n\n// For test use only.\n// Looks for an environment variable that forces the use of HTTP for all the endpoints. We\n// are doing this for ease of running the Playwright tests in CI.\nstatic bool ShouldUseHttpForEndpoints()\n{\n    const string EnvVarName = \"ESHOP_USE_HTTP_ENDPOINTS\";\n    var envValue = Environment.GetEnvironmentVariable(EnvVarName);\n\n    // Attempt to parse the environment variable value; return true if it's exactly \"1\".\n    return int.TryParse(envValue, out int result) && result == 1;\n}\n"
  },
  {
    "path": "src/eShop.AppHost/Properties/launchSettings.json",
    "content": "{\n  \"$schema\": \"http://json.schemastore.org/launchsettings.json\",\n  \"profiles\": {\n    \"https\": {\n      \"commandName\": \"Project\",\n      \"dotnetRunMessages\": true,\n      \"launchBrowser\": true,\n      \"applicationUrl\": \"https://localhost:19888;http://localhost:18848\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"DOTNET_ENVIRONMENT\": \"Development\",\n        \"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL\": \"https://localhost:18076\",\n        \"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL\": \"https://localhost:19076\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/eShop.AppHost/appsettings.json",
    "content": "{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\",\n      \"Aspire.Hosting.Dcp\": \"Warning\"\n    }\n  },\n  \"ConnectionStrings\": {\n    //\"OpenAi\": \"Endpoint=xxxx;Key=xxxx\"\n  }\n}\n"
  },
  {
    "path": "src/eShop.AppHost/eShop.AppHost.csproj",
    "content": "<Project Sdk=\"Aspire.AppHost.Sdk/13.1.0\">\n  \n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <TargetFramework>net10.0</TargetFramework>\n    <Nullable>enable</Nullable>\n    <IsPackable>false</IsPackable>\n    <UserSecretsId>b99dbce4-17d4-41d2-858a-2b0529d60bb8</UserSecretsId>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Aspire.Hosting.RabbitMQ\" />\n    <PackageReference Include=\"Aspire.Hosting.Redis\" />\n    <PackageReference Include=\"Aspire.Hosting.PostgreSQL\" />\n    <PackageReference Include=\"Aspire.Hosting.Azure.CognitiveServices\" />\n    <PackageReference Include=\"Aspire.Hosting.Yarp\" />\n    <PackageReference Include=\"CommunityToolkit.Aspire.Hosting.Ollama\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Basket.API\\Basket.API.csproj\" />\n    <ProjectReference Include=\"..\\Catalog.API\\Catalog.API.csproj\" />\n    <ProjectReference Include=\"..\\Identity.API\\Identity.API.csproj\" />\n    <ProjectReference Include=\"..\\Ordering.API\\Ordering.API.csproj\" />\n    <ProjectReference Include=\"..\\OrderProcessor\\OrderProcessor.csproj\" />\n    <ProjectReference Include=\"..\\PaymentProcessor\\PaymentProcessor.csproj\" />\n    <ProjectReference Include=\"..\\Webhooks.API\\Webhooks.API.csproj\" />\n    <ProjectReference Include=\"..\\WebApp\\WebApp.csproj\" />\n    <ProjectReference Include=\"..\\WebhookClient\\WebhookClient.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/eShop.ServiceDefaults/AuthenticationExtensions.cs",
    "content": "﻿using Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.IdentityModel.JsonWebTokens;\n\nnamespace eShop.ServiceDefaults;\n\npublic static class AuthenticationExtensions\n{\n    public static IServiceCollection AddDefaultAuthentication(this IHostApplicationBuilder builder)\n    {\n        var services = builder.Services;\n        var configuration = builder.Configuration;\n\n        // {\n        //   \"Identity\": {\n        //     \"Url\": \"http://identity\",\n        //     \"Audience\": \"basket\"\n        //    }\n        // }\n\n        var identitySection = configuration.GetSection(\"Identity\");\n\n        if (!identitySection.Exists())\n        {\n            // No identity section, so no authentication\n            return services;\n        }\n\n        // prevent from mapping \"sub\" claim to nameidentifier.\n        JsonWebTokenHandler.DefaultInboundClaimTypeMap.Remove(\"sub\");\n\n        services.AddAuthentication().AddJwtBearer(options =>\n        {\n            var identityUrl = identitySection.GetRequiredValue(\"Url\");\n            var audience = identitySection.GetRequiredValue(\"Audience\");\n\n            options.Authority = identityUrl;\n            options.RequireHttpsMetadata = false;\n            options.Audience = audience;\n            \n#if DEBUG\n            //Needed if using Android Emulator Locally. See https://learn.microsoft.com/en-us/dotnet/maui/data-cloud/local-web-services?view=net-maui-8.0#android\n            options.TokenValidationParameters.ValidIssuers = [identityUrl, \"https://10.0.2.2:5243\"];\n#else\n            options.TokenValidationParameters.ValidIssuers = [identityUrl];\n#endif\n            \n            options.TokenValidationParameters.ValidateAudience = false;\n        });\n\n        services.AddAuthorization();\n\n        return services;\n    }\n}\n"
  },
  {
    "path": "src/eShop.ServiceDefaults/ClaimsPrincipalExtensions.cs",
    "content": "﻿using System.Security.Claims;\n\nnamespace eShop.ServiceDefaults;\n\npublic static class ClaimsPrincipalExtensions\n{\n    public static string? GetUserId(this ClaimsPrincipal principal)\n        => principal.FindFirst(\"sub\")?.Value;\n\n    public static string? GetUserName(this ClaimsPrincipal principal) =>\n        principal.FindFirst(x => x.Type == ClaimTypes.Name)?.Value;\n}\n"
  },
  {
    "path": "src/eShop.ServiceDefaults/ConfigurationExtensions.cs",
    "content": "﻿namespace Microsoft.Extensions.Configuration;\n\npublic static class ConfigurationExtensions\n{\n    public static string GetRequiredValue(this IConfiguration configuration, string name) =>\n        configuration[name] ?? throw new InvalidOperationException($\"Configuration missing value for: {(configuration is IConfigurationSection s ? s.Path + \":\" + name : name)}\");\n}\n"
  },
  {
    "path": "src/eShop.ServiceDefaults/Extensions.cs",
    "content": "﻿using Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Diagnostics.HealthChecks;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Diagnostics.HealthChecks;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing OpenTelemetry.Logs;\nusing OpenTelemetry.Metrics;\nusing OpenTelemetry.Trace;\n\nnamespace eShop.ServiceDefaults;\n\npublic static partial class Extensions\n{\n    public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)\n    {\n        builder.AddBasicServiceDefaults();\n\n        builder.Services.AddServiceDiscovery();\n\n        builder.Services.ConfigureHttpClientDefaults(http =>\n        {\n            // Turn on resilience by default\n            http.AddStandardResilienceHandler();\n\n            // Turn on service discovery by default\n            http.AddServiceDiscovery();\n        });\n\n        return builder;\n    }\n\n    /// <summary>\n    /// Adds the services except for making outgoing HTTP calls.\n    /// </summary>\n    /// <remarks>\n    /// This allows for things like Polly to be trimmed out of the app if it isn't used.\n    /// </remarks>\n    public static IHostApplicationBuilder AddBasicServiceDefaults(this IHostApplicationBuilder builder)\n    {\n        // Default health checks assume the event bus and self health checks\n        builder.AddDefaultHealthChecks();\n\n        builder.ConfigureOpenTelemetry();\n\n        return builder;\n    }\n\n    public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)\n    {\n        builder.Logging.AddOpenTelemetry(logging =>\n        {\n            logging.IncludeFormattedMessage = true;\n            logging.IncludeScopes = true;\n        });\n\n        builder.Services.AddOpenTelemetry()\n            .WithMetrics(metrics =>\n            {\n                metrics.AddAspNetCoreInstrumentation()\n                    .AddHttpClientInstrumentation()\n                    .AddRuntimeInstrumentation()\n                    .AddMeter(\"Experimental.Microsoft.Extensions.AI\");\n            })\n            .WithTracing(tracing =>\n            {\n                if (builder.Environment.IsDevelopment())\n                {\n                    // We want to view all traces in development\n                    tracing.SetSampler(new AlwaysOnSampler());\n                }\n\n                tracing.AddAspNetCoreInstrumentation()\n                    .AddGrpcClientInstrumentation()\n                    .AddHttpClientInstrumentation()\n                    .AddSource(\"Experimental.Microsoft.Extensions.AI\");                    \n            });\n\n        builder.AddOpenTelemetryExporters();\n\n        return builder;\n    }\n\n    private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)\n    {\n        var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration[\"OTEL_EXPORTER_OTLP_ENDPOINT\"]);\n\n        if (useOtlpExporter)\n        {\n            builder.Services.Configure<OpenTelemetryLoggerOptions>(logging => logging.AddOtlpExporter());\n            builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter());\n            builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter());\n        }\n\n        return builder;\n    }\n\n    public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder)\n    {\n        builder.Services.AddHealthChecks()\n            // Add a default liveness check to ensure app is responsive\n            .AddCheck(\"self\", () => HealthCheckResult.Healthy(), [\"live\"]);\n\n        return builder;\n    }\n\n    public static WebApplication MapDefaultEndpoints(this WebApplication app)\n    {\n        // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package)\n        // app.MapPrometheusScrapingEndpoint();\n\n        // Adding health checks endpoints to applications in non-development environments has security implications.\n        // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.\n        if (app.Environment.IsDevelopment())\n        {\n            // All health checks must pass for app to be considered ready to accept traffic after starting\n            app.MapHealthChecks(\"/health\");\n\n            // Only health checks tagged with the \"live\" tag must pass for app to be considered alive\n            app.MapHealthChecks(\"/alive\", new HealthCheckOptions\n            {\n                Predicate = r => r.Tags.Contains(\"live\")\n            });\n        }\n\n        return app;\n    }\n}\n"
  },
  {
    "path": "src/eShop.ServiceDefaults/HttpClientExtensions.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing System.Net.Http.Headers;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Microsoft.AspNetCore.Authentication;\n\nnamespace eShop.ServiceDefaults;\n\npublic static class HttpClientExtensions\n{\n    public static IHttpClientBuilder AddAuthToken(this IHttpClientBuilder builder)\n    {\n        builder.Services.AddHttpContextAccessor();\n\n        builder.Services.TryAddTransient<HttpClientAuthorizationDelegatingHandler>();\n\n        builder.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>();\n\n        return builder;\n    }\n\n    private class HttpClientAuthorizationDelegatingHandler : DelegatingHandler\n    {\n        private readonly IHttpContextAccessor _httpContextAccessor;\n\n        public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccessor)\n        {\n            _httpContextAccessor = httpContextAccessor;\n        }\n\n        public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccessor, HttpMessageHandler innerHandler) : base(innerHandler)\n        {\n            _httpContextAccessor = httpContextAccessor;\n        }\n\n        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n        {\n            if (_httpContextAccessor.HttpContext is HttpContext context)\n            {\n                var accessToken = await context.GetTokenAsync(\"access_token\");\n\n                if (accessToken is not null)\n                {\n                    request.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", accessToken);\n                }\n            }\n\n            return await base.SendAsync(request, cancellationToken);\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/eShop.ServiceDefaults/OpenApi.Extensions.cs",
    "content": "﻿using Asp.Versioning;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Routing;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Scalar.AspNetCore;\n\nnamespace eShop.ServiceDefaults;\n\npublic static partial class Extensions\n{\n    public static IApplicationBuilder UseDefaultOpenApi(this WebApplication app)\n    {\n        var configuration = app.Configuration;\n        var openApiSection = configuration.GetSection(\"OpenApi\");\n\n        if (!openApiSection.Exists())\n        {\n            return app;\n        }\n\n        app.MapOpenApi();\n\n        if (app.Environment.IsDevelopment())\n        {\n            app.MapScalarApiReference(options =>\n            {\n                // Disable default fonts to avoid download unnecessary fonts\n                options.DefaultFonts = false;\n            });\n            app.MapGet(\"/\", () => Results.Redirect(\"/scalar/v1\")).ExcludeFromDescription();\n        }\n\n        return app;\n    }\n\n    public static IHostApplicationBuilder AddDefaultOpenApi(\n        this IHostApplicationBuilder builder,\n        IApiVersioningBuilder? apiVersioning = default)\n    {\n        var openApi = builder.Configuration.GetSection(\"OpenApi\");\n        var identitySection = builder.Configuration.GetSection(\"Identity\");\n\n        var scopes = identitySection.Exists()\n            ? identitySection.GetRequiredSection(\"Scopes\").GetChildren().ToDictionary(p => p.Key, p => p.Value)\n            : new Dictionary<string, string?>();\n\n\n        if (!openApi.Exists())\n        {\n            return builder;\n        }\n\n        if (apiVersioning is not null)\n        {\n            // the default format will just be ApiVersion.ToString(); for example, 1.0.\n            // this will format the version as \"'v'major[.minor][-status]\"\n            var versioned = apiVersioning.AddApiExplorer(options => options.GroupNameFormat = \"'v'VVV\");\n            string[] versions = [\"v1\", \"v2\"];\n            foreach (var description in versions)\n            {\n                builder.Services.AddOpenApi(description, options =>\n                {\n                    options.ApplyApiVersionInfo(openApi.GetRequiredValue(\"Document:Title\"), openApi.GetRequiredValue(\"Document:Description\"));\n                    options.ApplyAuthorizationChecks([.. scopes.Keys]);\n                    options.ApplySecuritySchemeDefinitions();\n                    options.ApplyOperationDeprecatedStatus();\n                    options.ApplyApiVersionDescription();\n                });\n            }\n        }\n\n        return builder;\n    }\n}\n"
  },
  {
    "path": "src/eShop.ServiceDefaults/OpenApiOptionsExtensions.cs",
    "content": "﻿using System.Text;\nusing Asp.Versioning.ApiExplorer;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc.ApiExplorer;\nusing Microsoft.AspNetCore.OpenApi;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Primitives;\nusing Microsoft.OpenApi;\nusing System.Text.Json.Nodes;\n\nnamespace eShop.ServiceDefaults;\n\ninternal static class OpenApiOptionsExtensions\n{\n    public static OpenApiOptions ApplyApiVersionInfo(this OpenApiOptions options, string title, string description)\n    {\n        options.AddDocumentTransformer((document, context, cancellationToken) =>\n        {\n            var versionedDescriptionProvider = context.ApplicationServices.GetService<IApiVersionDescriptionProvider>();\n            var apiDescription = versionedDescriptionProvider?.ApiVersionDescriptions\n                .SingleOrDefault(description => description.GroupName == context.DocumentName);\n            if (apiDescription is null)\n            {\n                return Task.CompletedTask;\n            }\n            document.Info.Version = apiDescription.ApiVersion.ToString();\n            document.Info.Title = title;\n            document.Info.Description = BuildDescription(apiDescription, description);\n            return Task.CompletedTask;\n        });\n        return options;\n    }\n\n    private static string BuildDescription(ApiVersionDescription api, string description)\n    {\n        var text = new StringBuilder(description);\n\n        if (api.IsDeprecated)\n        {\n            if (text.Length > 0)\n            {\n                if (text[^1] != '.')\n                {\n                    text.Append('.');\n                }\n\n                text.Append(' ');\n            }\n\n            text.Append(\"This API version has been deprecated.\");\n        }\n\n        if (api.SunsetPolicy is { } policy)\n        {\n            if (policy.Date is { } when)\n            {\n                if (text.Length > 0)\n                {\n                    text.Append(' ');\n                }\n\n                text.Append(\"The API will be sunset on \")\n                    .Append(when.Date.ToShortDateString())\n                    .Append('.');\n            }\n\n            if (policy.HasLinks)\n            {\n                text.AppendLine();\n\n                var rendered = false;\n\n                foreach (var link in policy.Links.Where(l => l.Type == \"text/html\"))\n                {\n                    if (!rendered)\n                    {\n                        text.Append(\"<h4>Links</h4><ul>\");\n                        rendered = true;\n                    }\n\n                    text.Append(\"<li><a href=\\\"\");\n                    text.Append(link.LinkTarget.OriginalString);\n                    text.Append(\"\\\">\");\n                    text.Append(\n                        StringSegment.IsNullOrEmpty(link.Title)\n                        ? link.LinkTarget.OriginalString\n                        : link.Title.ToString());\n                    text.Append(\"</a></li>\");\n                }\n\n                if (rendered)\n                {\n                    text.Append(\"</ul>\");\n                }\n            }\n        }\n\n        return text.ToString();\n    }\n\n    public static OpenApiOptions ApplySecuritySchemeDefinitions(this OpenApiOptions options)\n    {\n        options.AddDocumentTransformer<SecuritySchemeDefinitionsTransformer>();\n        return options;\n    }\n\n    public static OpenApiOptions ApplyAuthorizationChecks(this OpenApiOptions options, string[] scopes)\n    {\n        options.AddOperationTransformer((operation, context, cancellationToken) =>\n        {\n            var metadata = context.Description.ActionDescriptor.EndpointMetadata;\n\n            if (!metadata.OfType<IAuthorizeData>().Any())\n            {\n                return Task.CompletedTask;\n            }\n\n            operation.Responses ??= new OpenApiResponses();\n            operation.Responses.TryAdd(\"401\", new OpenApiResponse { Description = \"Unauthorized\" });\n            operation.Responses.TryAdd(\"403\", new OpenApiResponse { Description = \"Forbidden\" });\n\n            var oAuthScheme = new OpenApiSecuritySchemeReference(\"oauth2\", null);\n\n            operation.Security = new List<OpenApiSecurityRequirement>\n            {\n                new()\n                {\n                    [oAuthScheme] = scopes.ToList()\n                }\n            };\n\n            return Task.CompletedTask;\n        });\n        return options;\n    }\n\n    public static OpenApiOptions ApplyOperationDeprecatedStatus(this OpenApiOptions options)\n    {\n        options.AddOperationTransformer((operation, context, cancellationToken) =>\n        {\n            var apiDescription = context.Description;\n            operation.Deprecated |= apiDescription.IsDeprecated();\n            return Task.CompletedTask;\n        });\n        return options;\n    }\n\n    public static OpenApiOptions ApplyApiVersionDescription(this OpenApiOptions options)\n    {\n        options.AddOperationTransformer((operation, context, cancellationToken) =>\n        {\n            // Find parameter named \"api-version\" and add a description to it\n            var apiVersionParameter = operation.Parameters?.FirstOrDefault(p => p.Name == \"api-version\");\n            if (apiVersionParameter is not null)\n            {\n                apiVersionParameter.Description = \"The API version, in the format 'major.minor'.\";\n                if (apiVersionParameter.Schema is OpenApiSchema targetSchema)\n                {\n                    switch (context.DocumentName) {\n                        case \"v1\":\n                            targetSchema.Example = JsonNode.Parse(\"\\\"1.0\\\"\");\n                            break;\n                        case \"v2\":\n                            targetSchema.Example = JsonNode.Parse(\"\\\"2.0\\\"\");\n                            break;\n                    }\n                }\n            }\n            return Task.CompletedTask;\n        });\n        return options;\n    }\n\n    private class SecuritySchemeDefinitionsTransformer(IConfiguration configuration) : IOpenApiDocumentTransformer\n    {\n        public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)\n        {\n            var identitySection = configuration.GetSection(\"Identity\");\n            if (!identitySection.Exists())\n            {\n                return Task.CompletedTask;\n            }\n\n            var identityUrlExternal = identitySection.GetRequiredValue(\"Url\");\n            var scopes = identitySection.GetRequiredSection(\"Scopes\").GetChildren().ToDictionary(p => p.Key, p => p.Value ?? string.Empty);\n            var securityScheme = new OpenApiSecurityScheme\n            {\n                Type = SecuritySchemeType.OAuth2,\n                Flows = new OpenApiOAuthFlows()\n                {\n                    // TODO: Change this to use Authorization Code flow with PKCE\n                    Implicit = new OpenApiOAuthFlow()\n                    {\n                        AuthorizationUrl = new Uri($\"{identityUrlExternal}/connect/authorize\"),\n                        TokenUrl = new Uri($\"{identityUrlExternal}/connect/token\"),\n                        Scopes = scopes,\n                    }\n                }\n            };\n            document.Components ??= new();\n            document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();  \n            document.Components.SecuritySchemes.Add(\"oauth2\", securityScheme);\n            return Task.CompletedTask;\n        }\n    }\n}\n"
  },
  {
    "path": "src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <FrameworkReference Include=\"Microsoft.AspNetCore.App\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Asp.Versioning.Mvc.ApiExplorer\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.OpenApi\" />\n    <PackageReference Include=\"Microsoft.OpenApi\" />\n    <PackageReference Include=\"Scalar.AspNetCore\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Authentication.JwtBearer\" />\n    <PackageReference Include=\"Microsoft.Extensions.Http.Resilience\" />\n    <PackageReference Include=\"Microsoft.Extensions.ServiceDiscovery\" />\n    <PackageReference Include=\"OpenTelemetry.Exporter.OpenTelemetryProtocol\" />\n    <PackageReference Include=\"OpenTelemetry.Extensions.Hosting\" />\n    <PackageReference Include=\"OpenTelemetry.Instrumentation.AspNetCore\" />\n    <PackageReference Include=\"OpenTelemetry.Instrumentation.GrpcNetClient\" />\n    <PackageReference Include=\"OpenTelemetry.Instrumentation.Http\" />\n    <PackageReference Include=\"OpenTelemetry.Instrumentation.Runtime\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "tests/Basket.UnitTests/Basket.UnitTests.csproj",
    "content": "﻿<Project Sdk=\"MSTest.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks>\n    <IsPublishable>false</IsPublishable>\n    <IsPackable>false</IsPackable>\n    <OutputType>Exe</OutputType>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.AspNetCore.Mvc.Testing\" />\n    <PackageReference Include=\"Microsoft.Extensions.Identity.Stores\" />\n    <PackageReference Include=\"NSubstitute\" />\n    <PackageReference Include=\"NSubstitute.Analyzers.CSharp\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Basket.API\\Basket.API.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "tests/Basket.UnitTests/BasketServiceTests.cs",
    "content": "﻿using System.Security.Claims;\nusing eShop.Basket.API.Repositories;\nusing eShop.Basket.API.Grpc;\nusing eShop.Basket.API.Model;\nusing eShop.Basket.UnitTests.Helpers;\nusing Microsoft.Extensions.Logging.Abstractions;\nusing BasketItem = eShop.Basket.API.Model.BasketItem;\n\nnamespace eShop.Basket.UnitTests;\n\n[TestClass]\npublic class BasketServiceTests\n{\n    public TestContext TestContext { get; set; }\n\n    [TestMethod]\n    public async Task GetBasketReturnsEmptyForNoUser()\n    {\n        var mockRepository = Substitute.For<IBasketRepository>();\n        var service = new BasketService(mockRepository, NullLogger<BasketService>.Instance);\n        var serverCallContext = TestServerCallContext.Create(cancellationToken: TestContext.CancellationToken);\n        serverCallContext.SetUserState(\"__HttpContext\", new DefaultHttpContext());\n\n        var response = await service.GetBasket(new GetBasketRequest(), serverCallContext);\n\n        Assert.IsInstanceOfType<CustomerBasketResponse>(response);\n        Assert.IsEmpty(response.Items);\n    }\n\n    [TestMethod]\n    public async Task GetBasketReturnsItemsForValidUserId()\n    {\n        var mockRepository = Substitute.For<IBasketRepository>();\n        List<BasketItem> items = [new BasketItem { Id = \"some-id\" }];\n        mockRepository.GetBasketAsync(\"1\").Returns(Task.FromResult(new CustomerBasket { BuyerId = \"1\", Items = items }));\n        var service = new BasketService(mockRepository, NullLogger<BasketService>.Instance);\n        var serverCallContext = TestServerCallContext.Create(cancellationToken: TestContext.CancellationToken);\n        var httpContext = new DefaultHttpContext();\n        httpContext.User = new ClaimsPrincipal(new ClaimsIdentity([new Claim(\"sub\", \"1\")]));\n        serverCallContext.SetUserState(\"__HttpContext\", httpContext);\n\n        var response = await service.GetBasket(new GetBasketRequest(), serverCallContext);\n\n        Assert.IsInstanceOfType<CustomerBasketResponse>(response);\n        Assert.HasCount(1, response.Items);\n    }\n\n    [TestMethod]\n    public async Task GetBasketReturnsEmptyForInvalidUserId()\n    {\n        var mockRepository = Substitute.For<IBasketRepository>();\n        List<BasketItem> items = [new BasketItem { Id = \"some-id\" }];\n        mockRepository.GetBasketAsync(\"1\").Returns(Task.FromResult(new CustomerBasket { BuyerId = \"1\", Items = items }));\n        var service = new BasketService(mockRepository, NullLogger<BasketService>.Instance);\n        var serverCallContext = TestServerCallContext.Create(cancellationToken: TestContext.CancellationToken);\n        var httpContext = new DefaultHttpContext();\n        serverCallContext.SetUserState(\"__HttpContext\", httpContext);\n\n        var response = await service.GetBasket(new GetBasketRequest(), serverCallContext);\n\n        Assert.IsInstanceOfType<CustomerBasketResponse>(response);\n        Assert.IsEmpty(response.Items);\n    }\n}\n"
  },
  {
    "path": "tests/Basket.UnitTests/GlobalUsings.cs",
    "content": "﻿global using System;\nglobal using System.Collections.Generic;\nglobal using System.Threading;\nglobal using System.Threading.Tasks;\nglobal using Microsoft.AspNetCore.Http;\nglobal using Microsoft.AspNetCore.Mvc;\nglobal using NSubstitute;\nglobal using Microsoft.VisualStudio.TestTools.UnitTesting;\n\n[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.MethodLevel)]\n"
  },
  {
    "path": "tests/Basket.UnitTests/Helpers/TestServerCallContext.cs",
    "content": "﻿using Grpc.Core;\n\nnamespace eShop.Basket.UnitTests.Helpers;\n\npublic class TestServerCallContext : ServerCallContext\n    {\n        private readonly Metadata _requestHeaders;\n        private readonly CancellationToken _cancellationToken;\n        private readonly Metadata _responseTrailers;\n        private readonly AuthContext _authContext;\n        private readonly Dictionary<object, object> _userState;\n        private WriteOptions _writeOptions;\n\n        public Metadata ResponseHeaders { get; private set; }\n\n        private TestServerCallContext(Metadata requestHeaders, CancellationToken cancellationToken)\n        {\n            _requestHeaders = requestHeaders;\n            _cancellationToken = cancellationToken;\n            _responseTrailers = new Metadata();\n            _authContext = new AuthContext(string.Empty, new Dictionary<string, List<AuthProperty>>());\n            _userState = new Dictionary<object, object>();\n        }\n\n        protected override string MethodCore => \"MethodName\";\n        protected override string HostCore => \"HostName\";\n        protected override string PeerCore => \"PeerName\";\n        protected override DateTime DeadlineCore { get; }\n        protected override Metadata RequestHeadersCore => _requestHeaders;\n        protected override CancellationToken CancellationTokenCore => _cancellationToken;\n        protected override Metadata ResponseTrailersCore => _responseTrailers;\n        protected override Status StatusCore { get; set; }\n        protected override WriteOptions WriteOptionsCore { get => _writeOptions; set { _writeOptions = value; } }\n        protected override AuthContext AuthContextCore => _authContext;\n\n        protected override ContextPropagationToken CreatePropagationTokenCore(ContextPropagationOptions options)\n        {\n            throw new NotImplementedException();\n        }\n\n        protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders)\n        {\n            if (ResponseHeaders != null)\n            {\n                throw new InvalidOperationException(\"Response headers have already been written.\");\n            }\n\n            ResponseHeaders = responseHeaders;\n            return Task.CompletedTask;\n        }\n\n        protected override IDictionary<object, object> UserStateCore => _userState;\n\n        internal void SetUserState(object key, object value)\n            => _userState[key] = value;\n\n        public static TestServerCallContext Create(Metadata requestHeaders = null, CancellationToken cancellationToken = default)\n        {\n            return new TestServerCallContext(requestHeaders: new Metadata(), cancellationToken);\n        }\n    }\n"
  },
  {
    "path": "tests/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj",
    "content": "﻿<Project Sdk=\"Aspire.AppHost.Sdk/13.0.0\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <IsPublishable>false</IsPublishable>\n    <IsPackable>false</IsPackable>\n    <OutputType>Exe</OutputType>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Asp.Versioning.Http.Client\" />\n    <PackageReference Include=\"Aspire.Hosting.PostgreSQL\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Mvc.Testing\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.TestHost\" />\n    <PackageReference Include=\"xunit.v3.mtp-v2\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Catalog.API\\Catalog.API.csproj\" IsAspireProjectResource=\"false\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "tests/Catalog.FunctionalTests/CatalogApiFixture.cs",
    "content": "﻿using System.Reflection;\n\nusing Aspire.Hosting;\nusing Aspire.Hosting.ApplicationModel;\n\nusing Microsoft.AspNetCore.Mvc.Testing;\n\nnamespace eShop.Catalog.FunctionalTests;\n\npublic sealed class CatalogApiFixture : WebApplicationFactory<Program>, IAsyncLifetime\n{\n    private readonly IHost _app;\n\n    public IResourceBuilder<PostgresServerResource> Postgres { get; private set; }\n    private string _postgresConnectionString;\n\n    public CatalogApiFixture()\n    {\n        var options = new DistributedApplicationOptions { AssemblyName = typeof(CatalogApiFixture).Assembly.FullName, DisableDashboard = true };\n        var appBuilder = DistributedApplication.CreateBuilder(options);\n        Postgres = appBuilder.AddPostgres(\"CatalogDB\")\n            .WithImage(\"ankane/pgvector\")\n            .WithImageTag(\"latest\");\n        _app = appBuilder.Build();\n    }\n\n    protected override IHost CreateHost(IHostBuilder builder)\n    {\n        builder.ConfigureHostConfiguration(config =>\n        {\n            config.AddInMemoryCollection(new Dictionary<string, string>\n            {\n                { $\"ConnectionStrings:{Postgres.Resource.Name}\", _postgresConnectionString },\n                });\n        });\n        return base.CreateHost(builder);\n    }\n\n    public new async Task DisposeAsync()\n    {\n        await base.DisposeAsync();\n        await _app.StopAsync();\n        if (_app is IAsyncDisposable asyncDisposable)\n        {\n            await asyncDisposable.DisposeAsync().ConfigureAwait(false);\n        }\n        else\n        {\n            _app.Dispose();\n        }\n    }\n\n    public async ValueTask InitializeAsync()\n    {\n        await _app.StartAsync();\n        _postgresConnectionString = await Postgres.Resource.GetConnectionStringAsync();\n    }\n}\n"
  },
  {
    "path": "tests/Catalog.FunctionalTests/CatalogApiTests.cs",
    "content": "﻿using System.Net.Http.Json;\nusing System.Text.Json;\nusing Asp.Versioning;\nusing Asp.Versioning.Http;\nusing eShop.Catalog.API.Model;\nusing Microsoft.AspNetCore.Mvc.Testing;\n\nnamespace eShop.Catalog.FunctionalTests;\n\npublic sealed class CatalogApiTests : IClassFixture<CatalogApiFixture>\n{\n    private readonly WebApplicationFactory<Program> _webApplicationFactory;\n    private readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web);\n\n    public CatalogApiTests(CatalogApiFixture fixture)\n    {\n        _webApplicationFactory = fixture;\n    }\n\n    private HttpClient CreateHttpClient(ApiVersion apiVersion)\n    {\n        var handler = new ApiVersionHandler(new QueryStringApiVersionWriter(), apiVersion);\n        return _webApplicationFactory.CreateDefaultClient(handler);\n    }\n\n    [Theory]\n    [InlineData(1.0)]\n    [InlineData(2.0)]\n    public async Task GetCatalogItemsRespectsPageSize(double version)\n    {\n        var _httpClient = CreateHttpClient(new ApiVersion(version));\n\n        // Act\n        var response = await _httpClient.GetAsync(\"/api/catalog/items?pageIndex=0&pageSize=5\", TestContext.Current.CancellationToken);\n\n        // Assert\n        response.EnsureSuccessStatusCode();\n        var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        var result = JsonSerializer.Deserialize<PaginatedItems<CatalogItem>>(body, _jsonSerializerOptions);\n\n        // Assert 103 total items (101 seeded + 2 added by AddCatalogItem tests) with 5 retrieved from index 0\n        Assert.Equal(103, result.Count);\n        Assert.Equal(0, result.PageIndex);\n        Assert.Equal(5, result.PageSize);\n    }\n\n    [Theory]\n    [InlineData(1.0)]\n    [InlineData(2.0)]\n    public async Task UpdateCatalogItemWorksWithoutPriceUpdate(double version)\n    {\n        var _httpClient = CreateHttpClient(new ApiVersion(version));\n\n        // Act - 1\n        var response = await _httpClient.GetAsync(\"/api/catalog/items/1\", TestContext.Current.CancellationToken);\n        response.EnsureSuccessStatusCode();\n        var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        var itemToUpdate = JsonSerializer.Deserialize<CatalogItem>(body, _jsonSerializerOptions);\n\n        // Act - 2\n        var priorAvailableStock = itemToUpdate.AvailableStock;\n        itemToUpdate.AvailableStock -= 1;\n        response = version switch\n        {\n            1.0 => await _httpClient.PutAsJsonAsync(\"/api/catalog/items\", itemToUpdate, TestContext.Current.CancellationToken),\n            2.0 => await _httpClient.PutAsJsonAsync($\"/api/catalog/items/{itemToUpdate.Id}\", itemToUpdate, TestContext.Current.CancellationToken),\n            _ => throw new ArgumentOutOfRangeException(nameof(version), version, null)\n        };\n        response.EnsureSuccessStatusCode();\n\n        // Act - 3\n        response = await _httpClient.GetAsync(\"/api/catalog/items/1\", TestContext.Current.CancellationToken);\n        response.EnsureSuccessStatusCode();\n        body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        var updatedItem = JsonSerializer.Deserialize<CatalogItem>(body, _jsonSerializerOptions);\n\n        // Assert - 1\n        Assert.Equal(itemToUpdate.Id, updatedItem.Id);\n        Assert.NotEqual(priorAvailableStock, updatedItem.AvailableStock);\n    }\n\n    [Theory]\n    [InlineData(1.0)]\n    [InlineData(2.0)]\n    public async Task UpdateCatalogItemWorksWithPriceUpdate(double version)\n    {\n        var _httpClient = CreateHttpClient(new ApiVersion(version));\n\n        // Act - 1\n        var response = await _httpClient.GetAsync(\"/api/catalog/items/1\", TestContext.Current.CancellationToken);\n        response.EnsureSuccessStatusCode();\n        var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        var itemToUpdate = JsonSerializer.Deserialize<CatalogItem>(body, _jsonSerializerOptions);\n\n        // Act - 2\n        var priorAvailableStock = itemToUpdate.AvailableStock;\n        itemToUpdate.AvailableStock -= 1;\n        itemToUpdate.Price = 1.99m;\n        response = version switch\n        {\n            1.0 => await _httpClient.PutAsJsonAsync(\"/api/catalog/items\", itemToUpdate, TestContext.Current.CancellationToken),\n            2.0 => await _httpClient.PutAsJsonAsync($\"/api/catalog/items/{itemToUpdate.Id}\", itemToUpdate, TestContext.Current.CancellationToken),\n            _ => throw new ArgumentOutOfRangeException(nameof(version), version, null)\n        };\n        response.EnsureSuccessStatusCode();\n\n        // Act - 3\n        response = await _httpClient.GetAsync(\"/api/catalog/items/1\", TestContext.Current.CancellationToken);\n        response.EnsureSuccessStatusCode();\n        body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        var updatedItem = JsonSerializer.Deserialize<CatalogItem>(body, _jsonSerializerOptions);\n\n        // Assert - 1\n        Assert.Equal(itemToUpdate.Id, updatedItem.Id);\n        Assert.Equal(1.99m, updatedItem.Price);\n        Assert.NotEqual(priorAvailableStock, updatedItem.AvailableStock);\n    }\n\n    [Theory]\n    [InlineData(1.0)]\n    [InlineData(2.0)]\n    public async Task GetCatalogItemsbyIds(double version)\n    {\n        var _httpClient = CreateHttpClient(new ApiVersion(version));\n\n        // Act\n        var response = await _httpClient.GetAsync(\"/api/catalog/items/by?ids=1&ids=2&ids=3\", TestContext.Current.CancellationToken);\n\n        // Arrange\n        response.EnsureSuccessStatusCode();\n        var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        var result = JsonSerializer.Deserialize<List<CatalogItem>>(body, _jsonSerializerOptions);\n\n        // Assert 3 items\n        Assert.Equal(3, result.Count);\n    }\n\n    [Theory]\n    [InlineData(1.0)]\n    [InlineData(2.0)]\n    public async Task GetCatalogItemWithId(double version)\n    {\n        var _httpClient = CreateHttpClient(new ApiVersion(version));\n\n        // Act\n        var response = await _httpClient.GetAsync(\"/api/catalog/items/2\", TestContext.Current.CancellationToken);\n\n        // Arrange\n        response.EnsureSuccessStatusCode();\n        var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        var result = JsonSerializer.Deserialize<CatalogItem>(body, _jsonSerializerOptions);\n\n        // Assert\n        Assert.Equal(2, result.Id);\n        Assert.NotNull(result);\n    }\n\n    [Theory]\n    [InlineData(1.0)]\n    [InlineData(2.0)]\n    public async Task GetCatalogItemWithExactName(double version)\n    {\n        var _httpClient = CreateHttpClient(new ApiVersion(version));\n\n        // Act\n        var response = version switch\n        {\n            1.0 => await _httpClient.GetAsync(\"api/catalog/items/by/Wanderer%20Black%20Hiking%20Boots?PageSize=5&PageIndex=0\", TestContext.Current.CancellationToken),\n            2.0 => await _httpClient.GetAsync(\"api/catalog/items?name=Wanderer%20Black%20Hiking%20Boots&PageSize=5&PageIndex=0\", TestContext.Current.CancellationToken),\n            _ => throw new ArgumentOutOfRangeException(nameof(version), version, null)\n        };\n\n        // Arrange\n        response.EnsureSuccessStatusCode();\n        var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        var result = JsonSerializer.Deserialize<PaginatedItems<CatalogItem>>(body, _jsonSerializerOptions);\n\n        // Assert\n        Assert.NotNull(result.Data);\n        Assert.Equal(1, result.Count);\n        Assert.Equal(0, result.PageIndex);\n        Assert.Equal(5, result.PageSize);\n        Assert.Equal(\"Wanderer Black Hiking Boots\", result.Data.ToList().FirstOrDefault().Name);\n    }\n\n    [Theory]\n    [InlineData(1.0)]\n    [InlineData(2.0)]\n    public async Task GetCatalogItemWithPartialName(double version)\n    {\n        var _httpClient = CreateHttpClient(new ApiVersion(version));\n\n        // Act\n        var response = version switch\n        {\n            1.0 => await _httpClient.GetAsync(\"api/catalog/items/by/Alpine?PageSize=5&PageIndex=0\", TestContext.Current.CancellationToken),\n            2.0 => await _httpClient.GetAsync(\"api/catalog/items?name=Alpine&PageSize=5&PageIndex=0\", TestContext.Current.CancellationToken),\n            _ => throw new ArgumentOutOfRangeException(nameof(version), version, null)\n        };\n\n        // Arrange\n        response.EnsureSuccessStatusCode();\n        var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        var result = JsonSerializer.Deserialize<PaginatedItems<CatalogItem>>(body, _jsonSerializerOptions);\n\n        // Assert\n        Assert.NotNull(result.Data);\n        Assert.Equal(4, result.Count);\n        Assert.Equal(0, result.PageIndex);\n        Assert.Equal(5, result.PageSize);\n        Assert.Contains(\"Alpine\", result.Data.ToList().FirstOrDefault().Name);\n    }\n\n    [Theory]\n    [InlineData(1.0)]\n    [InlineData(2.0)]\n    public async Task GetCatalogItemPicWithId(double version)\n    {\n        var _httpClient = CreateHttpClient(new ApiVersion(version));\n\n        // Act\n        var response = await _httpClient.GetAsync(\"api/catalog/items/1/pic\", TestContext.Current.CancellationToken);\n\n        // Arrange\n        response.EnsureSuccessStatusCode();\n        var result = response.Content.Headers.ContentType.MediaType;\n\n        // Assert\n        Assert.Equal(\"image/webp\", result);\n    }\n\n    [Theory]\n    [InlineData(1.0)]\n    [InlineData(2.0)]\n    public async Task GetCatalogItemWithsemanticrelevance(double version)\n    {\n        var _httpClient = CreateHttpClient(new ApiVersion(version));\n\n        // Act\n        var response = version switch\n        {\n            1.0 => await _httpClient.GetAsync(\"api/catalog/items/withsemanticrelevance/Wanderer?PageSize=5&PageIndex=0\", TestContext.Current.CancellationToken),\n            2.0 => await _httpClient.GetAsync(\"api/catalog/items/withsemanticrelevance?text=Wanderer&PageSize=5&PageIndex=0\", TestContext.Current.CancellationToken),\n            _ => throw new ArgumentOutOfRangeException(nameof(version), version, null)\n        };\n\n        // Arrange\n        response.EnsureSuccessStatusCode();\n        var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        var result = JsonSerializer.Deserialize<PaginatedItems<CatalogItem>>(body, _jsonSerializerOptions);\n\n        // Assert\n        Assert.Equal(1, result.Count);\n        Assert.NotNull(result.Data);\n        Assert.Equal(0, result.PageIndex);\n        Assert.Equal(5, result.PageSize);\n    }\n\n    [Theory]\n    [InlineData(1.0)]\n    [InlineData(2.0)]\n    public async Task GetCatalogItemWithTypeIdBrandId(double version)\n    {\n        var _httpClient = CreateHttpClient(new ApiVersion(version));\n\n        // Act\n        var response = version switch\n        {\n            1.0 => await _httpClient.GetAsync(\"api/catalog/items/type/3/brand/3?PageSize=5&PageIndex=0\", TestContext.Current.CancellationToken),\n            2.0 => await _httpClient.GetAsync(\"api/catalog/items?type=3&brand=3&PageSize=5&PageIndex=0\", TestContext.Current.CancellationToken),\n            _ => throw new ArgumentOutOfRangeException(nameof(version), version, null)\n        };\n\n        // Arrange\n        response.EnsureSuccessStatusCode();\n        var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        var result = JsonSerializer.Deserialize<PaginatedItems<CatalogItem>>(body, _jsonSerializerOptions);\n\n        // Assert\n        Assert.NotNull(result.Data);\n        Assert.Equal(4, result.Count);\n        Assert.Equal(0, result.PageIndex);\n        Assert.Equal(5, result.PageSize);\n        Assert.Equal(3, result.Data.ToList().FirstOrDefault().CatalogTypeId);\n        Assert.Equal(3, result.Data.ToList().FirstOrDefault().CatalogBrandId);\n    }\n\n    [Theory]\n    [InlineData(1.0)]\n    [InlineData(2.0)]\n    public async Task GetAllCatalogTypeItemWithBrandId(double version)\n    {\n        var _httpClient = CreateHttpClient(new ApiVersion(version));\n\n        // Act\n        var response = version switch\n        {\n            1.0 => await _httpClient.GetAsync(\"api/catalog/items/type/all/brand/3?PageSize=5&PageIndex=0\", TestContext.Current.CancellationToken),\n            2.0 => await _httpClient.GetAsync(\"api/catalog/items?brand=3&PageSize=5&PageIndex=0\", TestContext.Current.CancellationToken),\n            _ => throw new ArgumentOutOfRangeException(nameof(version), version, null)\n        };\n\n        // Arrange\n        response.EnsureSuccessStatusCode();\n        var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        var result = JsonSerializer.Deserialize<PaginatedItems<CatalogItem>>(body, _jsonSerializerOptions);\n\n        // Assert\n        Assert.NotNull(result.Data);\n        Assert.Equal(11, result.Count);\n        Assert.Equal(0, result.PageIndex);\n        Assert.Equal(5, result.PageSize);\n        Assert.Equal(3, result.Data.ToList().FirstOrDefault().CatalogBrandId);\n    }\n\n    [Theory]\n    [InlineData(1.0)]\n    [InlineData(2.0)]\n    public async Task GetAllCatalogTypes(double version)\n    {\n        var _httpClient = CreateHttpClient(new ApiVersion(version));\n\n        // Act\n        var response = await _httpClient.GetAsync(\"api/catalog/catalogtypes\", TestContext.Current.CancellationToken);\n\n        // Arrange\n        response.EnsureSuccessStatusCode();\n        var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        var result = JsonSerializer.Deserialize<List<CatalogType>>(body, _jsonSerializerOptions);\n\n        // Assert\n        Assert.Equal(8, result.Count);\n        Assert.NotNull(result);\n    }\n\n    [Theory]\n    [InlineData(1.0)]\n    [InlineData(2.0)]\n    public async Task GetAllCatalogBrands(double version)\n    {\n        var _httpClient = CreateHttpClient(new ApiVersion(version));\n\n        // Act\n        var response = await _httpClient.GetAsync(\"api/catalog/catalogbrands\", TestContext.Current.CancellationToken);\n\n        // Arrange\n        response.EnsureSuccessStatusCode();\n        var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        var result = JsonSerializer.Deserialize<List<CatalogBrand>>(body, _jsonSerializerOptions);\n\n        // Assert\n        Assert.Equal(13, result.Count);\n        Assert.NotNull(result);\n    }\n\n    [Theory]\n    [InlineData(1.0)]\n    [InlineData(2.0)]\n    public async Task AddCatalogItem(double version)\n    {\n        var _httpClient = CreateHttpClient(new ApiVersion(version));\n\n        var id = version switch {\n            1.0 => 10015,\n            2.0 => 10016,\n            _ => 0\n        };\n\n        // Act - 1\n        var bodyContent = new CatalogItem(\"TestCatalog1\") {\n            Id = id,\n            Description = \"Test catalog description 1\",\n            Price = 11000.08m,\n            PictureFileName = null,\n            CatalogTypeId = 8,\n            CatalogType = null,\n            CatalogBrandId = 13,\n            CatalogBrand = null,\n            AvailableStock = 100,\n            RestockThreshold = 10,\n            MaxStockThreshold = 200,\n            OnReorder = false\n        };\n        var response = await _httpClient.PostAsJsonAsync(\"/api/catalog/items\", bodyContent, TestContext.Current.CancellationToken);\n        response.EnsureSuccessStatusCode();\n\n        // Act - 2\n        response = await _httpClient.GetAsync($\"/api/catalog/items/{id}\", TestContext.Current.CancellationToken);\n        response.EnsureSuccessStatusCode();\n        var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        var addedItem = JsonSerializer.Deserialize<CatalogItem>(body, _jsonSerializerOptions);\n\n        // Assert - 1\n        Assert.Equal(bodyContent.Id, addedItem.Id);\n\n    }\n\n    [Theory]\n    [InlineData(1.0)]\n    [InlineData(2.0)]\n    public async Task DeleteCatalogItem(double version)\n    {\n        var _httpClient = CreateHttpClient(new ApiVersion(version));\n\n        var id = version switch {\n            1.0 => 5,\n            2.0 => 6,\n            _ => 0\n        };\n\n        //Act - 1\n        var response = await _httpClient.DeleteAsync($\"/api/catalog/items/{id}\", TestContext.Current.CancellationToken);\n        response.EnsureSuccessStatusCode();\n\n        // Act - 2\n        var response1 = await _httpClient.GetAsync($\"/api/catalog/items/{id}\", TestContext.Current.CancellationToken);\n        var responseStatus = response1.StatusCode;\n\n        // Assert - 1\n        Assert.Equal(\"NoContent\", response.StatusCode.ToString());\n        Assert.Equal(\"NotFound\", responseStatus.ToString());\n    }\n}\n"
  },
  {
    "path": "tests/Catalog.FunctionalTests/GlobalUsings.cs",
    "content": "﻿global using System;\nglobal using System.Collections.Generic;\nglobal using System.Linq;\nglobal using System.Net.Http;\nglobal using System.Threading.Tasks;\nglobal using Microsoft.AspNetCore.Hosting;\nglobal using Microsoft.AspNetCore.TestHost;\nglobal using Microsoft.Extensions.Configuration;\nglobal using Microsoft.Extensions.Hosting;\nglobal using Xunit;\n"
  },
  {
    "path": "tests/ClientApp.UnitTests/ClientApp.UnitTests.csproj",
    "content": "<Project Sdk=\"MSTest.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n    <IsPackable>false</IsPackable>\n    <UseMaui>true</UseMaui>\n    <OutputType>Exe</OutputType>\n    <ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>\n  </PropertyGroup>\n\n  <ItemGroup>\n\n    <PackageReference Include=\"Microsoft.Maui.Controls\" Version=\"9.0.40\" />\n\n    <PackageReference Include=\"Microsoft.Maui.Controls.Compatibility\" Version=\"9.0.40\" />\n    <PackageReference Include=\"Microsoft.Maui.Controls.Maps\" Version=\"9.0.40\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\ClientApp\\ClientApp.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "tests/ClientApp.UnitTests/ClientApp.UnitTests.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.5.002.0\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"ClientApp.UnitTests\", \"ClientApp.UnitTests.csproj\", \"{54B18315-E610-49D7-A10E-357C7DC20D71}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{54B18315-E610-49D7-A10E-357C7DC20D71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{54B18315-E610-49D7-A10E-357C7DC20D71}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{54B18315-E610-49D7-A10E-357C7DC20D71}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{54B18315-E610-49D7-A10E-357C7DC20D71}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {F305FAE3-88DD-49C5-A802-7897D8EA350F}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "tests/ClientApp.UnitTests/GlobalUsings.cs",
    "content": "﻿global using eShop.ClientApp.Services;\nglobal using eShop.ClientApp.Services.AppEnvironment;\nglobal using eShop.ClientApp.Services.Basket;\nglobal using eShop.ClientApp.Services.Catalog;\nglobal using eShop.ClientApp.Services.Order;\nglobal using eShop.ClientApp.Services.Settings;\nglobal using eShop.ClientApp.ViewModels;\nglobal using Microsoft.VisualStudio.TestTools.UnitTesting;\n\n[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.MethodLevel)]\n"
  },
  {
    "path": "tests/ClientApp.UnitTests/Mocks/MockDialogService.cs",
    "content": "﻿namespace ClientApp.UnitTests.Mocks;\n\npublic class MockDialogService : IDialogService\n{\n    public Task ShowAlertAsync(string message, string title, string buttonLabel)\n    {\n        return Task.CompletedTask;\n    }\n}\n\n"
  },
  {
    "path": "tests/ClientApp.UnitTests/Mocks/MockNavigationService.cs",
    "content": "﻿namespace ClientApp.UnitTests.Mocks;\n\npublic class MockNavigationService : INavigationService\n{\n    public Task InitializeAsync()\n    {\n        return Task.CompletedTask;\n    }\n\n    public Task NavigateToAsync(string route, IDictionary<string, object>? routeParameters = null)\n    {\n        return Task.CompletedTask;\n    }\n\n    public Task PopAsync()\n    {\n        return Task.CompletedTask;\n    }\n}\n\n"
  },
  {
    "path": "tests/ClientApp.UnitTests/Mocks/MockSettingsService.cs",
    "content": "﻿using eShop.ClientApp.Models.Token;\n\nnamespace ClientApp.UnitTests.Mocks;\n\npublic class MockSettingsService : ISettingsService\n{\n    private const string AccessToken = \"access_token\";\n    private const string IdToken = \"id_token\";\n    private const string IdUseMocks = \"use_mocks\";\n    private const string IdIdentityBase = \"url_base\";\n    private const string IdGatewayMarketingBase = \"url_marketing\";\n    private const string IdGatewayShoppingBase = \"url_shopping\";\n    private const string IdUseFakeLocation = \"use_fake_location\";\n    private const string IdLatitude = \"latitude\";\n    private const string IdLongitude = \"longitude\";\n    private const string IdAllowGpsLocation = \"allow_gps_location\";\n    \n    private const string AccessTokenDefault = \"default_access_token\";\n    private const string IdTokenDefault = \"\";\n    private const bool UseMocksDefault = true;\n    private const bool UseFakeLocationDefault = false;\n    private const bool AllowGpsLocationDefault = false;\n    private const double FakeLatitudeDefault = 47.604610d;\n    private const double FakeLongitudeDefault = -122.315752d;\n    private const string UrlIdentityDefault = \"https://13.88.8.119\";\n    private const string UrlGatewayMarketingDefault = \"https://13.88.8.119\";\n    private const string UrlGatewayShoppingDefault = \"https://13.88.8.119\";\n    \n    private readonly IDictionary<string, object> _settings = new Dictionary<string, object>();\n    private UserToken? _userToken;\n\n    public string AuthAccessToken\n    {\n        get => GetValueOrDefault(AccessToken, AccessTokenDefault);\n        set => AddOrUpdateValue(AccessToken, value);\n    }\n\n    public string AuthIdToken\n    {\n        get => GetValueOrDefault(IdToken, IdTokenDefault);\n        set => AddOrUpdateValue(IdToken, value);\n    }\n\n    public Task<UserToken?> GetUserTokenAsync()\n    {\n        return Task.FromResult(_userToken);\n    }\n\n    public Task SetUserTokenAsync(UserToken? userToken)\n    {\n        _userToken = userToken;\n\n        return Task.CompletedTask;\n    }\n\n    public bool UseMocks\n    {\n        get => GetValueOrDefault(IdUseMocks, UseMocksDefault);\n        set => AddOrUpdateValue(IdUseMocks, value);\n    }\n\n    public string DefaultEndpoint\n    {\n        get => GetValueOrDefault(nameof(DefaultEndpoint), string.Empty);\n        set => AddOrUpdateValue(nameof(DefaultEndpoint), value);\n    }\n    \n    public string RegistrationEndpoint\n    {\n        get => GetValueOrDefault(nameof(RegistrationEndpoint), string.Empty);\n        set => AddOrUpdateValue(nameof(RegistrationEndpoint), value);\n    }\n    \n    public string AuthorizeEndpoint\n    {\n        get => GetValueOrDefault(nameof(AuthorizeEndpoint), string.Empty);\n        set => AddOrUpdateValue(nameof(AuthorizeEndpoint), value);\n    }\n    \n    public string UserInfoEndpoint\n    {\n        get => GetValueOrDefault(nameof(UserInfoEndpoint), string.Empty);\n        set => AddOrUpdateValue(nameof(UserInfoEndpoint), value);\n    }\n    \n    public string ClientId\n    {\n        get => GetValueOrDefault(nameof(ClientId), string.Empty);\n        set => AddOrUpdateValue(nameof(ClientId), value);\n    }\n    \n    public string ClientSecret\n    {\n        get => GetValueOrDefault(nameof(ClientSecret), string.Empty);\n        set => AddOrUpdateValue(nameof(ClientSecret), value);\n    }\n    \n    public string CallbackUri\n    {\n        get => GetValueOrDefault(nameof(CallbackUri), string.Empty);\n        set => AddOrUpdateValue(nameof(CallbackUri), value);\n    }\n\n    public string IdentityEndpointBase\n    {\n        get => GetValueOrDefault(IdIdentityBase, UrlIdentityDefault);\n        set => AddOrUpdateValue(IdIdentityBase, value);\n    }\n\n    public string GatewayCatalogEndpointBase\n    {\n        get => GetValueOrDefault(nameof(GatewayCatalogEndpointBase), string.Empty);\n        set => AddOrUpdateValue(nameof(GatewayCatalogEndpointBase), value);\n    }\n    \n    public string GatewayOrdersEndpointBase\n    {\n        get => GetValueOrDefault(nameof(GatewayOrdersEndpointBase), string.Empty);\n        set => AddOrUpdateValue(nameof(GatewayOrdersEndpointBase), value);\n    }\n    \n    public string GatewayBasketEndpointBase\n    {\n        get => GetValueOrDefault(nameof(GatewayBasketEndpointBase), string.Empty);\n        set => AddOrUpdateValue(nameof(GatewayBasketEndpointBase), value);\n    }\n\n    public string GatewayShoppingEndpointBase\n    {\n        get => GetValueOrDefault(IdGatewayShoppingBase, UrlGatewayShoppingDefault);\n        set => AddOrUpdateValue(IdGatewayShoppingBase, value);\n    }\n\n    public string GatewayMarketingEndpointBase\n    {\n        get => GetValueOrDefault(IdGatewayMarketingBase, UrlGatewayMarketingDefault);\n        set => AddOrUpdateValue(IdGatewayMarketingBase, value);\n    }\n\n    public bool UseFakeLocation\n    {\n        get => GetValueOrDefault(IdUseFakeLocation, UseFakeLocationDefault);\n        set => AddOrUpdateValue(IdUseFakeLocation, value);\n    }\n\n    public string Latitude\n    {\n        get => GetValueOrDefault(IdLatitude, FakeLatitudeDefault.ToString());\n        set => AddOrUpdateValue(IdLatitude, value);\n    }\n\n    public string Longitude\n    {\n        get => GetValueOrDefault(IdLongitude, FakeLongitudeDefault.ToString());\n        set => AddOrUpdateValue(IdLongitude, value);\n    }\n\n    public bool AllowGpsLocation\n    {\n        get => GetValueOrDefault(IdAllowGpsLocation, AllowGpsLocationDefault);\n        set => AddOrUpdateValue(IdAllowGpsLocation, value);\n    }\n\n    public void AddOrUpdateValue(string key, bool value) => AddOrUpdateValueInternal(key, value);\n    public void AddOrUpdateValue(string key, string value) => AddOrUpdateValueInternal(key, value);\n    public bool GetValueOrDefault(string key, bool defaultValue) => GetValueOrDefaultInternal(key, defaultValue);\n    public string GetValueOrDefault(string key, string defaultValue) => GetValueOrDefaultInternal(key, defaultValue);\n\n    void AddOrUpdateValueInternal<T>(string key, T value)\n    {\n        if (value is null)\n        {\n            Remove(key);\n        }\n        else\n        {\n            _settings[key] = value;\n        }\n    }\n\n    T GetValueOrDefaultInternal<T>(string key, T defaultValue = default!) =>\n        _settings.TryGetValue(key, out object? value)\n            ? null != value ? (T)value : defaultValue : defaultValue;\n\n    void Remove(string key)\n    {\n        if (_settings.ContainsKey(key))\n        {\n            _settings.Remove(key);\n        }\n    }\n}\n"
  },
  {
    "path": "tests/ClientApp.UnitTests/Mocks/MockViewModel.cs",
    "content": "﻿using eShop.ClientApp.Validations;\nusing eShop.ClientApp.ViewModels.Base;\n\nnamespace ClientApp.UnitTests.Mocks;\n\npublic class MockViewModel : ViewModelBase\n{\n    public ValidatableObject<string> Forename { get; } = new();\n\n    public ValidatableObject<string> Surname { get; } = new();\n\n    public MockViewModel(INavigationService navigationService)\n        : base(navigationService)\n    {\n        Forename = new ValidatableObject<string>();\n        Surname = new ValidatableObject<string>();\n\n        Forename.Validations.Add(new IsNotNullOrEmptyRule<string> { ValidationMessage = \"Forename is required.\" });\n        Surname.Validations.Add(new IsNotNullOrEmptyRule<string> { ValidationMessage = \"Surname name is required.\" });\n    }\n\n    public bool Validate()\n    {\n        bool isValidForename = Forename.Validate();\n        bool isValidSurname = Surname.Validate();\n        return isValidForename && isValidSurname;\n    }\n}\n"
  },
  {
    "path": "tests/ClientApp.UnitTests/Services/BasketServiceTests.cs",
    "content": "﻿namespace ClientApp.UnitTests.Services;\n\n[TestClass]\npublic class BasketServiceTests\n{\n    [TestMethod]\n    public async Task GetFakeBasketTest()\n    {\n        var catalogMockService = new CatalogMockService();\n        var result = await catalogMockService.GetCatalogAsync();\n        Assert.AreNotEqual(0, result.Count());\n    }\n}\n"
  },
  {
    "path": "tests/ClientApp.UnitTests/Services/CatalogServiceTests.cs",
    "content": "﻿namespace ClientApp.UnitTests.Services;\n\n[TestClass]\npublic class CatalogServiceTests\n{\n    [TestMethod]\n    public async Task GetFakeCatalogTest()\n    {\n        var catalogMockService = new CatalogMockService();\n        var catalog = await catalogMockService.GetCatalogAsync();\n\n        Assert.AreNotEqual(0, catalog.Count());\n    }\n\n    [TestMethod]\n    public async Task GetFakeCatalogBrandTest()\n    {\n        var catalogMockService = new CatalogMockService();\n        var catalogBrand = await catalogMockService.GetCatalogBrandAsync();\n\n        Assert.AreNotEqual(0, catalogBrand.Count());\n    }\n\n    [TestMethod]\n    public async Task GetFakeCatalogTypeTest()\n    {\n        var catalogMockService = new CatalogMockService();\n        var catalogType = await catalogMockService.GetCatalogTypeAsync();\n\n        Assert.AreNotEqual(0, catalogType.Count());\n    }\n}\n"
  },
  {
    "path": "tests/ClientApp.UnitTests/Services/OrdersServiceTests.cs",
    "content": "﻿using ClientApp.UnitTests.Mocks;\n\nnamespace ClientApp.UnitTests.Services;\n\n[TestClass]\npublic class OrdersServiceTests\n{\n    private readonly ISettingsService _settingsService;\n\n    public OrdersServiceTests()\n    {\n        _settingsService = new MockSettingsService();\n    }\n    \n    [TestMethod]\n    public async Task GetFakeOrderTest()\n    {\n        var ordersMockService = new OrderMockService();\n        var order = await ordersMockService.GetOrderAsync(1);\n\n        Assert.IsNotNull(order);\n    }\n\n    [TestMethod]\n    public async Task GetFakeOrdersTest()\n    {\n        var ordersMockService = new OrderMockService();\n        var result = await ordersMockService.GetOrdersAsync();\n\n        Assert.AreNotEqual(0, result.Count());\n    }\n}\n"
  },
  {
    "path": "tests/ClientApp.UnitTests/TestingExtensions.cs",
    "content": "﻿using System.Windows.Input;\nusing CommunityToolkit.Mvvm.Input;\n\nnamespace ClientApp.UnitTests;\n\npublic static class TestingExtensions\n{\n    public static async Task ExecuteUntilComplete(this ICommand command, object? parameter = null)\n    {\n        if (command is IAsyncRelayCommand arc)\n        {\n            await arc.ExecuteAsync(parameter);\n            return;\n        }\n\n        command.Execute(parameter);\n    }\n}\n\n"
  },
  {
    "path": "tests/ClientApp.UnitTests/ViewModels/CatalogItemViewModelTests.cs",
    "content": "﻿using ClientApp.UnitTests.Mocks;\nusing CommunityToolkit.Mvvm.Messaging;\nusing eShop.ClientApp.Messages;\nusing eShop.ClientApp.Models.Catalog;\nusing eShop.ClientApp.Services.Identity;\n\nnamespace ClientApp.UnitTests.ViewModels;\n\n[TestClass]\npublic class CatalogItemViewModelTests\n{\n    private readonly INavigationService _navigationService;\n    private readonly IAppEnvironmentService _appEnvironmentService;\n\n    public CatalogItemViewModelTests()\n    {\n        _navigationService = new MockNavigationService();\n        var mockCatalogService = new CatalogMockService();\n        var mockOrderService = new OrderMockService();\n        var mockIdentityService = new IdentityMockService();\n        var mockBasketService = new BasketMockService();\n\n        _appEnvironmentService =\n            new AppEnvironmentService(\n                mockBasketService, mockBasketService,\n                mockCatalogService, mockCatalogService,\n                mockOrderService, mockOrderService,\n                mockIdentityService, mockIdentityService);\n\n        _appEnvironmentService.UpdateDependencies(true);\n    }\n\n    [TestMethod]\n    public void AddCatalogItemCommandIsNotNullTest()\n    {\n        var CatalogItemViewModel = new CatalogItemViewModel(_appEnvironmentService, _navigationService);\n        Assert.IsNotNull(CatalogItemViewModel.AddCatalogItemCommand);\n    }\n    \n    [TestMethod]\n    public async Task AddCatalogItemCommandSendsAddProductMessageTest()\n    {\n        bool messageReceived = false;\n\n        var catalogItemViewModel = new CatalogItemViewModel(_appEnvironmentService, _navigationService);\n\n        catalogItemViewModel.CatalogItem = new CatalogItem {Id = 123, Name = \"test\", Price = 1.23m,};\n        \n        WeakReferenceMessenger.Default\n            .Register<CatalogItemViewModelTests, ProductCountChangedMessage>(\n                this,\n                (_, message) =>\n                {\n                    messageReceived = true;\n                });\n        \n        await catalogItemViewModel.AddCatalogItemCommand.ExecuteUntilComplete();\n\n        Assert.IsTrue(messageReceived);\n    }\n\n}\n"
  },
  {
    "path": "tests/ClientApp.UnitTests/ViewModels/CatalogViewModelTests.cs",
    "content": "﻿using ClientApp.UnitTests.Mocks;\nusing eShop.ClientApp.Services.Identity;\n\nnamespace ClientApp.UnitTests.ViewModels;\n\n[TestClass]\npublic class CatalogViewModelTests\n{\n    private readonly INavigationService _navigationService;\n    private readonly IAppEnvironmentService _appEnvironmentService;\n\n    public CatalogViewModelTests()\n    {\n        _navigationService = new MockNavigationService();\n\n        var mockBasketService = new BasketMockService();\n        var mockCatalogService = new CatalogMockService();\n        var mockOrderService = new OrderMockService();\n        var mockIdentityService = new IdentityMockService();\n\n        _appEnvironmentService =\n            new AppEnvironmentService(\n                mockBasketService, mockBasketService,\n                mockCatalogService, mockCatalogService,\n                mockOrderService, mockOrderService,\n                mockIdentityService, mockIdentityService);\n\n        _appEnvironmentService.UpdateDependencies(true);\n    }\n    \n    [TestMethod]\n    public void FilterCommandIsNotNullTest()\n    {\n        var catalogViewModel = new CatalogViewModel(_appEnvironmentService, _navigationService);\n        Assert.IsNotNull(catalogViewModel.FilterCommand);\n    }\n\n    [TestMethod]\n    public void ClearFilterCommandIsNotNullTest()\n    {\n        var catalogViewModel = new CatalogViewModel(_appEnvironmentService, _navigationService);\n        Assert.IsNotNull(catalogViewModel.ClearFilterCommand);\n    }\n\n    [TestMethod]\n    public void ProductsPropertyIsEmptyWhenViewModelInstantiatedTest()\n    {\n        var catalogViewModel = new CatalogViewModel(_appEnvironmentService, _navigationService);\n        Assert.IsEmpty(catalogViewModel.Products);\n    }\n\n    [TestMethod]\n    public void BrandsPropertyIsEmptyWhenViewModelInstantiatedTest()\n    {\n        var catalogViewModel = new CatalogViewModel(_appEnvironmentService, _navigationService);\n        Assert.IsEmpty(catalogViewModel.Brands);\n    }\n\n    [TestMethod]\n    public void BrandPropertyIsNullWhenViewModelInstantiatedTest()\n    {\n        var catalogViewModel = new CatalogViewModel(_appEnvironmentService, _navigationService);\n        Assert.IsNull(catalogViewModel.SelectedBrand);\n    }\n\n    [TestMethod]\n    public void TypesPropertyIsEmptyWhenViewModelInstantiatedTest()\n    {\n        var catalogViewModel = new CatalogViewModel(_appEnvironmentService, _navigationService);\n        Assert.IsEmpty(catalogViewModel.Types);\n    }\n\n    [TestMethod]\n    public void TypePropertyIsNullWhenViewModelInstantiatedTest()\n    {\n        var catalogViewModel = new CatalogViewModel(_appEnvironmentService, _navigationService);\n        Assert.IsNull(catalogViewModel.SelectedType);\n    }\n\n    [TestMethod]\n    public void IsFilterPropertyIsFalseWhenViewModelInstantiatedTest()\n    {\n        var catalogViewModel = new CatalogViewModel(_appEnvironmentService, _navigationService);\n        Assert.IsFalse(catalogViewModel.IsFiltering);\n    }\n\n    [TestMethod]\n    public async Task ProductsPropertyIsNotNullAfterViewModelInitializationTest()\n    {\n        var catalogViewModel = new CatalogViewModel(_appEnvironmentService, _navigationService);\n\n        await catalogViewModel.InitializeAsync();\n\n        Assert.IsNotNull(catalogViewModel.Products);\n    }\n\n    [TestMethod]\n    public async Task BrandsPropertyIsNotNullAfterViewModelInitializationTest()\n    {\n        var catalogViewModel = new CatalogViewModel(_appEnvironmentService, _navigationService);\n\n        await catalogViewModel.InitializeAsync();\n\n        Assert.IsNotNull(catalogViewModel.Brands);\n    }\n\n    [TestMethod]\n    public async Task TypesPropertyIsNotNullAfterViewModelInitializationTest()\n    {\n        var catalogViewModel = new CatalogViewModel(_appEnvironmentService, _navigationService);\n\n        await catalogViewModel.InitializeAsync();\n\n        Assert.IsNotNull(catalogViewModel.Types);\n    }\n\n    [TestMethod]\n    public async Task SettingBadgeCountPropertyShouldRaisePropertyChanged()\n    {\n        bool invoked = false;\n\n        var catalogViewModel = new CatalogViewModel(_appEnvironmentService, _navigationService);\n\n        catalogViewModel.PropertyChanged += (_, e) =>\n        {\n            if (e?.PropertyName?.Equals(nameof(CatalogViewModel.BadgeCount)) ?? false)\n            {\n                invoked = true;\n            }\n        };\n        await catalogViewModel.InitializeAsync();\n\n        Assert.IsTrue(invoked);\n    }\n\n    [TestMethod]\n    public async Task ClearFilterCommandResetsPropertiesTest()\n    {\n        var catalogViewModel = new CatalogViewModel(_appEnvironmentService, _navigationService);\n\n        await catalogViewModel.InitializeAsync();\n        await catalogViewModel.ClearFilterCommand.ExecuteUntilComplete(null);\n\n        Assert.IsNull(catalogViewModel.SelectedBrand);\n        Assert.IsNull(catalogViewModel.SelectedType);\n        Assert.IsNotNull(catalogViewModel.Products);\n    }\n}\n"
  },
  {
    "path": "tests/ClientApp.UnitTests/ViewModels/MainViewModelTests.cs",
    "content": "﻿using ClientApp.UnitTests.Mocks;\n\nnamespace ClientApp.UnitTests.ViewModels;\n\n[TestClass]\npublic class MainViewModelTests\n{\n    private readonly INavigationService _navigationService;\n\n    public MainViewModelTests()\n    {\n        _navigationService = new MockNavigationService();\n    }\n\n    [TestMethod]\n    public void SettingsCommandIsNotNullWhenViewModelInstantiatedTest()\n    {\n        var mainViewModel = new MainViewModel(_navigationService);\n        Assert.IsNotNull(mainViewModel.SettingsCommand);\n    }\n\n    [TestMethod]\n    public void IsBusyPropertyIsFalseWhenViewModelInstantiatedTest()\n    {\n        var mainViewModel = new MainViewModel(_navigationService);\n        Assert.IsFalse(mainViewModel.IsBusy);\n    }\n}\n"
  },
  {
    "path": "tests/ClientApp.UnitTests/ViewModels/MockViewModelTests.cs",
    "content": "﻿using ClientApp.UnitTests.Mocks;\n\nnamespace ClientApp.UnitTests.ViewModels;\n\n[TestClass]\npublic class MockViewModelTests\n{\n    private readonly INavigationService _navigationService;\n\n    public MockViewModelTests()\n    {\n        _navigationService = new MockNavigationService();\n    }\n\n    [TestMethod]\n    public void CheckValidationFailsWhenPropertiesAreEmptyTest()\n    {\n        var mockViewModel = new MockViewModel(_navigationService);\n\n        bool isValid = mockViewModel.Validate();\n\n        Assert.IsFalse(isValid);\n        Assert.IsNull(mockViewModel.Forename.Value);\n        Assert.IsNull(mockViewModel.Surname.Value);\n        Assert.IsFalse(mockViewModel.Forename.IsValid);\n        Assert.IsFalse(mockViewModel.Surname.IsValid);\n        Assert.AreNotEqual(0, mockViewModel.Forename.Errors.Count());\n        Assert.AreNotEqual(0, mockViewModel.Surname.Errors.Count());\n    }\n\n    [TestMethod]\n    public void CheckValidationFailsWhenOnlyForenameHasDataTest()\n    {\n        var mockViewModel = new MockViewModel(_navigationService);\n        mockViewModel.Forename.Value = \"John\";\n\n        bool isValid = mockViewModel.Validate();\n\n        Assert.IsFalse(isValid);\n        Assert.IsNotNull(mockViewModel.Forename.Value);\n        Assert.IsNull(mockViewModel.Surname.Value);\n        Assert.IsTrue(mockViewModel.Forename.IsValid);\n        Assert.IsFalse(mockViewModel.Surname.IsValid);\n        Assert.AreEqual(0, mockViewModel.Forename.Errors.Count());\n        Assert.AreNotEqual(0, mockViewModel.Surname.Errors.Count());\n    }\n\n    [TestMethod]\n    public void CheckValidationPassesWhenOnlySurnameHasDataTest()\n    {\n        var mockViewModel = new MockViewModel(_navigationService);\n        mockViewModel.Surname.Value = \"Smith\";\n\n        bool isValid = mockViewModel.Validate();\n\n        Assert.IsFalse(isValid);\n        Assert.IsNull(mockViewModel.Forename.Value);\n        Assert.IsNotNull(mockViewModel.Surname.Value);\n        Assert.IsFalse(mockViewModel.Forename.IsValid);\n        Assert.IsTrue(mockViewModel.Surname.IsValid);\n        Assert.AreNotEqual(0, mockViewModel.Forename.Errors.Count());\n        Assert.AreEqual(0, mockViewModel.Surname.Errors.Count());\n    }\n\n    [TestMethod]\n    public void CheckValidationPassesWhenBothPropertiesHaveDataTest()\n    {\n        var mockViewModel = new MockViewModel(_navigationService);\n        mockViewModel.Forename.Value = \"John\";\n        mockViewModel.Surname.Value = \"Smith\";\n\n        bool isValid = mockViewModel.Validate();\n\n        Assert.IsTrue(isValid);\n        Assert.IsNotNull(mockViewModel.Forename.Value);\n        Assert.IsNotNull(mockViewModel.Surname.Value);\n        Assert.IsTrue(mockViewModel.Forename.IsValid);\n        Assert.IsTrue(mockViewModel.Surname.IsValid);\n        Assert.AreEqual(0, mockViewModel.Forename.Errors.Count());\n        Assert.AreEqual(0, mockViewModel.Surname.Errors.Count());\n    }\n\n    [TestMethod]\n    public void SettingForenamePropertyShouldRaisePropertyChanged()\n    {\n        bool invoked = false;\n        var mockViewModel = new MockViewModel(_navigationService);\n\n        mockViewModel.Forename.PropertyChanged += (_, e) =>\n        {\n            if (e?.PropertyName?.Equals(nameof(mockViewModel.Forename.Value)) ?? false)\n            {\n                invoked = true;\n            }\n        };\n        mockViewModel.Forename.Value = \"John\";\n\n        Assert.IsTrue(invoked);\n    }\n\n    [TestMethod]\n    public void SettingSurnamePropertyShouldRaisePropertyChanged()\n    {\n        bool invoked = false;\n        var mockViewModel = new MockViewModel(_navigationService);\n\n        mockViewModel.Surname.PropertyChanged += (_, e) =>\n        {\n            if (e?.PropertyName?.Equals(nameof(mockViewModel.Surname.Value)) ?? false)\n            {\n                invoked = true;\n            }\n        };\n        mockViewModel.Surname.Value = \"Smith\";\n\n        Assert.IsTrue(invoked);\n    }\n}\n"
  },
  {
    "path": "tests/ClientApp.UnitTests/ViewModels/OrderViewModelTests.cs",
    "content": "﻿using ClientApp.UnitTests.Mocks;\nusing eShop.ClientApp.Services.Identity;\n\nnamespace ClientApp.UnitTests.ViewModels;\n\n[TestClass]\npublic class OrderViewModelTests\n{\n    private readonly INavigationService _navigationService;\n    private readonly ISettingsService _settingsService;\n    private readonly IAppEnvironmentService _appEnvironmentService;\n\n    public OrderViewModelTests()\n    {\n        _navigationService = new MockNavigationService();\n        _settingsService = new MockSettingsService();\n\n        var mockBasketService = new BasketMockService();\n        var mockCatalogService = new CatalogMockService();\n        var mockOrderService = new OrderMockService();\n        var mockIdentityService = new IdentityMockService();\n\n        _appEnvironmentService =\n            new AppEnvironmentService(\n                mockBasketService, mockBasketService,\n                mockCatalogService, mockCatalogService,\n                mockOrderService, mockOrderService,\n                mockIdentityService, mockIdentityService);\n\n        _appEnvironmentService.UpdateDependencies(true);\n    }\n\n    [TestMethod]\n    public void OrderPropertyIsNullWhenViewModelInstantiatedTest()\n    {\n        var orderViewModel = new OrderDetailViewModel(_appEnvironmentService, _navigationService, _settingsService);\n        Assert.IsNull(orderViewModel.Order);\n    }\n\n    [TestMethod]\n    public async Task OrderPropertyIsNotNullAfterViewModelInitializationTest()\n    {\n        var orderViewModel = new OrderDetailViewModel(_appEnvironmentService, _navigationService, _settingsService);\n\n        var order = await _appEnvironmentService.OrderService.GetOrderAsync(1);\n\n        orderViewModel.OrderNumber = order.OrderNumber;\n        await orderViewModel.InitializeAsync();\n\n        Assert.IsNotNull(orderViewModel.Order);\n    }\n\n    [TestMethod]\n    public async Task SettingOrderPropertyShouldRaisePropertyChanged()\n    {\n        bool invoked = false;\n\n        var orderViewModel = new OrderDetailViewModel(_appEnvironmentService, _navigationService, _settingsService);\n\n        orderViewModel.PropertyChanged += (_, e) =>\n        {\n            if (e?.PropertyName?.Equals(nameof(OrderDetailViewModel.Order)) ?? false)\n            {\n                invoked = true;\n            }\n        };\n        var order = await _appEnvironmentService.OrderService.GetOrderAsync(1);\n        orderViewModel.OrderNumber = order.OrderNumber;\n        await orderViewModel.InitializeAsync();\n\n        Assert.IsTrue(invoked);\n    }\n}\n"
  },
  {
    "path": "tests/Directory.Build.props",
    "content": "<Project>\n\n  <!-- MSTest settings -->\n  <PropertyGroup>\n    <MSTestAnalysisMode>Recommended</MSTestAnalysisMode>\n  </PropertyGroup>\n\n  <!-- xUnit settings -->\n  <PropertyGroup>\n    <UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>\n  </PropertyGroup>\n\n</Project>"
  },
  {
    "path": "tests/Ordering.FunctionalTests/AutoAuthorizeMiddleware.cs",
    "content": "﻿namespace eShop.Ordering.FunctionalTests;\n\nclass AutoAuthorizeMiddleware\n{\n    public const string IDENTITY_ID = \"9e3163b9-1ae6-4652-9dc6-7898ab7b7a00\";\n\n    private readonly RequestDelegate _next;\n\n    public AutoAuthorizeMiddleware(RequestDelegate rd)\n    {\n        _next = rd;\n    }\n\n    public async Task Invoke(HttpContext httpContext)\n    {\n        var identity = new ClaimsIdentity(\"cookies\");\n\n        identity.AddClaim(new Claim(\"sub\", IDENTITY_ID));\n        identity.AddClaim(new Claim(\"unique_name\", IDENTITY_ID));\n        identity.AddClaim(new Claim(ClaimTypes.Name, IDENTITY_ID));\n\n        httpContext.User.AddIdentity(identity);\n\n        await _next.Invoke(httpContext);\n    }\n}\n"
  },
  {
    "path": "tests/Ordering.FunctionalTests/GlobalUsings.cs",
    "content": "﻿global using System;\nglobal using System.Collections.Generic;\nglobal using System.Linq;\nglobal using System.Net.Http;\nglobal using System.Security.Claims;\nglobal using System.Threading.Tasks;\nglobal using Microsoft.AspNetCore.Builder;\nglobal using Microsoft.AspNetCore.Hosting;\nglobal using Microsoft.AspNetCore.Http;\nglobal using Microsoft.AspNetCore.TestHost;\nglobal using Microsoft.Extensions.Configuration;\nglobal using Microsoft.Extensions.DependencyInjection;\nglobal using Xunit;\n"
  },
  {
    "path": "tests/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj",
    "content": "﻿<Project Sdk=\"Aspire.AppHost.Sdk/13.0.0\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <IsPublishable>false</IsPublishable>\n    <IsPackable>false</IsPackable>\n    <OutputType>Exe</OutputType>\n  </PropertyGroup>\n  \n  <ItemGroup>\n    <PackageReference Include=\"Asp.Versioning.Http.Client\" />\n    <PackageReference Include=\"Aspire.Hosting.PostgreSQL\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Mvc.Testing\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.TestHost\" />\n    <PackageReference Include=\"xunit.v3.mtp-v2\" />\n  </ItemGroup>\n \n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Ordering.API\\Ordering.API.csproj\" IsAspireProjectResource=\"false\" />\n    <ProjectReference Include=\"..\\..\\src\\Identity.API\\Identity.API.csproj\" />\n    <ProjectReference Include=\"..\\..\\src\\Ordering.Domain\\Ordering.Domain.csproj\" IsAspireProjectResource=\"false\" />\n    <ProjectReference Include=\"..\\..\\src\\Ordering.Infrastructure\\Ordering.Infrastructure.csproj\" IsAspireProjectResource=\"false\" />\n  </ItemGroup>\n\n</Project>"
  },
  {
    "path": "tests/Ordering.FunctionalTests/OrderingApiFixture.cs",
    "content": "﻿using Aspire.Hosting;\nusing Aspire.Hosting.ApplicationModel;\n\nusing Microsoft.AspNetCore.Mvc.Testing;\nusing Microsoft.Extensions.Hosting;\n\nnamespace eShop.Ordering.FunctionalTests;\n\npublic sealed class OrderingApiFixture : WebApplicationFactory<Program>, IAsyncLifetime\n{\n    private readonly IHost _app;\n\n    public IResourceBuilder<PostgresServerResource> Postgres { get; private set; }\n    public IResourceBuilder<PostgresServerResource> IdentityDB { get; private set; }\n    public IResourceBuilder<ProjectResource> IdentityApi { get; private set; }\n\n    private string _postgresConnectionString;\n\n    public OrderingApiFixture()\n    {\n        var options = new DistributedApplicationOptions { AssemblyName = typeof(OrderingApiFixture).Assembly.FullName, DisableDashboard = true };\n        var appBuilder = DistributedApplication.CreateBuilder(options);\n        Postgres = appBuilder.AddPostgres(\"OrderingDB\");\n        IdentityDB = appBuilder.AddPostgres(\"IdentityDB\");\n        IdentityApi = appBuilder.AddProject<Projects.Identity_API>(\"identity-api\").WithReference(IdentityDB);\n        _app = appBuilder.Build();\n    }\n\n    protected override IHost CreateHost(IHostBuilder builder)\n    {\n        builder.ConfigureHostConfiguration(config =>\n        {\n            config.AddInMemoryCollection(new Dictionary<string, string>\n            {\n                { $\"ConnectionStrings:{Postgres.Resource.Name}\", _postgresConnectionString },\n                { \"Identity:Url\", IdentityApi.GetEndpoint(\"http\").Url }\n            });\n        });\n        builder.ConfigureServices(services =>\n        {\n            services.AddSingleton<IStartupFilter>(new AutoAuthorizeStartupFilter());\n        });\n        return base.CreateHost(builder);\n    }\n\n    public new async Task DisposeAsync()\n    {\n        await base.DisposeAsync();\n        await _app.StopAsync();\n        if (_app is IAsyncDisposable asyncDisposable)\n        {\n            await asyncDisposable.DisposeAsync().ConfigureAwait(false);\n        }\n        else\n        {\n            _app.Dispose();\n        }\n    }\n\n    public async ValueTask InitializeAsync()\n    {\n        await _app.StartAsync();\n        _postgresConnectionString = await Postgres.Resource.GetConnectionStringAsync();\n    }\n\n    private class AutoAuthorizeStartupFilter : IStartupFilter\n    {\n        public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)\n        {\n            return builder =>\n            {\n                builder.UseMiddleware<AutoAuthorizeMiddleware>();\n                next(builder);\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "tests/Ordering.FunctionalTests/OrderingApiTests.cs",
    "content": "﻿using System.Net;\nusing System.Text;\nusing System.Text.Json;\nusing Asp.Versioning;\nusing Asp.Versioning.Http;\nusing eShop.Ordering.API.Application.Commands;\nusing eShop.Ordering.API.Application.Models;\nusing eShop.Ordering.API.Application.Queries;\nusing Microsoft.AspNetCore.Mvc.Testing;\n\nnamespace eShop.Ordering.FunctionalTests;\n\npublic sealed class OrderingApiTests : IClassFixture<OrderingApiFixture>\n{\n    private readonly WebApplicationFactory<Program> _webApplicationFactory;\n    private readonly HttpClient _httpClient;\n\n    public OrderingApiTests(OrderingApiFixture fixture)\n    {\n        var handler = new ApiVersionHandler(new QueryStringApiVersionWriter(), new ApiVersion(1.0));\n\n        _webApplicationFactory = fixture;\n        _httpClient = _webApplicationFactory.CreateDefaultClient(handler);\n    }\n\n    [Fact]\n    public async Task GetAllStoredOrdersWorks()\n    {\n        // Act\n        var response = await _httpClient.GetAsync(\"api/orders\", TestContext.Current.CancellationToken);\n        var s = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        response.EnsureSuccessStatusCode();\n\n        // Assert\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task CancelWithEmptyGuidFails()\n    {\n        // Act\n        var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, \"application/json\")\n        {\n            Headers = { { \"x-requestid\", Guid.Empty.ToString() } }\n        };\n        var response = await _httpClient.PutAsync(\"/api/orders/cancel\", content, TestContext.Current.CancellationToken);\n        var s = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n\n        // Assert\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task CancelNonExistentOrderFails()\n    {\n        // Act\n        var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, \"application/json\")\n        {\n            Headers = { { \"x-requestid\", Guid.NewGuid().ToString() } }\n        };\n        var response = await _httpClient.PutAsync(\"api/orders/cancel\", content, TestContext.Current.CancellationToken);\n        var s = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n\n        // Assert\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task ShipWithEmptyGuidFails()\n    {\n        // Act\n        var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, \"application/json\")\n        {\n            Headers = { { \"x-requestid\", Guid.Empty.ToString() } }\n        };\n        var response = await _httpClient.PutAsync(\"api/orders/ship\", content, TestContext.Current.CancellationToken);\n        var s = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n\n        // Assert\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task ShipNonExistentOrderFails()\n    {\n        // Act\n        var content = new StringContent(BuildOrder(), UTF8Encoding.UTF8, \"application/json\")\n        {\n            Headers = { { \"x-requestid\", Guid.NewGuid().ToString() } }\n        };\n        var response = await _httpClient.PutAsync(\"api/orders/ship\", content, TestContext.Current.CancellationToken);\n\n        // Assert\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task GetAllOrdersCardType()\n    {\n        // Act 1\n        var response = await _httpClient.GetAsync(\"api/orders/cardtypes\", TestContext.Current.CancellationToken);\n        var s = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        response.EnsureSuccessStatusCode();\n\n        // Assert\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task GetStoredOrdersWithOrderId()\n    {\n        // Act\n        var response = await _httpClient.GetAsync(\"api/orders/1\", TestContext.Current.CancellationToken);\n        var responseStatus = response.StatusCode;\n\n        // Assert\n        Assert.Equal(\"NotFound\", responseStatus.ToString());\n    }\n\n    [Fact]\n    public async Task AddNewEmptyOrder()\n    {\n        // Act\n        var content = new StringContent(JsonSerializer.Serialize(new Order()), UTF8Encoding.UTF8, \"application/json\")\n        {\n            Headers = { { \"x-requestid\", Guid.Empty.ToString() } }\n        };\n        var response = await _httpClient.PostAsync(\"api/orders\", content, TestContext.Current.CancellationToken);\n        var s = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n\n        // Assert\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task AddNewOrder()\n    {\n        // Act\n        var item = new BasketItem\n        {\n            Id = \"1\",\n            ProductId = 12,\n            ProductName = \"Test\",\n            UnitPrice = 10,\n            OldUnitPrice = 9,\n            Quantity = 1,\n            PictureUrl = null\n        };\n        var cardExpirationDate = Convert.ToDateTime(\"2023-12-22T12:34:24.334Z\");\n        var OrderRequest = new CreateOrderRequest(\"1\", \"TestUser\", null, null, null, null, null, \"XXXXXXXXXXXX0005\", \"Test User\", cardExpirationDate, \"test buyer\", 1, null, new List<BasketItem> { item });\n        var content = new StringContent(JsonSerializer.Serialize(OrderRequest), UTF8Encoding.UTF8, \"application/json\")\n        {\n            Headers = { { \"x-requestid\", Guid.NewGuid().ToString() } }\n        };\n        var response = await _httpClient.PostAsync(\"api/orders\", content, TestContext.Current.CancellationToken);\n        var s = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n\n        // Assert\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task PostDraftOrder()\n    {\n        // Act\n        var item = new BasketItem\n        {\n            Id = \"1\",\n            ProductId = 12,\n            ProductName = \"Test\",\n            UnitPrice = 10,\n            OldUnitPrice = 9,\n            Quantity = 1,\n            PictureUrl = null\n        };\n        var bodyContent = new CustomerBasket(\"1\", new List<BasketItem> { item });\n        var content = new StringContent(JsonSerializer.Serialize(bodyContent), UTF8Encoding.UTF8, \"application/json\")\n        {\n            Headers = { { \"x-requestid\", Guid.NewGuid().ToString() } }\n        };\n        var response = await _httpClient.PostAsync(\"api/orders/draft\", content, TestContext.Current.CancellationToken);\n        var s = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n\n        // Assert\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task CreateOrderDraftSucceeds()\n    {\n        var payload = FakeOrderDraftCommand();\n        var content = new StringContent(JsonSerializer.Serialize(FakeOrderDraftCommand()), UTF8Encoding.UTF8, \"application/json\")\n        {\n            Headers = { { \"x-requestid\", Guid.NewGuid().ToString() } }\n        };\n        var response = await _httpClient.PostAsync(\"api/orders/draft\", content, TestContext.Current.CancellationToken);\n\n        var s = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);\n        var responseData = JsonSerializer.Deserialize<OrderDraftDTO>(s, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n        Assert.Equal(payload.Items.Count(), responseData.OrderItems.Count());\n        Assert.Equal(payload.Items.Sum(o => o.Quantity * o.UnitPrice), responseData.Total);\n        AssertThatOrderItemsAreTheSameAsRequestPayloadItems(payload, responseData);\n    }\n\n    private CreateOrderDraftCommand FakeOrderDraftCommand()\n    {\n        return new CreateOrderDraftCommand(\n            BuyerId: Guid.NewGuid().ToString(),\n            new List<BasketItem>()\n            {\n                new BasketItem()\n                {\n                    Id = Guid.NewGuid().ToString(),\n                    ProductId = 1,\n                    ProductName = \"Test Product 1\",\n                    UnitPrice = 10.2m,\n                    OldUnitPrice = 9.8m,\n                    Quantity = 2,\n                    PictureUrl = Guid.NewGuid().ToString(),\n                }\n            });\n    }\n\n    private static void AssertThatOrderItemsAreTheSameAsRequestPayloadItems(CreateOrderDraftCommand payload, OrderDraftDTO responseData)\n    {\n        // check that OrderItems contain all product Ids from the payload\n        var payloadItemsProductIds = payload.Items.Select(x => x.ProductId);\n        var orderItemsProductIds = responseData.OrderItems.Select(x => x.ProductId);\n        Assert.All(orderItemsProductIds, orderItemProdId => payloadItemsProductIds.Contains(orderItemProdId));\n        // TODO: might need to add more asserts in here\n    }\n\n    string BuildOrder()\n    {\n        var order = new\n        {\n            OrderNumber = \"-1\"\n        };\n        return JsonSerializer.Serialize(order);\n    }\n}\n"
  },
  {
    "path": "tests/Ordering.UnitTests/Application/IdentifiedCommandHandlerTest.cs",
    "content": "﻿namespace eShop.Ordering.UnitTests.Application;\n\n[TestClass]\npublic class IdentifiedCommandHandlerTest\n{\n    private readonly IRequestManager _requestManager;\n    private readonly IMediator _mediator;\n    private readonly ILogger<IdentifiedCommandHandler<CreateOrderCommand, bool>> _loggerMock;\n\n    public IdentifiedCommandHandlerTest()\n    {\n        _requestManager = Substitute.For<IRequestManager>();\n        _mediator = Substitute.For<IMediator>();\n        _loggerMock = Substitute.For<ILogger<IdentifiedCommandHandler<CreateOrderCommand, bool>>>();\n    }\n\n    [TestMethod]\n    public async Task Handler_sends_command_when_order_no_exists()\n    {\n        // Arrange\n        var fakeGuid = Guid.NewGuid();\n        var fakeOrderCmd = new IdentifiedCommand<CreateOrderCommand, bool>(FakeOrderRequest(), fakeGuid);\n\n        _requestManager.ExistAsync(Arg.Any<Guid>())\n            .Returns(Task.FromResult(false));\n\n        _mediator.Send(Arg.Any<IRequest<bool>>(), default)\n            .Returns(Task.FromResult(true));\n\n        // Act\n        var handler = new CreateOrderIdentifiedCommandHandler(_mediator, _requestManager, _loggerMock);\n        var result = await handler.Handle(fakeOrderCmd, CancellationToken.None);\n\n        // Assert\n        Assert.IsTrue(result);\n        await _mediator.Received().Send(Arg.Any<IRequest<bool>>(), default);\n    }\n\n    [TestMethod]\n    public async Task Handler_sends_no_command_when_order_already_exists()\n    {\n        // Arrange\n        var fakeGuid = Guid.NewGuid();\n        var fakeOrderCmd = new IdentifiedCommand<CreateOrderCommand, bool>(FakeOrderRequest(), fakeGuid);\n\n        _requestManager.ExistAsync(Arg.Any<Guid>())\n            .Returns(Task.FromResult(true));\n\n        _mediator.Send(Arg.Any<IRequest<bool>>(), default)\n            .Returns(Task.FromResult(true));\n\n        // Act\n        var handler = new CreateOrderIdentifiedCommandHandler(_mediator, _requestManager, _loggerMock);\n        var result = await handler.Handle(fakeOrderCmd, CancellationToken.None);\n\n        // Assert\n       await  _mediator.DidNotReceive().Send(Arg.Any<IRequest<bool>>(), default);\n    }\n\n    private CreateOrderCommand FakeOrderRequest(Dictionary<string, object> args = null)\n    {\n        return new CreateOrderCommand(\n            new List<BasketItem>(),\n            userId: args != null && args.ContainsKey(\"userId\") ? (string)args[\"userId\"] : null,\n            userName: args != null && args.ContainsKey(\"userName\") ? (string)args[\"userName\"] : null,\n            city: args != null && args.ContainsKey(\"city\") ? (string)args[\"city\"] : null,\n            street: args != null && args.ContainsKey(\"street\") ? (string)args[\"street\"] : null,\n            state: args != null && args.ContainsKey(\"state\") ? (string)args[\"state\"] : null,\n            country: args != null && args.ContainsKey(\"country\") ? (string)args[\"country\"] : null,\n            zipcode: args != null && args.ContainsKey(\"zipcode\") ? (string)args[\"zipcode\"] : null,\n            cardNumber: args != null && args.ContainsKey(\"cardNumber\") ? (string)args[\"cardNumber\"] : \"1234\",\n            cardExpiration: args != null && args.ContainsKey(\"cardExpiration\") ? (DateTime)args[\"cardExpiration\"] : DateTime.MinValue,\n            cardSecurityNumber: args != null && args.ContainsKey(\"cardSecurityNumber\") ? (string)args[\"cardSecurityNumber\"] : \"123\",\n            cardHolderName: args != null && args.ContainsKey(\"cardHolderName\") ? (string)args[\"cardHolderName\"] : \"XXX\",\n            cardTypeId: args != null && args.ContainsKey(\"cardTypeId\") ? (int)args[\"cardTypeId\"] : 0);\n    }\n}\n"
  },
  {
    "path": "tests/Ordering.UnitTests/Application/NewOrderCommandHandlerTest.cs",
    "content": "﻿using eShop.Ordering.API.Application.IntegrationEvents;\nusing eShop.Ordering.Domain.AggregatesModel.OrderAggregate;\n\nnamespace eShop.Ordering.UnitTests.Application;\n\n[TestClass]\npublic class NewOrderRequestHandlerTest\n{\n    private readonly IOrderRepository _orderRepositoryMock;\n    private readonly IIdentityService _identityServiceMock;\n    private readonly IMediator _mediator;\n    private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;\n\n    public NewOrderRequestHandlerTest()\n    {\n\n        _orderRepositoryMock = Substitute.For<IOrderRepository>();\n        _identityServiceMock = Substitute.For<IIdentityService>();\n        _orderingIntegrationEventService = Substitute.For<IOrderingIntegrationEventService>();\n        _mediator = Substitute.For<IMediator>();\n    }\n\n    [TestMethod]\n    public async Task Handle_return_false_if_order_is_not_persisted()\n    {\n        var buyerId = \"1234\";\n\n        var fakeOrderCmd = FakeOrderRequestWithBuyer(new Dictionary<string, object>\n        { [\"cardExpiration\"] = DateTime.UtcNow.AddYears(1) });\n\n        _orderRepositoryMock.GetAsync(Arg.Any<int>())\n            .Returns(Task.FromResult(FakeOrder()));\n\n        _orderRepositoryMock.UnitOfWork.SaveChangesAsync(default)\n            .Returns(Task.FromResult(1));\n\n        _identityServiceMock.GetUserIdentity().Returns(buyerId);\n\n        var LoggerMock = Substitute.For<ILogger<CreateOrderCommandHandler>>();\n        //Act\n        var handler = new CreateOrderCommandHandler(_mediator, _orderingIntegrationEventService, _orderRepositoryMock, _identityServiceMock, LoggerMock);\n        var cltToken = new CancellationToken();\n        var result = await handler.Handle(fakeOrderCmd, cltToken);\n\n        //Assert\n        Assert.IsFalse(result);\n    }\n\n    [TestMethod]\n    public void Handle_throws_exception_when_no_buyerId()\n    {\n        //Assert\n        Assert.ThrowsExactly<ArgumentNullException>(() => new Buyer(string.Empty, string.Empty));\n    }\n\n    private Buyer FakeBuyer()\n    {\n        return new Buyer(Guid.NewGuid().ToString(), \"1\");\n    }\n\n    private Order FakeOrder()\n    {\n        return new Order(\"1\", \"fakeName\", new Address(\"street\", \"city\", \"state\", \"country\", \"zipcode\"), 1, \"12\", \"111\", \"fakeName\", DateTime.UtcNow.AddYears(1));\n    }\n\n    private CreateOrderCommand FakeOrderRequestWithBuyer(Dictionary<string, object> args = null)\n    {\n        return new CreateOrderCommand(\n            new List<BasketItem>(),\n            userId: args != null && args.ContainsKey(\"userId\") ? (string)args[\"userId\"] : null,\n            userName: args != null && args.ContainsKey(\"userName\") ? (string)args[\"userName\"] : null,\n            city: args != null && args.ContainsKey(\"city\") ? (string)args[\"city\"] : null,\n            street: args != null && args.ContainsKey(\"street\") ? (string)args[\"street\"] : null,\n            state: args != null && args.ContainsKey(\"state\") ? (string)args[\"state\"] : null,\n            country: args != null && args.ContainsKey(\"country\") ? (string)args[\"country\"] : null,\n            zipcode: args != null && args.ContainsKey(\"zipcode\") ? (string)args[\"zipcode\"] : null,\n            cardNumber: args != null && args.ContainsKey(\"cardNumber\") ? (string)args[\"cardNumber\"] : \"1234\",\n            cardExpiration: args != null && args.ContainsKey(\"cardExpiration\") ? (DateTime)args[\"cardExpiration\"] : DateTime.MinValue,\n            cardSecurityNumber: args != null && args.ContainsKey(\"cardSecurityNumber\") ? (string)args[\"cardSecurityNumber\"] : \"123\",\n            cardHolderName: args != null && args.ContainsKey(\"cardHolderName\") ? (string)args[\"cardHolderName\"] : \"XXX\",\n            cardTypeId: args != null && args.ContainsKey(\"cardTypeId\") ? (int)args[\"cardTypeId\"] : 0);\n    }\n}\n"
  },
  {
    "path": "tests/Ordering.UnitTests/Application/OrdersWebApiTest.cs",
    "content": "﻿namespace eShop.Ordering.UnitTests.Application;\n\nusing Microsoft.AspNetCore.Http.HttpResults;\nusing eShop.Ordering.API.Application.Queries;\nusing Order = eShop.Ordering.API.Application.Queries.Order;\nusing NSubstitute.ExceptionExtensions;\n\n[TestClass]\npublic class OrdersWebApiTest\n{\n    private readonly IMediator _mediatorMock;\n    private readonly IOrderQueries _orderQueriesMock;\n    private readonly IIdentityService _identityServiceMock;\n    private readonly ILogger<OrderServices> _loggerMock;\n\n    public OrdersWebApiTest()\n    {\n        _mediatorMock = Substitute.For<IMediator>();\n        _orderQueriesMock = Substitute.For<IOrderQueries>();\n        _identityServiceMock = Substitute.For<IIdentityService>();\n        _loggerMock = Substitute.For<ILogger<OrderServices>>();\n    }\n\n    [TestMethod]\n    public async Task Cancel_order_with_requestId_success()\n    {\n        // Arrange\n        _mediatorMock.Send(Arg.Any<IdentifiedCommand<CancelOrderCommand, bool>>(), default)\n            .Returns(Task.FromResult(true));\n\n        // Act\n        var orderServices = new OrderServices(_mediatorMock, _orderQueriesMock, _identityServiceMock, _loggerMock);\n        var result = await OrdersApi.CancelOrderAsync(Guid.NewGuid(), new CancelOrderCommand(1), orderServices);\n\n        // Assert\n        Assert.IsInstanceOfType<Ok>(result.Result);\n    }\n\n    [TestMethod]\n    public async Task Cancel_order_bad_request()\n    {\n        // Arrange\n        _mediatorMock.Send(Arg.Any<IdentifiedCommand<CancelOrderCommand, bool>>(), default)\n            .Returns(Task.FromResult(true));\n\n        // Act\n        var orderServices = new OrderServices(_mediatorMock, _orderQueriesMock, _identityServiceMock, _loggerMock);\n        var result = await OrdersApi.CancelOrderAsync(Guid.Empty, new CancelOrderCommand(1), orderServices);\n\n        // Assert\n        Assert.IsInstanceOfType<BadRequest<string>>(result.Result);\n    }\n\n    [TestMethod]\n    public async Task Ship_order_with_requestId_success()\n    {\n        // Arrange\n        _mediatorMock.Send(Arg.Any<IdentifiedCommand<ShipOrderCommand, bool>>(), default)\n            .Returns(Task.FromResult(true));\n\n        // Act\n        var orderServices = new OrderServices(_mediatorMock, _orderQueriesMock, _identityServiceMock, _loggerMock);\n        var result = await OrdersApi.ShipOrderAsync(Guid.NewGuid(), new ShipOrderCommand(1), orderServices);\n\n        // Assert\n        Assert.IsInstanceOfType<Ok>(result.Result);\n\n    }\n\n    [TestMethod]\n    public async Task Ship_order_bad_request()\n    {\n        // Arrange\n        _mediatorMock.Send(Arg.Any<IdentifiedCommand<CreateOrderCommand, bool>>(), default)\n            .Returns(Task.FromResult(true));\n\n        // Act\n        var orderServices = new OrderServices(_mediatorMock, _orderQueriesMock, _identityServiceMock, _loggerMock);\n        var result = await OrdersApi.ShipOrderAsync(Guid.Empty, new ShipOrderCommand(1), orderServices);\n\n        // Assert\n        Assert.IsInstanceOfType<BadRequest<string>>(result.Result);\n    }\n\n    [TestMethod]\n    public async Task Get_orders_success()\n    {\n        // Arrange\n        var fakeDynamicResult = Enumerable.Empty<OrderSummary>();\n\n        _identityServiceMock.GetUserIdentity()\n            .Returns(Guid.NewGuid().ToString());\n\n        _orderQueriesMock.GetOrdersFromUserAsync(Guid.NewGuid().ToString())\n            .Returns(Task.FromResult(fakeDynamicResult));\n\n        // Act\n        var orderServices = new OrderServices(_mediatorMock, _orderQueriesMock, _identityServiceMock, _loggerMock);\n        var result = await OrdersApi.GetOrdersByUserAsync(orderServices);\n\n        // Assert\n        Assert.IsInstanceOfType<Ok<IEnumerable<OrderSummary>>>(result);\n    }\n\n    [TestMethod]\n    public async Task Get_order_success()\n    {\n        // Arrange\n        var fakeOrderId = 123;\n        var fakeDynamicResult = new Order();\n        _orderQueriesMock.GetOrderAsync(Arg.Any<int>())\n            .Returns(Task.FromResult(fakeDynamicResult));\n\n        // Act\n        var orderServices = new OrderServices(_mediatorMock, _orderQueriesMock, _identityServiceMock, _loggerMock);\n        var result = await OrdersApi.GetOrderAsync(fakeOrderId, orderServices);\n\n        // Assert\n        Assert.IsInstanceOfType<Ok<Order>>(result.Result);\n        Assert.AreSame(fakeDynamicResult, ((Ok<Order>)result.Result).Value);\n    }\n\n    [TestMethod]\n    public async Task Get_order_fails()\n    {\n        // Arrange\n        var fakeOrderId = 123;\n#pragma warning disable NS5003\n        _orderQueriesMock.GetOrderAsync(Arg.Any<int>())\n            .Throws(new KeyNotFoundException());\n#pragma warning restore NS5003\n\n        // Act\n        var orderServices = new OrderServices(_mediatorMock, _orderQueriesMock, _identityServiceMock, _loggerMock);\n        var result = await OrdersApi.GetOrderAsync(fakeOrderId, orderServices);\n\n        // Assert\n        Assert.IsInstanceOfType<NotFound>(result.Result);\n    }\n\n    [TestMethod]\n    public async Task Get_cardTypes_success()\n    {\n        // Arrange\n        var fakeDynamicResult = Enumerable.Empty<CardType>();\n        _orderQueriesMock.GetCardTypesAsync()\n            .Returns(Task.FromResult(fakeDynamicResult));\n\n        // Act\n        var result = await OrdersApi.GetCardTypesAsync(_orderQueriesMock);\n\n        // Assert\n        Assert.IsInstanceOfType<Ok<IEnumerable<CardType>>>(result);\n        Assert.AreSame(fakeDynamicResult, result.Value);\n    }\n}\n"
  },
  {
    "path": "tests/Ordering.UnitTests/Application/SetStockRejectedOrderStatusCommandTest.cs",
    "content": "﻿using System.Text.Json;\n\nnamespace eShop.Ordering.UnitTests.Application;\n\n[TestClass]\npublic class SetStockRejectedOrderStatusCommandTest\n{\n    [TestMethod]\n    public void Set_Stock_Rejected_OrderStatusCommand_Check_Serialization()\n    {\n        // Arrange\n        var command = new SetStockRejectedOrderStatusCommand(123, new List<int> { 1, 2, 3 });\n\n        // Act\n        var json = JsonSerializer.Serialize(command);\n        var deserializedCommand = JsonSerializer.Deserialize<SetStockRejectedOrderStatusCommand>(json);\n\n        //Assert\n        Assert.AreEqual(command.OrderNumber, deserializedCommand.OrderNumber);\n\n        //Assert for List<int>\n        Assert.IsNotNull(deserializedCommand.OrderStockItems);\n        Assert.HasCount(command.OrderStockItems.Count, deserializedCommand.OrderStockItems);\n\n        for (int i = 0; i < command.OrderStockItems.Count; i++)\n        {\n            Assert.AreEqual(command.OrderStockItems[i], deserializedCommand.OrderStockItems[i]);\n        }\n    }\n}\n\n"
  },
  {
    "path": "tests/Ordering.UnitTests/Builders.cs",
    "content": "﻿using eShop.Ordering.Domain.AggregatesModel.OrderAggregate;\n\nnamespace eShop.Ordering.UnitTests.Domain;\n\npublic class AddressBuilder\n{\n    public Address Build()\n    {\n        return new Address(\"street\", \"city\", \"state\", \"country\", \"zipcode\");\n    }\n}\n\npublic class OrderBuilder\n{\n    private readonly Order order;\n\n    public OrderBuilder(Address address)\n    {\n        order = new Order(\n            \"userId\",\n            \"fakeName\",\n            address,\n            cardTypeId: 5,\n            cardNumber: \"12\",\n            cardSecurityNumber: \"123\",\n            cardHolderName: \"name\",\n            cardExpiration: DateTime.UtcNow);\n    }\n\n    public OrderBuilder AddOne(\n        int productId,\n        string productName,\n        decimal unitPrice,\n        decimal discount,\n        string pictureUrl,\n        int units = 1)\n    {\n        order.AddOrderItem(productId, productName, unitPrice, discount, pictureUrl, units);\n        return this;\n    }\n\n    public Order Build()\n    {\n        return order;\n    }\n}\n"
  },
  {
    "path": "tests/Ordering.UnitTests/Domain/BuyerAggregateTest.cs",
    "content": "﻿namespace eShop.Ordering.UnitTests.Domain;\n\n[TestClass]\npublic class BuyerAggregateTest\n{\n    public BuyerAggregateTest()\n    { }\n\n    [TestMethod]\n    public void Create_buyer_item_success()\n    {\n        //Arrange    \n        var identity = new Guid().ToString();\n        var name = \"fakeUser\";\n\n        //Act \n        var fakeBuyerItem = new Buyer(identity, name);\n\n        //Assert\n        Assert.IsNotNull(fakeBuyerItem);\n    }\n\n    [TestMethod]\n    public void Create_buyer_item_fail()\n    {\n        //Arrange    \n        var identity = string.Empty;\n        var name = \"fakeUser\";\n\n        //Act - Assert\n        Assert.ThrowsExactly<ArgumentNullException>(() => new Buyer(identity, name));\n    }\n\n    [TestMethod]\n    public void add_payment_success()\n    {\n        //Arrange    \n        var cardTypeId = 1;\n        var alias = \"fakeAlias\";\n        var cardNumber = \"124\";\n        var securityNumber = \"1234\";\n        var cardHolderName = \"FakeHolderNAme\";\n        var expiration = DateTime.UtcNow.AddYears(1);\n        var orderId = 1;\n        var name = \"fakeUser\";\n        var identity = new Guid().ToString();\n        var fakeBuyerItem = new Buyer(identity, name);\n\n        //Act\n        var result = fakeBuyerItem.VerifyOrAddPaymentMethod(cardTypeId, alias, cardNumber, securityNumber, cardHolderName, expiration, orderId);\n\n        //Assert\n        Assert.IsNotNull(result);\n    }\n\n    [TestMethod]\n    public void create_payment_method_success()\n    {\n        //Arrange    \n        var cardTypeId = 1;\n        var alias = \"fakeAlias\";\n        var cardNumber = \"124\";\n        var securityNumber = \"1234\";\n        var cardHolderName = \"FakeHolderNAme\";\n        var expiration = DateTime.UtcNow.AddYears(1);\n        var fakePaymentMethod = new PaymentMethod(cardTypeId, alias, cardNumber, securityNumber, cardHolderName, expiration);\n\n        //Act\n        var result = new PaymentMethod(cardTypeId, alias, cardNumber, securityNumber, cardHolderName, expiration);\n\n        //Assert\n        Assert.IsNotNull(result);\n    }\n\n    [TestMethod]\n    public void create_payment_method_expiration_fail()\n    {\n        //Arrange    \n        var cardTypeId = 1;\n        var alias = \"fakeAlias\";\n        var cardNumber = \"124\";\n        var securityNumber = \"1234\";\n        var cardHolderName = \"FakeHolderNAme\";\n        var expiration = DateTime.UtcNow.AddYears(-1);\n\n        //Act - Assert\n        Assert.ThrowsExactly<OrderingDomainException>(() => new PaymentMethod(cardTypeId, alias, cardNumber, securityNumber, cardHolderName, expiration));\n    }\n\n    [TestMethod]\n    public void payment_method_isEqualTo()\n    {\n        //Arrange    \n        var cardTypeId = 1;\n        var alias = \"fakeAlias\";\n        var cardNumber = \"124\";\n        var securityNumber = \"1234\";\n        var cardHolderName = \"FakeHolderNAme\";\n        var expiration = DateTime.UtcNow.AddYears(1);\n\n        //Act\n        var fakePaymentMethod = new PaymentMethod(cardTypeId, alias, cardNumber, securityNumber, cardHolderName, expiration);\n        var result = fakePaymentMethod.IsEqualTo(cardTypeId, cardNumber, expiration);\n\n        //Assert\n        Assert.IsTrue(result);\n    }\n\n    [TestMethod]\n    public void Add_new_PaymentMethod_raises_new_event()\n    {\n        //Arrange    \n        var alias = \"fakeAlias\";\n        var orderId = 1;\n        var cardTypeId = 5;\n        var cardNumber = \"12\";\n        var cardSecurityNumber = \"123\";\n        var cardHolderName = \"FakeName\";\n        var cardExpiration = DateTime.UtcNow.AddYears(1);\n        var expectedResult = 1;\n        var name = \"fakeUser\";\n\n        //Act \n        var fakeBuyer = new Buyer(Guid.NewGuid().ToString(), name);\n        fakeBuyer.VerifyOrAddPaymentMethod(cardTypeId, alias, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration, orderId);\n\n        //Assert\n        Assert.HasCount(expectedResult, fakeBuyer.DomainEvents);\n    }\n}\n"
  },
  {
    "path": "tests/Ordering.UnitTests/Domain/OrderAggregateTest.cs",
    "content": "﻿namespace eShop.Ordering.UnitTests.Domain;\n\nusing eShop.Ordering.Domain.AggregatesModel.OrderAggregate;\nusing eShop.Ordering.UnitTests.Domain;\n\n[TestClass]\npublic class OrderAggregateTest\n{\n    public OrderAggregateTest()\n    { }\n\n    [TestMethod]\n    public void Create_order_item_success()\n    {\n        //Arrange    \n        var productId = 1;\n        var productName = \"FakeProductName\";\n        var unitPrice = 12;\n        var discount = 15;\n        var pictureUrl = \"FakeUrl\";\n        var units = 5;\n\n        //Act \n        var fakeOrderItem = new OrderItem(productId, productName, unitPrice, discount, pictureUrl, units);\n\n        //Assert\n        Assert.IsNotNull(fakeOrderItem);\n    }\n\n    [TestMethod]\n    public void Invalid_number_of_units()\n    {\n        //Arrange    \n        var productId = 1;\n        var productName = \"FakeProductName\";\n        var unitPrice = 12;\n        var discount = 15;\n        var pictureUrl = \"FakeUrl\";\n        var units = -1;\n\n        //Act - Assert\n        Assert.ThrowsExactly<OrderingDomainException>(() => new OrderItem(productId, productName, unitPrice, discount, pictureUrl, units));\n    }\n\n    [TestMethod]\n    public void Invalid_total_of_order_item_lower_than_discount_applied()\n    {\n        //Arrange    \n        var productId = 1;\n        var productName = \"FakeProductName\";\n        var unitPrice = 12;\n        var discount = 15;\n        var pictureUrl = \"FakeUrl\";\n        var units = 1;\n        \n        //Act - Assert\n        Assert.ThrowsExactly<OrderingDomainException>(() => new OrderItem(productId, productName, unitPrice, discount, pictureUrl, units));       \n    }\n\n    [TestMethod]\n    public void Invalid_discount_setting()\n    {\n        //Arrange    \n        var productId = 1;\n        var productName = \"FakeProductName\";\n        var unitPrice = 12;\n        var discount = 15;\n        var pictureUrl = \"FakeUrl\";\n        var units = 5;\n\n        //Act \n        var fakeOrderItem = new OrderItem(productId, productName, unitPrice, discount, pictureUrl, units);\n\n        //Assert\n        Assert.ThrowsExactly<OrderingDomainException>(() => fakeOrderItem.SetNewDiscount(-1));\n    }\n\n    [TestMethod]\n    public void Invalid_units_setting()\n    {\n        //Arrange    \n        var productId = 1;\n        var productName = \"FakeProductName\";\n        var unitPrice = 12;\n        var discount = 15;\n        var pictureUrl = \"FakeUrl\";\n        var units = 5;\n\n        //Act \n        var fakeOrderItem = new OrderItem(productId, productName, unitPrice, discount, pictureUrl, units);\n\n        //Assert\n        Assert.ThrowsExactly<OrderingDomainException>(() => fakeOrderItem.AddUnits(-1));\n    }\n\n    [TestMethod]\n    public void when_add_two_times_on_the_same_item_then_the_total_of_order_should_be_the_sum_of_the_two_items()\n    {\n        var address = new AddressBuilder().Build();\n        var order = new OrderBuilder(address)\n            .AddOne(1, \"cup\", 10.0m, 0, string.Empty)\n            .AddOne(1, \"cup\", 10.0m, 0, string.Empty)\n            .Build();\n\n        Assert.AreEqual(20.0m, order.GetTotal());\n    }\n\n    [TestMethod]\n    public void Add_new_Order_raises_new_event()\n    {\n        //Arrange\n        var street = \"fakeStreet\";\n        var city = \"FakeCity\";\n        var state = \"fakeState\";\n        var country = \"fakeCountry\";\n        var zipcode = \"FakeZipCode\";\n        var cardTypeId = 5;\n        var cardNumber = \"12\";\n        var cardSecurityNumber = \"123\";\n        var cardHolderName = \"FakeName\";\n        var cardExpiration = DateTime.UtcNow.AddYears(1);\n        var expectedResult = 1;\n\n        //Act \n        var fakeOrder = new Order(\"1\", \"fakeName\", new Address(street, city, state, country, zipcode), cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration);\n\n        //Assert\n        Assert.HasCount(expectedResult, fakeOrder.DomainEvents);\n    }\n\n    [TestMethod]\n    public void Add_event_Order_explicitly_raises_new_event()\n    {\n        //Arrange   \n        var street = \"fakeStreet\";\n        var city = \"FakeCity\";\n        var state = \"fakeState\";\n        var country = \"fakeCountry\";\n        var zipcode = \"FakeZipCode\";\n        var cardTypeId = 5;\n        var cardNumber = \"12\";\n        var cardSecurityNumber = \"123\";\n        var cardHolderName = \"FakeName\";\n        var cardExpiration = DateTime.UtcNow.AddYears(1);\n        var expectedResult = 2;\n\n        //Act \n        var fakeOrder = new Order(\"1\", \"fakeName\", new Address(street, city, state, country, zipcode), cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration);\n        fakeOrder.AddDomainEvent(new OrderStartedDomainEvent(fakeOrder, \"fakeName\", \"1\", cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration));\n        //Assert\n        Assert.HasCount(expectedResult, fakeOrder.DomainEvents);\n    }\n\n    [TestMethod]\n    public void Remove_event_Order_explicitly()\n    {\n        //Arrange    \n        var street = \"fakeStreet\";\n        var city = \"FakeCity\";\n        var state = \"fakeState\";\n        var country = \"fakeCountry\";\n        var zipcode = \"FakeZipCode\";\n        var cardTypeId = 5;\n        var cardNumber = \"12\";\n        var cardSecurityNumber = \"123\";\n        var cardHolderName = \"FakeName\";\n        var cardExpiration = DateTime.UtcNow.AddYears(1);\n        var fakeOrder = new Order(\"1\", \"fakeName\", new Address(street, city, state, country, zipcode), cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration);\n        var @fakeEvent = new OrderStartedDomainEvent(fakeOrder, \"1\", \"fakeName\", cardTypeId, cardNumber, cardSecurityNumber, cardHolderName, cardExpiration);\n        var expectedResult = 1;\n\n        //Act         \n        fakeOrder.AddDomainEvent(@fakeEvent);\n        fakeOrder.RemoveDomainEvent(@fakeEvent);\n        //Assert\n        Assert.HasCount(expectedResult, fakeOrder.DomainEvents);\n    }\n}\n"
  },
  {
    "path": "tests/Ordering.UnitTests/Domain/SeedWork/ValueObjectTests.cs",
    "content": "﻿namespace eShop.Ordering.UnitTests.Domain.SeedWork;\n\n[TestClass]\npublic class ValueObjectTests\n{\n    [TestMethod]\n    [DynamicData(nameof(EqualValueObjects))]\n    public void Equals_EqualValueObjects_ReturnsTrue(ValueObject instanceA, ValueObject instanceB, string reason)\n    {\n        // Act\n        var result = EqualityComparer<ValueObject>.Default.Equals(instanceA, instanceB);\n\n        // Assert\n        Assert.IsTrue(result, reason);\n    }\n\n    [TestMethod]\n    [DynamicData(nameof(NonEqualValueObjects))]\n    public void Equals_NonEqualValueObjects_ReturnsFalse(ValueObject instanceA, ValueObject instanceB, string reason)\n    {\n        // Act\n        var result = EqualityComparer<ValueObject>.Default.Equals(instanceA, instanceB);\n\n        // Assert\n        Assert.IsFalse(result, reason);\n    }\n\n    private static readonly ValueObject APrettyValueObject = new ValueObjectA(1, \"2\", Guid.Parse(\"97ea43f0-6fef-4fb7-8c67-9114a7ff6ec0\"), new ComplexObject(2, \"3\"));\n\n    public static IEnumerable<object[]> EqualValueObjects\n    {\n        get\n        {\n            return new[]\n            {\n                new object[]\n                {\n                    null,\n                    null,\n                    \"they should be equal because they are both null\"\n                },\n                new object[]\n                {\n                    APrettyValueObject,\n                    APrettyValueObject,\n                    \"they should be equal because they are the same object\"\n                },\n                new object[]\n                {\n                    new ValueObjectA(1, \"2\", Guid.Parse(\"97ea43f0-6fef-4fb7-8c67-9114a7ff6ec0\"), new ComplexObject(2, \"3\")),\n                    new ValueObjectA(1, \"2\", Guid.Parse(\"97ea43f0-6fef-4fb7-8c67-9114a7ff6ec0\"), new ComplexObject(2, \"3\")),\n                    \"they should be equal because they have equal members\"\n                },\n                new object[]\n                {\n                    new ValueObjectA(1, \"2\", Guid.Parse(\"97ea43f0-6fef-4fb7-8c67-9114a7ff6ec0\"), new ComplexObject(2, \"3\"), notAnEqualityComponent: \"xpto\"),\n                    new ValueObjectA(1, \"2\", Guid.Parse(\"97ea43f0-6fef-4fb7-8c67-9114a7ff6ec0\"), new ComplexObject(2, \"3\"), notAnEqualityComponent: \"xpto2\"),\n                    \"they should be equal because all equality components are equal, even though an additional member was set\"\n                },\n                new object[]\n                {\n                    new ValueObjectB(1, \"2\",  1, 2, 3 ),\n                    new ValueObjectB(1, \"2\",  1, 2, 3 ),\n                    \"they should be equal because all equality components are equal, including the 'C' list\"\n                }\n            };\n        }\n    }\n\n    public static IEnumerable<object[]> NonEqualValueObjects\n    {\n        get\n        {\n            return new[]\n            {\n\n                new object[] {\n                    new ValueObjectA(a: 1, b: \"2\", c: Guid.Parse(\"97ea43f0-6fef-4fb7-8c67-9114a7ff6ec0\"), d: new ComplexObject(2, \"3\")),\n                    new ValueObjectA(a: 2, b: \"2\", c: Guid.Parse(\"97ea43f0-6fef-4fb7-8c67-9114a7ff6ec0\"), d: new ComplexObject(2, \"3\")),\n                    \"they should not be equal because the 'A' member on ValueObjectA is different among them\"\n                },\n                new object[] {\n                    new ValueObjectA(a: 1, b: \"2\", c: Guid.Parse(\"97ea43f0-6fef-4fb7-8c67-9114a7ff6ec0\"), d: new ComplexObject(2, \"3\")),\n                    new ValueObjectA(a: 1, b: null, c: Guid.Parse(\"97ea43f0-6fef-4fb7-8c67-9114a7ff6ec0\"), d: new ComplexObject(2, \"3\")),\n                    \"they should not be equal because the 'B' member on ValueObjectA is different among them\"\n                },\n                new object[] {\n                    new ValueObjectA(a: 1, b: \"2\", c: Guid.Parse(\"97ea43f0-6fef-4fb7-8c67-9114a7ff6ec0\"), d: new ComplexObject(a: 2, b: \"3\")),\n                    new ValueObjectA(a: 1, b: \"2\", c: Guid.Parse(\"97ea43f0-6fef-4fb7-8c67-9114a7ff6ec0\"), d: new ComplexObject(a: 3, b: \"3\")),\n                    \"they should not be equal because the 'A' member on ValueObjectA's 'D' member is different among them\"\n                },\n                new object[] {\n                    new ValueObjectA(a: 1, b: \"2\", c: Guid.Parse(\"97ea43f0-6fef-4fb7-8c67-9114a7ff6ec0\"), d: new ComplexObject(a: 2, b: \"3\")),\n                    new ValueObjectB(a: 1, b: \"2\"),\n                    \"they should not be equal because they are not of the same type\"\n                },\n                new object[] {\n                    new ValueObjectB(1, \"2\",  1, 2, 3 ),\n                    new ValueObjectB(1, \"2\",  1, 2, 3, 4 ),\n                    \"they should be not be equal because the 'C' list contains one additional value\"\n                },\n                new object[] {\n                    new ValueObjectB(1, \"2\",  1, 2, 3, 5 ),\n                    new ValueObjectB(1, \"2\",  1, 2, 3 ),\n                    \"they should be not be equal because the 'C' list contains one additional value\"\n                },\n                new object[] {\n                    new ValueObjectB(1, \"2\",  1, 2, 3, 5 ),\n                    new ValueObjectB(1, \"2\",  1, 2, 3, 4 ),\n                    \"they should be not be equal because the 'C' lists are not equal\"\n                }\n            };        }\n\n    }\n\n    private class ValueObjectA : ValueObject\n    {\n        public ValueObjectA(int a, string b, Guid c, ComplexObject d, string notAnEqualityComponent = null)\n        {\n            A = a;\n            B = b;\n            C = c;\n            D = d;\n            NotAnEqualityComponent = notAnEqualityComponent;\n        }\n\n        public int A { get; }\n        public string B { get; }\n        public Guid C { get; }\n        public ComplexObject D { get; }\n        public string NotAnEqualityComponent { get; }\n\n        protected override IEnumerable<object> GetEqualityComponents()\n        {\n            yield return A;\n            yield return B;\n            yield return C;\n            yield return D;\n        }\n    }\n\n    private class ValueObjectB : ValueObject\n    {\n        public ValueObjectB(int a, string b, params int[] c)\n        {\n            A = a;\n            B = b;\n            C = c.ToList();\n        }\n\n        public int A { get; }\n        public string B { get; }\n\n        public List<int> C { get; }\n\n        protected override IEnumerable<object> GetEqualityComponents()\n        {\n            yield return A;\n            yield return B;\n\n            foreach (var c in C)\n            {\n                yield return c;\n            }\n        }\n    }\n\n    private class ComplexObject : IEquatable<ComplexObject>\n    {\n        public ComplexObject(int a, string b)\n        {\n            A = a;\n            B = b;\n        }\n\n        public int A { get; set; }\n\n        public string B { get; set; }\n\n        public override bool Equals(object obj)\n        {\n            return Equals(obj as ComplexObject);\n        }\n\n        public bool Equals(ComplexObject other)\n        {\n            return other != null &&\n                    A == other.A &&\n                    B == other.B;\n        }\n\n        public override int GetHashCode()\n        {\n            return HashCode.Combine(A, B);\n        }\n    }\n}\n"
  },
  {
    "path": "tests/Ordering.UnitTests/GlobalUsings.cs",
    "content": "﻿global using System;\nglobal using System.Collections.Generic;\nglobal using System.Linq;\nglobal using System.Threading;\nglobal using System.Threading.Tasks;\nglobal using MediatR;\nglobal using Microsoft.AspNetCore.Mvc;\nglobal using eShop.Ordering.API.Application.Commands;\nglobal using eShop.Ordering.API.Application.Models;\nglobal using eShop.Ordering.API.Infrastructure.Services;\nglobal using eShop.Ordering.Domain.AggregatesModel.BuyerAggregate;\nglobal using eShop.Ordering.Domain.Events;\nglobal using eShop.Ordering.Domain.Exceptions;\nglobal using eShop.Ordering.Domain.SeedWork;\nglobal using eShop.Ordering.Infrastructure.Idempotency;\nglobal using Microsoft.Extensions.Logging;\nglobal using NSubstitute;\nglobal using eShop.Ordering.UnitTests;\nglobal using Microsoft.VisualStudio.TestTools.UnitTesting;\n\n[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.MethodLevel)]\n"
  },
  {
    "path": "tests/Ordering.UnitTests/Ordering.UnitTests.csproj",
    "content": "﻿<Project Sdk=\"MSTest.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks>\n    <IsPublishable>false</IsPublishable>\n    <IsPackable>false</IsPackable>\n    <OutputType>Exe</OutputType>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"NSubstitute\" />\n    <PackageReference Include=\"NSubstitute.Analyzers.CSharp\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Ordering.API\\Ordering.API.csproj\" />\n    <ProjectReference Include=\"..\\..\\src\\Ordering.Domain\\Ordering.Domain.csproj\" />\n    <ProjectReference Include=\"..\\..\\src\\Ordering.Infrastructure\\Ordering.Infrastructure.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "tests/README.md",
    "content": "# eShop Tests\n\nThis directory contains a collection of unit and functional tests for validating the behavior of various components in the eShop application.\n\n**NOTE:** Functional tests in this leverage the Aspire host to spin up test containers and require that Docker be running as a pre-requisite."
  }
]