From fc2918eceac64116874501ab44c7a9cc97631ca1 Mon Sep 17 00:00:00 2001 From: Arne Limburg Date: Mon, 7 Nov 2022 18:44:24 +0100 Subject: [PATCH 1/6] Add services --- .devcontainers/devcontainer.json | 35 + .github/workflows/build.yaml | 38 + .gitignore | 7 + README.md | 39 + address-validation-service/.gitignore | 1 + address-validation-service/Dockerfile | 17 + address-validation-service/pom.xml | 212 + .../src/main/checkstyle/configuration.xml | 214 + .../src/main/checkstyle/java.header | 15 + .../application/AddressApplication.java | 26 + .../address/application/AddressResource.java | 99 + .../sample/address/domain/Address.java | 38 + .../address/domain/AddressRepository.java | 65 + .../sample/address/domain/City.java | 105 + .../sample/address/domain/CityName.java | 72 + .../sample/address/domain/HouseNumber.java | 81 + .../sample/address/domain/Street.java | 74 + .../sample/address/domain/StreetName.java | 76 + .../sample/address/domain/ZipCode.java | 76 + .../address/infrastructure/CorsFilter.java | 37 + .../src/main/resources/plz.txt | 8219 +++++++++++++++++ ...ry-service-address-validation-service.json | 126 + .../sample/address/ambiguous-address.json | 7 + .../sample/address/missing-address.json | 7 + .../sample/address/misspelled-address.json | 7 + .../sample/address/valid-address.json | 7 + billing-service/.gitignore | 1 + billing-service/Dockerfile | 17 + billing-service/pom.xml | 212 + .../src/main/checkstyle/configuration.xml | 214 + .../src/main/checkstyle/java.header | 15 + .../application/AddressApplication.java | 26 + .../address/application/AddressResource.java | 72 + .../sample/address/domain/Address.java | 106 + .../sample/address/domain/AddressLine.java | 114 + .../address/domain/AddressRepository.java | 55 + .../sample/address/domain/City.java | 113 + .../sample/address/domain/CityName.java | 72 + .../sample/address/domain/CustomerNumber.java | 77 + .../sample/address/domain/HouseNumber.java | 81 + .../sample/address/domain/Location.java | 66 + .../sample/address/domain/Recipient.java | 81 + .../sample/address/domain/Street.java | 74 + .../sample/address/domain/StreetName.java | 76 + .../sample/address/domain/ZipCode.java | 76 + .../address/infrastructure/CorsFilter.java | 37 + .../sample/address/JsonObjectComparision.java | 41 + .../address/domain/TestAddressRepository.java | 25 + .../de/openknowledge/sample/address/007.json | 8 + .../sample/address/0815-new.json | 8 + .../de/openknowledge/sample/address/0815.json | 8 + .../de/openknowledge/sample/address/0816.json | 8 + customer-service/.gitignore | 1 + customer-service/Dockerfile | 17 + customer-service/pom.xml | 212 + .../src/main/checkstyle/configuration.xml | 214 + .../src/main/checkstyle/java.header | 15 + .../sample/address/domain/Address.java | 106 + .../sample/address/domain/AddressLine.java | 115 + .../domain/BillingAddressRepository.java | 76 + .../sample/address/domain/City.java | 113 + .../sample/address/domain/CityName.java | 72 + .../domain/DeliveryAddressRepository.java | 91 + .../sample/address/domain/HouseNumber.java | 81 + .../sample/address/domain/Recipient.java | 81 + .../sample/address/domain/Street.java | 74 + .../sample/address/domain/StreetName.java | 76 + .../sample/address/domain/ZipCode.java | 76 + .../address/infrastructure/CorsFilter.java | 37 + .../ValidationExceptionHandler.java | 47 + .../application/CustomerApplication.java | 26 + .../application/CustomerResource.java | 115 + .../sample/customer/domain/Customer.java | 98 + .../sample/customer/domain/CustomerName.java | 77 + .../customer/domain/CustomerNumber.java | 76 + .../customer/domain/CustomerRepository.java | 67 + .../META-INF/microprofile-config.properties | 2 + .../sample/customer/CustomerServiceTest.java | 209 + .../customer/JsonObjectComparision.java | 41 + .../sample/infrastructure/CdiMock.java | 35 + .../src/test/resources/META-INF/beans.xml | 11 + .../sample/customer/erika-with-addresses.json | 12 + .../openknowledge/sample/customer/erika.json | 4 + .../sample/customer/james-with-addresses.json | 12 + .../openknowledge/sample/customer/james.json | 4 + .../sample/customer/max-with-addresses.json | 20 + .../de/openknowledge/sample/customer/max.json | 4 + .../sample/customer/sherlock-address.json | 8 + .../sample/customer/sherlock.json | 3 + delivery-service/.gitignore | 1 + delivery-service/Dockerfile | 17 + delivery-service/pom.xml | 212 + .../src/main/checkstyle/configuration.xml | 214 + .../src/main/checkstyle/java.header | 15 + .../application/AddressesApplication.java | 26 + .../application/AddressesResource.java | 76 + .../sample/address/domain/Address.java | 106 + .../sample/address/domain/AddressLine.java | 115 + .../address/domain/AddressRepository.java | 58 + .../domain/AddressValidationService.java | 59 + .../sample/address/domain/City.java | 113 + .../sample/address/domain/CityName.java | 72 + .../sample/address/domain/CustomerNumber.java | 77 + .../sample/address/domain/HouseNumber.java | 81 + .../sample/address/domain/Location.java | 66 + .../sample/address/domain/Recipient.java | 81 + .../sample/address/domain/Street.java | 74 + .../sample/address/domain/StreetName.java | 76 + .../sample/address/domain/ZipCode.java | 76 + .../address/infrastructure/CorsFilter.java | 37 + .../ValidationExceptionHandler.java | 49 + .../META-INF/microprofile-config.properties | 1 + .../sample/infrastructure/CdiMock.java | 35 + .../customer-service-delivery-service.json | 180 + .../sample/address/007-invalid.json | 8 + .../sample/address/0815-new.json | 8 + .../de/openknowledge/sample/address/0815.json | 8 + .../sample/address/0816-invalid.json | 8 + .../de/openknowledge/sample/address/0816.json | 8 + .../de/openknowledge/sample/address/0817.json | 8 + docker-compose.yaml | 28 + keycloak/Dockerfile | 33 + 122 files changed, 15737 insertions(+) create mode 100644 .devcontainers/devcontainer.json create mode 100644 .github/workflows/build.yaml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 address-validation-service/.gitignore create mode 100644 address-validation-service/Dockerfile create mode 100644 address-validation-service/pom.xml create mode 100644 address-validation-service/src/main/checkstyle/configuration.xml create mode 100644 address-validation-service/src/main/checkstyle/java.header create mode 100644 address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java create mode 100644 address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java create mode 100644 address-validation-service/src/main/java/de/openknowledge/sample/address/domain/Address.java create mode 100644 address-validation-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java create mode 100644 address-validation-service/src/main/java/de/openknowledge/sample/address/domain/City.java create mode 100644 address-validation-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java create mode 100644 address-validation-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java create mode 100644 address-validation-service/src/main/java/de/openknowledge/sample/address/domain/Street.java create mode 100644 address-validation-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java create mode 100644 address-validation-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java create mode 100644 address-validation-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java create mode 100644 address-validation-service/src/main/resources/plz.txt create mode 100644 address-validation-service/src/test/pacts/delivery-service-address-validation-service.json create mode 100644 address-validation-service/src/test/resources/de/openknowledge/sample/address/ambiguous-address.json create mode 100644 address-validation-service/src/test/resources/de/openknowledge/sample/address/missing-address.json create mode 100644 address-validation-service/src/test/resources/de/openknowledge/sample/address/misspelled-address.json create mode 100644 address-validation-service/src/test/resources/de/openknowledge/sample/address/valid-address.json create mode 100644 billing-service/.gitignore create mode 100644 billing-service/Dockerfile create mode 100644 billing-service/pom.xml create mode 100644 billing-service/src/main/checkstyle/configuration.xml create mode 100644 billing-service/src/main/checkstyle/java.header create mode 100644 billing-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java create mode 100644 billing-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java create mode 100644 billing-service/src/main/java/de/openknowledge/sample/address/domain/Address.java create mode 100644 billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java create mode 100644 billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java create mode 100644 billing-service/src/main/java/de/openknowledge/sample/address/domain/City.java create mode 100644 billing-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java create mode 100644 billing-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java create mode 100644 billing-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java create mode 100644 billing-service/src/main/java/de/openknowledge/sample/address/domain/Location.java create mode 100644 billing-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java create mode 100644 billing-service/src/main/java/de/openknowledge/sample/address/domain/Street.java create mode 100644 billing-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java create mode 100644 billing-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java create mode 100644 billing-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java create mode 100644 billing-service/src/test/java/de/openknowledge/sample/address/JsonObjectComparision.java create mode 100644 billing-service/src/test/java/de/openknowledge/sample/address/domain/TestAddressRepository.java create mode 100644 billing-service/src/test/resources/de/openknowledge/sample/address/007.json create mode 100644 billing-service/src/test/resources/de/openknowledge/sample/address/0815-new.json create mode 100644 billing-service/src/test/resources/de/openknowledge/sample/address/0815.json create mode 100644 billing-service/src/test/resources/de/openknowledge/sample/address/0816.json create mode 100644 customer-service/.gitignore create mode 100644 customer-service/Dockerfile create mode 100644 customer-service/pom.xml create mode 100644 customer-service/src/main/checkstyle/configuration.xml create mode 100644 customer-service/src/main/checkstyle/java.header create mode 100644 customer-service/src/main/java/de/openknowledge/sample/address/domain/Address.java create mode 100644 customer-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java create mode 100644 customer-service/src/main/java/de/openknowledge/sample/address/domain/BillingAddressRepository.java create mode 100644 customer-service/src/main/java/de/openknowledge/sample/address/domain/City.java create mode 100644 customer-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java create mode 100644 customer-service/src/main/java/de/openknowledge/sample/address/domain/DeliveryAddressRepository.java create mode 100644 customer-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java create mode 100644 customer-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java create mode 100644 customer-service/src/main/java/de/openknowledge/sample/address/domain/Street.java create mode 100644 customer-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java create mode 100644 customer-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java create mode 100644 customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java create mode 100644 customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java create mode 100644 customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerApplication.java create mode 100644 customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerResource.java create mode 100644 customer-service/src/main/java/de/openknowledge/sample/customer/domain/Customer.java create mode 100644 customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerName.java create mode 100644 customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerNumber.java create mode 100644 customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerRepository.java create mode 100644 customer-service/src/main/resources/META-INF/microprofile-config.properties create mode 100644 customer-service/src/test/java/de/openknowledge/sample/customer/CustomerServiceTest.java create mode 100644 customer-service/src/test/java/de/openknowledge/sample/customer/JsonObjectComparision.java create mode 100644 customer-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java create mode 100644 customer-service/src/test/resources/META-INF/beans.xml create mode 100644 customer-service/src/test/resources/de/openknowledge/sample/customer/erika-with-addresses.json create mode 100644 customer-service/src/test/resources/de/openknowledge/sample/customer/erika.json create mode 100644 customer-service/src/test/resources/de/openknowledge/sample/customer/james-with-addresses.json create mode 100644 customer-service/src/test/resources/de/openknowledge/sample/customer/james.json create mode 100644 customer-service/src/test/resources/de/openknowledge/sample/customer/max-with-addresses.json create mode 100644 customer-service/src/test/resources/de/openknowledge/sample/customer/max.json create mode 100644 customer-service/src/test/resources/de/openknowledge/sample/customer/sherlock-address.json create mode 100644 customer-service/src/test/resources/de/openknowledge/sample/customer/sherlock.json create mode 100644 delivery-service/.gitignore create mode 100644 delivery-service/Dockerfile create mode 100644 delivery-service/pom.xml create mode 100644 delivery-service/src/main/checkstyle/configuration.xml create mode 100644 delivery-service/src/main/checkstyle/java.header create mode 100644 delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesApplication.java create mode 100644 delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesResource.java create mode 100644 delivery-service/src/main/java/de/openknowledge/sample/address/domain/Address.java create mode 100644 delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java create mode 100644 delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java create mode 100644 delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressValidationService.java create mode 100644 delivery-service/src/main/java/de/openknowledge/sample/address/domain/City.java create mode 100644 delivery-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java create mode 100644 delivery-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java create mode 100644 delivery-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java create mode 100644 delivery-service/src/main/java/de/openknowledge/sample/address/domain/Location.java create mode 100644 delivery-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java create mode 100644 delivery-service/src/main/java/de/openknowledge/sample/address/domain/Street.java create mode 100644 delivery-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java create mode 100644 delivery-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java create mode 100644 delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java create mode 100644 delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java create mode 100644 delivery-service/src/main/resources/META-INF/microprofile-config.properties create mode 100644 delivery-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java create mode 100644 delivery-service/src/test/pacts/customer-service-delivery-service.json create mode 100644 delivery-service/src/test/resources/de/openknowledge/sample/address/007-invalid.json create mode 100644 delivery-service/src/test/resources/de/openknowledge/sample/address/0815-new.json create mode 100644 delivery-service/src/test/resources/de/openknowledge/sample/address/0815.json create mode 100644 delivery-service/src/test/resources/de/openknowledge/sample/address/0816-invalid.json create mode 100644 delivery-service/src/test/resources/de/openknowledge/sample/address/0816.json create mode 100644 delivery-service/src/test/resources/de/openknowledge/sample/address/0817.json create mode 100644 docker-compose.yaml create mode 100644 keycloak/Dockerfile diff --git a/.devcontainers/devcontainer.json b/.devcontainers/devcontainer.json new file mode 100644 index 00000000..fa90a14f --- /dev/null +++ b/.devcontainers/devcontainer.json @@ -0,0 +1,35 @@ +// If you want to run as a non-root user in the container, see .devcontainer/docker-compose.yml. +{ + "name": "OpenAPI Editor", + + // Update the 'dockerComposeFile' list if you have more compose files or use different names. + // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. + "dockerComposeFile": [ + "../docker-compose.yml", + "docker-compose.yml" + ], + + // The 'service' property is the name of the service for the container that VS Code should + // use. Update this value and .devcontainer/docker-compose.yml to the real service name. + "service": "openapi-editor", + + // The optional 'workspaceFolder' property is the path VS Code should open by default when + // connected. This is typically a file mount in .devcontainer/docker-compose.yml + "workspaceFolder": "/workspace" + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Uncomment the next line if you want start specific services in your Docker Compose config. + // "runServices": [], + + // Uncomment the next line if you want to keep your containers running after VS Code shuts down. + // "shutdownAction": "none", + + // Uncomment the next line to run commands after the container is created - for example installing curl. + // "postCreateCommand": "apt-get update && apt-get install -y curl", + + // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. + // "remoteUser": "vscode" +} + diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 00000000..2748e7c6 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,38 @@ +name: Online Shop +on: + push: + branches: [ "**" ] +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + java-version: '21' + distribution: 'temurin' + cache: maven + + - name: Build customer-service + run: mvn clean package -f customer-service/pom.xml -Dservice.name=customer-service + + - name: Build billing-service + run: mvn clean package -f billing-service/pom.xml -Dservice.name=billing-service + + - name: Build delivery-service + run: mvn clean package -f delivery-service/pom.xml -Dservice.name=delivery-service + + - name: Build address-validation-service + run: mvn clean package -f address-validation-service/pom.xml -Dservice.name=address-validation-service + + - name: Build with Docker + run: docker compose build + + - name: Start with Docker + run: docker compose up -d + + - name: Stop Docker Containers + run: docker compose down diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a7a0be89 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.* +/*/.* +!.gitignore +!.github +/keycloak/* +target/ +*.iml diff --git a/README.md b/README.md new file mode 100644 index 00000000..27853ff1 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# Workshop API Design + +Herzlich willkommen zum Workshop API Design. + +## Übungen + +### API Design + +- [OpenAPI](https://github.com/openknowledge/workshop-api-design/tree/openapi) +- [Mocking](https://github.com/openknowledge/workshop-api-design/tree/wiremock) +- [AsyncAPI](https://github.com/openknowledge/workshop-api-design/tree/asyncapi) + +### API Testing + +- [Pact](https://github.com/openknowledge/workshop-api-design/tree/pact-mock-server) +- [Pact Pipeline](https://github.com/openknowledge/workshop-api-design/tree/pact) + +### API Security + +- [JWT](https://github.com/openknowledge/workshop-api-design/tree/jwt) +- [OAuth2](https://github.com/openknowledge/workshop-api-design/tree/oauth2) +- [OAuth2 mit PKCE](https://github.com/openknowledge/workshop-api-design/tree/oauth2-pkce) + +### API Governance + +- [Linting](https://github.com/openknowledge/workshop-api-design/tree/linting) + +### API Management + +- [Rate Limiting](https://github.com/openknowledge/workshop-api-design/tree/rate-limiting) +- [Backstage](https://github.com/openknowledge/workshop-api-design/tree/backstage) + +### API Operation + +- [Observability](https://github.com/openknowledge/workshop-api-design/tree/observability) + +### API Evolution + +- [Versioning](https://github.com/openknowledge/workshop-api-design/tree/versioning) diff --git a/address-validation-service/.gitignore b/address-validation-service/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/address-validation-service/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/address-validation-service/Dockerfile b/address-validation-service/Dockerfile new file mode 100644 index 00000000..afe1340d --- /dev/null +++ b/address-validation-service/Dockerfile @@ -0,0 +1,17 @@ +FROM maven:3.9.9-eclipse-temurin-21 AS mvn + +WORKDIR /usr/src/online-shop +COPY pom.xml ./ +RUN mvn package dependency:go-offline # cache dependencies +COPY src ./src +RUN mvn clean package meecrowave:bundle -Dservice.name=address-validation-service -Dmaven.test.skip=true -Dcheckstyle.skip=true + +FROM eclipse-temurin:21-jre + +RUN apt-get update -qq && apt-get install --no-install-recommends -qqy unzip && rm -rf /var/lib/apt/lists/* +COPY --from=mvn /usr/src/online-shop/target/address-validation-service-meecrowave-distribution.zip /opt/address-validation-service.zip +WORKDIR /opt +RUN unzip address-validation-service.zip && rm address-validation-service.zip +EXPOSE 8080 + +ENTRYPOINT ["/opt/address-validation-service-distribution/bin/meecrowave.sh", "run"] diff --git a/address-validation-service/pom.xml b/address-validation-service/pom.xml new file mode 100644 index 00000000..3f87a412 --- /dev/null +++ b/address-validation-service/pom.xml @@ -0,0 +1,212 @@ + + + + + 4.0.0 + + de.openknowledge.sample.shop + ${service.name} + 1.0.0-SNAPSHOT + + + online-shop + 17 + 17 + false + UTF-8 + 1.2.15 + 1.9.6 + 5.8.2 + + + + + + org.apache.meecrowave + meecrowave-specs-api + ${meecrowave.version} + provided + + + org.apache.meecrowave + meecrowave-core + ${meecrowave.version} + provided + + + org.apache.geronimo.specs + geronimo-validation_1.0_spec + 1.1 + + + org.eclipse.microprofile.config + microprofile-config-api + 1.4 + + + org.apache.geronimo.config + geronimo-config-impl + 1.2.2 + + + org.eclipse.microprofile.jwt + microprofile-jwt-auth-api + 1.2.1 + + + org.apache.geronimo + geronimo-jwt-auth + 1.0.4 + + + org.apache.deltaspike.modules + deltaspike-security-module-api + ${deltaspike.version} + compile + + + org.apache.deltaspike.modules + deltaspike-security-module-impl + ${deltaspike.version} + runtime + + + org.eclipse.microprofile.opentracing + microprofile-opentracing-api + 1.2 + + + io.opentracing + opentracing-api + 0.31.0 + + + org.apache.geronimo + geronimo-opentracing + 1.0.3 + + + org.eclipse.microprofile.health + microprofile-health-api + 3.0-RC3 + + + org.apache.geronimo + geronimo-health + 2.0.1 + + + org.microprofile-ext.health-ext + healthprobe-jvm + 1.2.0 + + + org.microprofile-ext.health-ext + healthprobe-system + 1.2.0 + + + org.eclipse.microprofile.metrics + microprofile-metrics-api + 3.0-RC2 + + + org.apache.geronimo + geronimo-metrics + 1.0.6 + + + org.apache.commons + commons-lang3 + 3.9 + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.assertj + assertj-core + 3.22.0 + test + + + org.mockito + mockito-core + 2.28.2 + test + + + org.apache.meecrowave + meecrowave-junit + ${meecrowave.version} + test + + + rocks.limburg.cdimock + cdimock + 1.0.4 + test + + + com.tngtech.archunit + archunit + 1.0.1 + test + + + + ${project.artifactId} + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.4 + + + org.apache.meecrowave + meecrowave-maven-plugin + ${meecrowave.version} + + + maven-checkstyle-plugin + 3.2.1 + + true + ${project.basedir}/src/main/checkstyle/java.header + true + true + + + + compile + compile + + checkstyle + + + ${project.basedir}/src/main/checkstyle/configuration.xml + true + + + + + + + diff --git a/address-validation-service/src/main/checkstyle/configuration.xml b/address-validation-service/src/main/checkstyle/configuration.xml new file mode 100644 index 00000000..3392d8cf --- /dev/null +++ b/address-validation-service/src/main/checkstyle/configuration.xml @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/address-validation-service/src/main/checkstyle/java.header b/address-validation-service/src/main/checkstyle/java.header new file mode 100644 index 00000000..baba0272 --- /dev/null +++ b/address-validation-service/src/main/checkstyle/java.header @@ -0,0 +1,15 @@ +/* + * Copyright \d{4}( - \d{4}|(, \d{4})*) [A-Za-z ,\-]. + * + * Licensed under the Apache License, Version 2\.0 \(the "License"\); + * you may not use this file except in compliance with the License\. + * You may obtain a copy of the License at + * + * http://www\.apache\.org/licenses/LICENSE-2\.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\. + * See the License for the specific language governing permissions and + * limitations under the License\. + */ diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java new file mode 100644 index 00000000..89397e09 --- /dev/null +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java @@ -0,0 +1,26 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.application; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +/** + * Application initialization + */ +@ApplicationPath("/") +public class AddressApplication extends Application { +} diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java new file mode 100644 index 00000000..91bf2c84 --- /dev/null +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java @@ -0,0 +1,99 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.application; + +import static java.util.stream.Collectors.joining; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.logging.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; + +import de.openknowledge.sample.address.domain.Address; +import de.openknowledge.sample.address.domain.AddressRepository; +import de.openknowledge.sample.address.domain.City; + +/** + * RESTFul endpoint for valid addresses + */ +@ApplicationScoped +@Path("/valid-addresses") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class AddressResource { + + private static final Logger LOGGER = Logger.getLogger(AddressResource.class.getSimpleName()); + private static final String PROBLEM_JSON_TYPE = "application/problem+json"; + private static final String PROBLEM_JSON + = "{\"type\": \"%s\", \"title\": \"%s\", \"status\": %d, \"detail\": \"%s\", \"instance\": \"%s\"}"; + + @Inject + private AddressRepository addressesRepository; + + @GET + public Response healthCheck() { + return Response.ok().build(); + } + + @POST + @Path("/") + @Consumes(MediaType.APPLICATION_JSON) + public Response validateAddress(Address address, @Context UriInfo uri) throws URISyntaxException { + LOGGER.info("RESTful call 'POST valid address'"); + if (addressesRepository.isValid(address)) { + LOGGER.fine("address is valid"); + return Response.ok().build(); + } else { + URI type = uri.getRequestUri().resolve("/errors/invalid-city"); + URI instance = UriBuilder.fromResource(getClass()).path(address.getCity().getZipCode().toString()).build(); + List suggestions = addressesRepository.findSuggestions(address.getCity()); + LOGGER.fine(suggestions.size() + " suggestions found: " + suggestions); + if (suggestions.size() == 1) { + return Response.status(Response.Status.BAD_REQUEST).type(PROBLEM_JSON_TYPE) + .entity(String.format(PROBLEM_JSON, type, "invalid city", + Response.Status.BAD_REQUEST.getStatusCode(), + "Did you mean " + suggestions.iterator().next() + "?", instance)) + .build(); + } else if (!suggestions.isEmpty()) { + return Response.status(Response.Status.BAD_REQUEST).type(PROBLEM_JSON_TYPE) + .entity(String.format(PROBLEM_JSON, type, "invalid city", + Response.Status.BAD_REQUEST.getStatusCode(), + "Did you mean one of " + + suggestions.stream().map(Object::toString).collect(joining(", ")) + "?", + instance)) + .build(); + } else { + return Response.status(Response.Status.BAD_REQUEST).type(PROBLEM_JSON_TYPE) + .entity(String.format(PROBLEM_JSON, type, "invalid city", + Response.Status.BAD_REQUEST.getStatusCode(), "no matching city found", instance)) + .build(); + } + } + } +} diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/Address.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/Address.java new file mode 100644 index 00000000..f5c9f37a --- /dev/null +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/Address.java @@ -0,0 +1,38 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +public class Address { + + private Street street; + private City city; + + public Street getStreet() { + return street; + } + + public void setStreet(Street street) { + this.street = street; + } + + public City getCity() { + return city; + } + + public void setCity(City city) { + this.city = city; + } +} diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java new file mode 100644 index 00000000..f568c161 --- /dev/null +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java @@ -0,0 +1,65 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static java.lang.String.format; +import static java.util.stream.Collectors.toList; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Logger; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class AddressRepository { + + private static final Logger LOGGER = Logger.getLogger(AddressRepository.class.getSimpleName()); + + private Set cities; + + @PostConstruct + public void initialize() { + cities = new HashSet<>(); + try (BufferedReader cityStream + = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/plz.txt")))) { + + for (String line = cityStream.readLine(); line != null; line = cityStream.readLine()) { + City city = new City(line); + for (CityName name : city.getCityNames()) { + this.cities.add(new City(city.getZipCode(), name)); + } + } + } catch (IOException e) { + throw new IllegalStateException(e); + } + LOGGER.info(format("address validation repository initialized with %d cities: ", cities.size())); + } + + public boolean isValid(Address address) { + return cities.contains(address.getCity()); + } + + public List findSuggestions(City city) { + ZipCode zipCode = city.getZipCode(); + return cities.stream().filter(c -> c.getZipCode().equals(zipCode)).collect(toList()); + } +} diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/City.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/City.java new file mode 100644 index 00000000..c21639b3 --- /dev/null +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/City.java @@ -0,0 +1,105 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + + +import static java.util.stream.Collectors.toList; +import static org.apache.commons.lang3.Validate.notNull; + +import java.util.List; +import java.util.stream.Stream; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.City.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class City { + + private String name; + + public City(String name) { + this.name = notNull(name, "name may not be empty").trim(); + } + + public City(ZipCode zipCode, CityName name) { + this(zipCode + " " + name); + } + + protected City() { + // for framework + } + + public static City valueOf(String name) { + return new City(name); + } + + public ZipCode getZipCode() { + return new ZipCode(name.substring(0, 5)); + } + + public List getCityNames() { + String names = name.substring(5); + if (names.endsWith("u.a.")) { + names = names.substring(0, names.length() - "u.a.".length()); + } + return Stream.of(names.split(",")).map(CityName::new).collect(toList()); + } + + public CityName getCityName() { + return getCityNames().iterator().next(); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + City city = (City)object; + + return toString().equals(city.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public City adaptFromJson(String name) throws Exception { + return new City(name); + } + + @Override + public String adaptToJson(City name) throws Exception { + return name.toString(); + } + } + +} diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java new file mode 100644 index 00000000..1fea0874 --- /dev/null +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.CityName.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class CityName { + + private String name; + + public CityName(String name) { + this.name = notNull(name, "name may not be empty").trim(); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + CityName city = (CityName)object; + + return toString().equals(city.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public CityName adaptFromJson(String name) throws Exception { + return new CityName(name); + } + + @Override + public String adaptToJson(CityName name) throws Exception { + return name.toString(); + } + } + +} diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java new file mode 100644 index 00000000..e54c14c7 --- /dev/null +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java @@ -0,0 +1,81 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notBlank; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.HouseNumber.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class HouseNumber { + + private String number; + + protected HouseNumber() { + // for frameworks + } + + public HouseNumber(String number) { + this.number = notBlank(number, "number may not be empty").trim(); + } + + public static HouseNumber valueOf(String number) { + return new HouseNumber(number); + } + + @Override + public String toString() { + return number; + } + + @Override + public int hashCode() { + return number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof HouseNumber)) { + return false; + } + + HouseNumber houseNumber = (HouseNumber)object; + + return toString().equals(houseNumber.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public HouseNumber adaptFromJson(String number) throws Exception { + return new HouseNumber(number); + } + + @Override + public String adaptToJson(HouseNumber number) throws Exception { + return number.toString(); + } + + } + +} diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/Street.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/Street.java new file mode 100644 index 00000000..bc0af1cd --- /dev/null +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/Street.java @@ -0,0 +1,74 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.annotation.JsonbCreator; +import javax.json.bind.annotation.JsonbProperty; + +public class Street { + + private StreetName name; + private HouseNumber number; + + @JsonbCreator + public Street(@JsonbProperty("name") StreetName name, @JsonbProperty("number") HouseNumber houseNumber) { + this.name = notNull(name, "name may not be null"); + this.number = notNull(houseNumber, "house number may not be null"); + } + + public StreetName getName() { + return name; + } + + public HouseNumber getNumber() { + return number; + } + + @Override + public int hashCode() { + return name.hashCode() ^ number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof Street)) { + return false; + } + + Street street = (Street)object; + + return name.equals(street.getName()) && number.equals(street.getNumber()); + } + + @Override + public String toString() { + if (isEnglish()) { + return number + " " + name; + } else { + return name + " " + number; + } + } + + private boolean isEnglish() { + return name.toString().toLowerCase().contains("street"); + } +} diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java new file mode 100644 index 00000000..16e7ae01 --- /dev/null +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notBlank; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.StreetName.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class StreetName { + + private String name; + + protected StreetName() { + // for frameworks + } + + public StreetName(String name) { + this.name = notBlank(name, "name may not be empty").trim(); + } + + public static StreetName valueOf(String name) { + return new StreetName(name); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof StreetName streetName)) { + return false; + } + + return toString().equals(streetName.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public StreetName adaptFromJson(String name) throws Exception { + return new StreetName(name); + } + + @Override + public String adaptToJson(StreetName name) throws Exception { + return name.toString(); + } + } +} diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java new file mode 100644 index 00000000..1e62563b --- /dev/null +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.ZipCode.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class ZipCode { + + private String code; + + public ZipCode(String code) { + this.code = notNull(code, "code may not be empty").trim(); + } + + public boolean isGerman() { + return code.length() == 5; + } + + @Override + public String toString() { + return code; + } + + @Override + public int hashCode() { + return code.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + ZipCode zipCode = (ZipCode)object; + + return toString().equals(zipCode.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public ZipCode adaptFromJson(String zip) throws Exception { + return new ZipCode(zip); + } + + @Override + public String adaptToJson(ZipCode zip) throws Exception { + return zip.toString(); + } + } + +} diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java new file mode 100644 index 00000000..8a84c0f2 --- /dev/null +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.infrastructure; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.ext.Provider; + +/** + * Filter to allow cross origin calls. + */ +@Provider +public class CorsFilter implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { + responseContext.getHeaders().add("Access-Control-Allow-Origin", "*"); + responseContext.getHeaders().add("Access-Control-Allow-Headers", "origin, content-type, accept, authorization"); + responseContext.getHeaders().add("Access-Control-Allow-Credentials", "true"); + responseContext.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD"); + responseContext.getHeaders().add("Access-Control-Max-Age", "1209600"); + } +} diff --git a/address-validation-service/src/main/resources/plz.txt b/address-validation-service/src/main/resources/plz.txt new file mode 100644 index 00000000..33652500 --- /dev/null +++ b/address-validation-service/src/main/resources/plz.txt @@ -0,0 +1,8219 @@ +04931 Mühlberg, Bad Liebenwerda +04932 Röderland, Großthiemig u.a. +04916 Herzberg/ Elster +03253 Doberlug-Kirchhain +04895 Falkenberg/ Elster +04938 Uebigau-Wahrenbrück +04924 Bad Liebenwerda +19309 Lenzen +19348 Perleberg, Berge u.a. +19322 Wittenberge, Rühstädt +19357 Karstädt, Dambeck, Klüß +14827 Wiesenburg +14806 Belzig +14797 Lehnin +14823 Niemegk +14778 Beetzsee, Wollin, Wenzlow, Golzow u.a. +14715 Milower Land, Schollene, Nennhausen u.a. +14776 Brandenburg/Havel +14828 Görzke +14793 Ziesar +14774 Brandenburg/ Havel +14789 Wusterwitz, Rosenau, Bensdorf +14798 Havelsee +14772 Brandenburg/ Havel +14770 Brandenburg/ Havel +15936 Dahme u.a. +15837 Baruth +15938 Golßen +12529 Schönefeld +14913 Jüterbog +14947 Nuthe-Urstromtal +14547 Beelitz +14929 Treuenbrietzen +14550 Groß Kreutz +04936 Schlieben +14822 Brück, Borkheide u.a. +14552 Michendorf +14542 Werder/ Havel +14554 Seddinger See +14548 Schwielowswee +14476 Potsdam +14469 Potsdam +14471 Potsdam +14959 Trebbin +14943 Luckenwalde +15806 Zossen +15838 Am Mellensee +14974 Ludwigsfelde +14532 Kleinmachnow +14482 Potsdam +14467 Potsdam +14558 Nuthetal +14473 Potsdam +14478 Potsdam +14480 Potsdam +15831 Blankenfelde-Mahlow +15834 Rangsdorf +15827 Blankenfelde-Mahlow +14979 Großbeeren +14513 Teltow +12209 Berlin-Lichterfelde +16928 Pritzwalk, Groß Pankow +16949 Triglitz, Putlitz +16945 Meyenburg, Kümmernitztal u.a. +16845 Neustadt (Dosse) u.a. +16909 Wittstock/Dosse, Heiligengrabe +16818 Fehrbellin, Temnitzquell, Märkisch Linden u.a. +16866 Gumtow, Kyritz u.a. +14662 Friesack +19339 Plattenburg +14727 Premnitz +14712 Rathenow +14728 Rhinow +19336 Legde/Quitzöbel, Bad Wilsnack +16868 Wusterhausen +14669 Ketzin +16831 Rheinsberg +16835 Lindow u.a. +16775 Gransee, Löwenberg +16348 Wandlitz +16727 Velten, Oberkrämer +17268 Templin, Boitzenburg u.a. +14621 Schönwalde +14641 Nauen +16766 Kremmen +16244 Schorfheide +16792 Zehdenick +16798 Fürstenberg +16515 Oranienburg, Mühlenbecker Land +14656 Brieselang +14612 Falkensee +14624 Dallgow-Döberitz +16833 Fehrbellin +16816 Neuruppin +16761 Hennigsdorf +16540 Hohen Neuendorf +13465 Berlin Frohnau +16567 Mühlenbecker Land +16548 Glienicke/Nordbahn +16552 Mühlenbecker Land +16556 Borgsdorf +16767 Leegebruch +16559 Liebenwalde +16547 Birkenwerder +16562 Hohen Neuendorf OT Bergfelde +16827 Neuruppin +16837 Rheinsberg +17279 Lychen +01945 Ruhland +01983 Großräschen +01979 Lauchhammer +03130 Spremberg, Tschernitz u.a. +03238 Finsterwalde +01990 Ortrand +04934 Hohenleipisch +04928 Plessa, Schraden +04910 Elsterwerda +01987 Schwarzheide N.L. +01998 Schipkau +01968 Senftenberg +01994 Schipkau +01996 Senftenberg +01993 Schipkau +03116 Drebkau +03103 Neu-Seeland, Neupetershain +03119 Welzow +03159 Döbern +03222 Lübbenau/ Spreewald +03249 Sonnewalde +03205 Calau, Bronkow +03185 Peitz +15306 Seelow, Lietzen u.a. +15746 Groß Köris +15757 Halbe +15913 Straupitz +15926 Luckau, Waldrehna, Heideblick, Fürstlich Drehna +15848 Beeskow +15518 Briesen, Rauen u.a. +15528 Spreenhagen +15859 Storkow +15537 Erkner +03246 Crinitz +15907 Lübben (Spreewald) +15910 Schönwalde +03229 Altdöbern, Luckaitztal +03226 Vetschau +03099 Kolkwitz +03096 Burg/Spreewald u.a. +15868 Lieberose +15749 Mittenwalde +15741 Bestensee +15755 Teupitz +15748 Märkisch Buchholz +15754 Heidesee +15745 Wildau +15738 Zeuthen +15711 Königs Wusterhausen +15732 Schulzendorf b. Eichenwade +15562 Rüdersdorf +15569 Woltersdorf +15713 Königs Wusterhausen +15712 Königs Wusterhausen +15526 Bad Saarow-Pieskow +15864 Wendisch Rietz +15517 Fürstenwalde/ Spree +03044 Cottbus +03197 Jänschwalde +03055 Cottbus +03042 Cottbus +03054 Cottbus +03046 Cottbus +03051 Cottbus +03052 Cottbus +03172 Guben, Schenkendöbern +15295 Brieskow-Finkenheerd +15890 Eisenhüttenstadt +15236 Treplin, Jacobsdorf, Frankfurt (Oder) +03058 Neuhausen/Spree +03048 Cottbus +03050 Cottbus +03053 Cottbus +03149 Forst/ Lausitz +15898 Neuzelle +15299 Müllrose +15234 Frankfurt/ Oder +15326 Zeschdorf, Podelzig, Lebus +15230 Frankfurt/ Oder +15232 Frankfurt/ Oder +15366 Neuenhagen, Hoppegarten +12623 Berlin Mahlsdorf +16341 Panketal +16230 Melchow, Chorin u.a. +16248 Oderberg u.a. +16356 Werneuchen +16321 Bernau +16247 Joachimsthal u.a. +16269 Wriezen +16259 Bad Freienwalde u.a. +15345 Lichtenow, Altlandsberg u.a. +16278 Angermünde +16306 Biesendahlshof, Berkholz-Meyenburg +17291 Prenzlau, Nordwestuckermark u.a. +15374 Müncheberg +16303 Schwedt +15370 Fredersdorf-Vogelsdorf, Petershagen +15378 Rüdersdorf +15566 Schöneiche bei Berlin +15344 Strausberg +16359 Biesenthal +16225 Eberswalde +16227 Eberswalde +15377 Oberbarnim, Märkische Höhe u.a. +15320 Neutrebbin, Neuhardenberg +15328 Golzow, Zechin u.a. +15324 Letschin +16307 Gartz (Oder) +17337 Uckerland, Groß Luckow, Schönhausen +17326 Brüssow +14109 Berlin Wannsee +14129 Berlin Nikolassee +14089 Berlin Gatow +12307 Berlin +12277 Berlin +12107 Berlin Mariendorf +14195 Berlin Dahlem +12105 Berlin Mariendorf +12109 Berlin Mariendorf +14167 Berlin Zehlendorf +12347 Berlin Britz +14169 Berlin Zehlendorf +14165 Berlin Zehlendorf +14163 Berlin Zehlendorf +12209 Berlin-Lichterfelde +12207 Berlin Lichtenfelde +12205 Berlin Lichtenfelde +12203 Berlin Lichtenfelde +12309 Berlin +12305 Berlin +12249 Berlin Lankwitz +12279 Berlin +12247 Berlin Lankwitz +12167 Berlin Steglitz +12349 Berlin Buckow +14055 Berlin Westend +14193 Berlin Grunewald +13587 Berlin Hakenfelde +13585 Berlin Spandau +13503 Berlin Tegel +13589 Berlin Falkenhagener Feld +13505 Berlin Tegel +13599 Berlin Haselhorst +13581 Berlin Spandau +13593 Berlin Wilhelmstadt +13591 Berlin Staaken +13583 Berlin Spandau +13597 Berlin Spandau +13595 Berlin Wlhelmstadt +14050 Berlin Westend +10249 Berlin Friedrichshain +10965 Berlin Kreuzberg +10115 Berlin Mitte +10785 Berlin Tiergarten +13158 Berlin Rosenthal +13127 Berlin Französisch Buchholz +13159 Berlin Blankenfelde +13359 Berlin Gesundbrunnen +13357 Berlin Gesundbrunnen +13469 Berlin Lübars +13465 Berlin Frohnau +13467 Berlin Hermsdorf +13405 Berlin Wedding +13437 Berlin Reinickendorf +13507 Berlin Tegel +12165 Berlin Steglitz +12163 Berlin Steglitz +14197 Berlin Wilmersdorf +14199 Berlin Schmargendorf +10709 Berlin Wilmersdorf +10711 Berlin Halensee +10713 Berlin Wilmersdorf +10707 Berlin Wilmersdorf +14052 Berlin Westend +14053 Berlin Westend +13629 Berlin Siemensstadt +14057 Berlin Charlottenburg +14059 Berlin Charlottenburg +10553 Berlin Moabit +10629 Berlin Charlottenburg +10627 Berlin Charlottenburg +10587 Berlin Charlottenburg +10625 Berlin Charlottenburg +10589 Berlin Charlottenburg +10585 Berlin Charlottenburg +13351 Berlin Wedding +13627 Berlin Charlottenburg-Nord +12169 Berlin Steglitz +12157 Berlin Schöneberg +12161 Berlin Friedenau +12159 Berlin Friedenau +10715 Berlin Wilhelmsdorf +10827 Berlin +10825 Berlin +10829 Berlin Schöneberg +12103 Berlin Tempelhof +12101 Berlin Tempelhof +10823 Berlin-West +10719 Berlin Wilmersdorf +10717 Berlin Wilmersdorf +10779 Berlin Schöneberg +10777 Berlin Wilmersdorf +10783 Berlin Schöneberg +10781 Berlin Schöneberg +12099 Berlin Tempelhof +12051 Berlin Neukölln +12047 Berlin Kreuzberg +12049 Berlin Neukölln +10999 Berlin Kreuzberg +10967 Berlin Kreuzberg +10961 Berlin Kreuzberg +10969 Berlin Kreuzberg +10997 Berlin Kreuzberg +10963 Berlin Kreuzberg +10551 Berlin Moabit +10555 Berlin Moabit +10557 Berlin Moabit +10559 Berlin Moabit +10623 Berlin Charlottenburg +10787 Berlin Tiergarten +13353 Berlin Wedding +13347 Berlin Wedding +10789 Berlin Schöneberg +10405 Berlin Prenzlauer Berg +10437 Berlin Prenzlauer Berg +10439 Berlin Prenzlauer Berg +10435 Berlin Prenzlauer Berg +10178 Berlin Mitte +10179 Berlin Mitte +10119 Berlin Mitte +10117 Berlin Mitte +13355 Berlin Wedding +13403 Berlin Reinickendorf +13509 Berlin Reinickendorf +13349 Berlin Wedding +13435 Berlin Märkisches Viertel +13407 Berlin-West +13409 Berlin-West +13439 Berlin Märkisches Viertel +13189 Berlin Pankow +13156 Berlin Niederschönhausen +13187 Berlin Pankow +15537 Erkner +12557 Berlin +12559 Berlin Köpenick +12524 Berlin Altglienicke +12527 Berlin Schmöckwitz +12587 Berlin Wiesengrund +12526 Berlin Bohnsdorf +12555 Berlin Köpenik +12489 Berlin Teltowkanal III +12439 Berlin Niederschöneweide +12355 Berlin Rudow +12357 Berlin Rudow +12437 Berlin Baumschulenweg +12353 Berlin Gropiusstadt +12351 Berlin Buckow +12359 Berlin Britz +12487 Berlin +12589 Berlin Rahnsdorf +12435 Berlin Alt Treptow +12057 Berlin Neukölln +12045 Berlin Neukölln +12053 Berlin Neukölln +12059 Berlin Neukölln +12055 Berlin Neukölln +12043 Berlin Neukölln +10245 Berlin Friedrichshain +12623 Berlin Mahlsdorf +12621 Berlin Kaulsdorf +12619 Berlin +12689 Berlin +12681 Berlin +12679 Berlin +12459 Berlin Oberschöneweide +10318 Berlin Karlshorst +10319 Berlin +10317 Berlin Rummelsburg +10365 Berlin Lichtenberg +13055 Berlin Alt-Hohenschönhausen +13053 Berlin Alt-Hohenschönhausen +13088 Berlin Weißensee +13057 Berlin Falkenberg +13125 Berlin Buch +10367 Berlin Lichtenberg +10369 Berlin Lichtenberg +10247 Berlin Friedrichshain +10243 Berlin Friedrichshain +10407 Berlin Prenzlauer Berg +10409 Berlin Prenzlauer Berg +13086 Berlin Weißensee +10315 Berlin Friedrichsfelde +12683 Berlin Biesdorf +12685 Berlin +12629 Berlin +12627 Berlin Hellersdorf +13089 Berlin Heinelsdorf +13129 Berlin Blankenburg +13059 Berlin Wartenberg +13051 Berlin Neu-Schönhausen +12687 Berlin +79588 Efringen-Kirchen +79415 Bad Bellingen +79395 Neuenburg am Rhein +79539 Lörrach +79541 Lörrach +79576 Weil am Rhein +79589 Binzen +79585 Steinen +79594 Inzlingen +79618 Rheinfelden (Baden) +79639 Grenzach-Wyhlen +78713 Schramberg +78052 Villingen-Schwenningen +77955 Ettenheim +79761 Waldshut-Tiengen +79341 Kenzingen +79336 Herbolzheim +78166 Donaueschingen +78199 Bräunlingen +77761 Schiltach +79591 Eimeldingen +79540 Lörrach +79650 Schopfheim +79664 Wehr +79736 Rickenbach +79713 Bad Säckingen +79739 Schwörstadt +79592 Fischingen +79400 Kandern +79429 Malsburg-Marzell +79418 Schliengen +79692 Kleines Wiesental +79424 Auggen +79379 Müllheim +79689 Maulburg +79244 Münstertal +79219 Staufen im Breisgau +79295 Sulzburg +79410 Badenweiler +79597 Schallbach +79595 Rümmingen +79599 Wittlingen +79677 Schönau im Schwarzwald +79669 Zell im Wiesental +79674 Todtnau +79685 Häg-Ehrsberg +79688 Hausen im Wiesental +79737 Herrischried +79682 Todtmoos +79694 Utzenfeld +79695 Wieden +79686 Hasel +79725 Laufenburg (Baden) +79730 Murg +79733 Görwihl +79774 Albbruck +79804 Dogern +79790 Küssaberg +79801 Hohentengen am Hochrhein +79771 Klettgau +79787 Lauchringen +79837 St. Blasien, Ibach +79875 Dachsberg +79862 Höchenschwand +79809 Weilheim +79872 Bernau im Schwarzwald +79859 Schluchsee +79793 Wutöschingen +79805 Eggingen +79777 Ühlingen-Birkendorf +79865 Grafenhausen +79848 Bonndorf im Schwarzwald +79780 Stühlingen +79879 Wutach +79112 Freiburg im Breisgau +79111 Freiburg im Breisgau +79206 Breisach am Rhein +79426 Buggingen +79423 Heitersheim +79427 Eschbach +79258 Hartheim +79189 Bad Krozingen +79227 Schallstadt +79238 Ehrenkirchen +79232 March +79224 Umkirch +79288 Gottenheim +79241 Ihringen +79282 Ballrechten-Dottingen +79292 Pfaffenweiler +79291 Merdingen +79100 Freiburg im Breisgau +79117 Freiburg im Breisgau +79104 Freiburg im Breisgau +79108 Freiburg im Breisgau +79194 Gundelfingen, Heuweiler +79254 Oberried +79283 Bollschweil +79299 Wittnau +79289 Horben +79280 Au (Breisgau) +79199 Kirchzarten +79252 Stegen +79286 Glottertal +79285 Ebringen +79294 Sölden +79115 Freiburg im Breisgau +79114 Freiburg im Breisgau +79249 Merzhausen +79102 Freiburg im Breisgau +79110 Freiburg im Breisgau +79098 Freiburg im Breisgau +79106 Freiburg im Breisgau +77977 Rust +77975 Ringsheim +79356 Eichstetten +79235 Vogtsburg im Kaiserstuhl +79361 Sasbach am Kaiserstuhl +79346 Endingen am Kaiserstuhl +79353 Bahlingen am Kaiserstuhl +79331 Teningen +79365 Rheinhausen +79362 Forchheim +79369 Wyhl +79359 Riegel Kaiserstuhl +79367 Weisweil +79268 Bötzingen +79183 Waldkirch +79261 Gutach im Breisgau +79350 Sexau +79348 Freiamt +79211 Denzlingen +79312 Emmendingen +79279 Vörstetten +79276 Reute +79364 Malterdingen +77978 Schuttertal +79868 Feldberg +79853 Lenzkirch +79822 Titisee-Neustadt +79874 Breitnau +79856 Hinterzarten +79256 Buchenbach +79274 St. Märgen +79271 Sankt Peter +79263 Simonswald +78148 Gütenbach +78120 Furtwangen im Schwarzwald +79843 Löffingen +79877 Friedenweiler +79871 Eisenbach +78147 Vöhrenbach +77716 Fischerbach, Haslach, Hofstetten +79215 Elzach, Biederbach +77709 Wolfach, Oberwolfach +79297 Winden im Elztal +78141 Schönwald im Schwarzwald +78136 Schonach im Schwarzwald +77793 Gutach (Schwarzwaldbahn) +77796 Mühlenbach +77756 Hausach +77790 Steinach +78144 Schramberg +78098 Triberg im Schwarzwald +78089 Unterkirnach +78112 St. Georgen +78126 Königsfeld im Schwarzwald +78739 Hardt +78132 Hornberg +78730 Lauterbach +78733 Aichhalden +79802 Dettighofen +79807 Lottstetten +88605 Meßkirch, Sauldorf +88630 Pfullendorf +78176 Blumberg +78234 Engen +78187 Geisingen +72419 Neufra +78600 Kolbingen +72488 Sigmaringen +79798 Jestetten +78250 Tengen +78259 Mühlhausen-Ehingen +78247 Hilzingen +78244 Gottmadingen +78239 Rielasingen-Worblingen +78262 Gailingen am Hochrhein +78267 Aach +78224 Singen +78337 Öhningen +78345 Moos +78343 Gaienhofen +78315 Radolfzell am Bodensee +78256 Steißlingen +78359 Orsingen-Nenzingen +78333 Stockach +78351 Bodman-Ludwigshafen +78269 Volkertshausen +88682 Salem +88699 Frickingen +88690 Uhldingen-Mühlhofen +88696 Owingen +78467 Konstanz +78465 Konstanz +78464 Konstanz +88662 Überlingen +78476 Allensbach +78479 Reichenau +78462 Konstanz +78354 Sipplingen +78056 Villingen-Schwenningen +78050 Villingen-Schwenningen +78086 Brigachtal +78073 Bad Dürrheim +78609 Tuningen +78183 Hüfingen +78591 Durchhausen +78607 Talheim +78194 Immendingen +78532 Tuttlingen +78573 Wurmlingen +78604 Rietheim-Weilheim +78589 Dürbheim +78606 Seitingen-Oberflacht +78549 Spaichingen +78594 Gunningen +78647 Trossingen +78595 Hausen ob Verena +78048 Villingen-Schwenningen +78628 Rottweil +78667 Villingendorf +78652 Deißlingen +78078 Niedereschach +78655 Dunningen +78658 Zimmern ob Rottweil +78664 Eschbronn +78736 Epfendorf +78662 Bösingen +78087 Mönchweiler +78054 Villingen-Schwenningen +78083 Dauchingen +78564 Wehingen, Reichenbach +78582 Balgheim +78554 Aldingen +78588 Denkingen +78559 Gosheim +78585 Bubsheim +72336 Balingen +72348 Rosenfeld +78583 Böttingen +78601 Mahlstetten +72365 Ratshausen +72355 Schömberg +72356 Dautmergen +72369 Zimmern unter der Burg +78661 Dietingen +78586 Deilingen +78669 Wellendingen +78665 Frittlingen +72367 Weilen unter den Rinnen +72358 Dormettingen +72359 Dotternhausen +88637 Leibertingen, Buchheim +88631 Beuron +78253 Eigeltingen +78357 Mühlingen +78576 Emmingen-Liptingen +78579 Neuhausen ob Eck +78567 Fridingen an der Donau +78570 Mühlheim an der Donau +88634 Herdwangen-Schönach +88639 Wald +78355 Hohenfels +72505 Krauchenwies +72514 Inzigkofen +72461 Albstadt +72458 Albstadt +72459 Albstadt +78592 Egesheim +78598 Königsheim +78603 Renquishausen +78580 Bärenthal +78597 Irndorf +72362 Nusplingen +72469 Meßstetten +72477 Schwenningen +72510 Stetten am kalten Markt +72364 Obernheim +72361 Hausen am Tann +72475 Bitz +72501 Gammertingen +72513 Hettingen +72474 Winterlingen +72479 Straßberg +72519 Veringenstadt +72511 Bingen +88368 Bergatreute +88239 Wangen im Allgäu +88633 Heiligenberg +88276 Berg +88410 Bad Wurzach +88427 Bad Schussenried +88529 Zwiefalten +89584 Ehingen (Donau), Lauterach +88512 Mengen +88518 Herbertingen +72534 Hayingen +88069 Tettnang +88079 Kressbronn am Bodensee +88085 Langenargen +88097 Eriskirch +88271 Wilhelmsdorf +88709 Meersburg +88045 Friedrichshafen +88048 Friedrichshafen +88636 Illmensee +88693 Deggenhausertal +88263 Horgenzell +88677 Markdorf +88094 Oberteuringen +88697 Bermatingen +88090 Immenstaad am Bodensee +88718 Daisendorf +88719 Stetten +88250 Weingarten +88212 Ravensburg +88074 Meckenbeuren +88046 Friedrichshafen +88255 Baienfurt +88214 Ravensburg +88287 Grünkraut +88213 Ravensburg +88281 Schlier +88147 Achberg +88167 Röthenbach (Allgäu) +88289 Waldburg +88279 Amtzell +88353 Kißlegg +88267 Vogt +88364 Wolfegg +88099 Neukirch +88285 Bodnegg +88299 Leutkirch im Allgäu +88260 Argenbühl +88316 Isny im Allgäu +88374 Hoßkirch +88348 Bad Saulgau, Allmannsweiler +88356 Ostrach +88367 Hohentengen +88377 Riedhausen +88376 Königseggwald +88373 Fleischwangen +88361 Altshausen +88371 Ebersbach-Musbach +88370 Ebenweiler +88379 Unterwaldhausen +88284 Wolpertswende +88273 Fronreute +88422 Bad Buchau +88326 Aulendorf +72517 Sigmaringendorf +88515 Langenenslingen +88521 Ertingen +88499 Riedlingen +72539 Pfronstetten +72516 Scheer +88527 Unlingen +88525 Dürmentingen +88524 Uttenweiler +89597 Munderkingen +89611 Obermarchtal +89613 Oberstadion +89607 Emerkingen +89617 Untermarchtal +88339 Bad Waldsee +88456 Ingoldingen +88444 Ummendorf +88436 Eberhardzell +88454 Hochdorf +88459 Tannheim +88319 Aitrach +88450 Berkheim +88430 Rot an der Rot +88416 Ochsenhausen +88457 Kirchdorf an der Iller +88317 Aichstetten +88448 Attenweiler +88447 Warthausen +88487 Mietingen +88433 Schemmerhofen +88400 Biberach an der Riß +88437 Maselheim +88471 Laupheim +89619 Unterstadion +88441 Mittelbiberach +89616 Rottenacker +89608 Griesingen +88489 Wain +88451 Dettingen an der Iller +88480 Achstetten +88477 Schwendi +88453 Erolzheim +88483 Burgrieden +88486 Kirchberg an der Iller +88481 Balzheim +88484 Gutenzell-Hürbel +89165 Dietenheim +89186 Illerrieden +89194 Schnürpflingen +77966 Kappel-Grafenhausen +77743 Neuried +77974 Meißenheim +77963 Schwanau +77740 Bad Peterstal-Griesbach +76596 Forbach +72250 Freudenstadt +77656 Offenburg +77654 Offenburg +77652 Offenburg +77972 Mahlberg +77933 Lahr/Schwarzwald +77971 Kippenheim +77960 Seelbach +77791 Berghaupten +77749 Hohberg +77948 Friesenheim +77746 Schutterwald +77770 Durbach +77797 Ohlsbach +77799 Ortenberg +77855 Achern (Abweichung Exklaven) +77866 Rheinau +77871 Renchen +77767 Appenweier +77731 Willstätt +77694 Kehl +77704 Oberkirch +77781 Biberach +77736 Zell am Harmersbach +77723 Gengenbach +77784 Oberharmersbach +77787 Nordrach +77728 Oppenau +77776 Bad Rippoldsau-Schapbach +77773 Schenkenzell +72275 Alpirsbach +72290 Loßburg +76534 Baden-Baden +77833 Ottersweier +77815 Bühl +77830 Bühlertal +77880 Sasbach +77887 Sasbachwalden +77886 Lauf +77876 Kappelrodeck +77794 Lautenbach +77883 Ottenhöfen im Schwarzwald +77889 Seebach +76593 Gernsbach +72297 Seewald +72270 Baiersbronn +77836 Rheinmünster +77839 Lichtenau +76532 Baden-Baden +76547 Sinzheim +76437 Rastatt +76473 Iffezheim +76549 Hügelsheim +76332 Bad Herrenalb +76597 Loffenau +76530 Baden-Baden +76316 Malsch +76275 Ettlingen +76571 Gaggenau +76461 Muggensturm +76470 Ötigheim +76456 Kuppenheim +76599 Weisenbach +76476 Bischweier +76479 Steinmauern +76477 Elchesheim-Illingen +76187 Karlsruhe +76149 Karlsruhe +76287 Rheinstetten +76185 Karlsruhe +76137 Karlsruhe +76199 Karlsruhe +76344 Eggenstein-Leopoldshafen +76189 Karlsruhe +76467 Bietigheim +76448 Durmersheim +76474 Au am Rhein +76135 Karlsruhe +76133 Karlsruhe +76661 Philippsburg +76351 Linkenheim-Hochstetten +76706 Dettenheim +78727 Oberndorf am Neckar +78737 Fluorn-Winzeln +72172 Sulz am Neckar +72280 Dornstetten +72296 Schopfloch +72293 Glatten +72175 Dornhan +71032 Böblingen +74336 Brackenheim +72072 Tübingen +72818 Trochtelfingen +72116 Mössingen +72108 Rottenburg am Neckar +72160 Horb am Neckar +72184 Eutingen im Gäu +72178 Waldachtal +72401 Haigerloch +72351 Geislingen +72181 Starzach +72186 Empfingen +72189 Vöhringen +75337 Enzklösterle +72226 Simmersfeld +72213 Altensteig +72294 Grömbach +72227 Egenhausen +72221 Haiterbach +72285 Pfalzgrafenweiler +75323 Bad Wildbad +75389 Neuweiler +72299 Wörnersberg +75387 Neubulach +75391 Gechingen +72202 Nagold +71083 Herrenberg +71126 Gäufelden +71131 Jettingen +71149 Bondorf +71159 Mötzingen +72218 Wildberg +72224 Ebhausen +72229 Rohrdorf +75392 Deckenpfronn +75385 Bad Teinach-Zavelstein +75365 Calw +72131 Ofterdingen +72144 Dußlingen +72145 Hirrlingen +72411 Bodelshausen +72379 Hechingen +72417 Jungingen +72406 Bisingen +72415 Grosselfingen +72414 Rangendingen +72762 Reutlingen +72770 Reutlingen +72793 Pfullingen +72805 Lichtenstein +72820 Sonnenbühl +72147 Nehren +72810 Gomaringen +72393 Burladingen +72149 Neustetten +71034 Böblingen +72070 Tübingen +72076 Tübingen +71088 Holzgerlingen +71155 Altdorf +72119 Ammerbuch +71116 Gärtringen +71134 Aidlingen +71154 Nufringen +71157 Hildrizhausen +71139 Ehningen +70771 Leinfelden-Echterdingen +70794 Filderstadt +72074 Tübingen +72766 Reutlingen +72768 Reutlingen +72127 Kusterdingen +72138 Kirchentellinsfurt +72124 Pliezhausen +72827 Wannweil +72654 Neckartenzlingen +72141 Walddorfhäslach +72657 Altenriet +72631 Aichtal +71111 Waldenbuch +71093 Weil im Schönbuch +71101 Schönaich +72764 Reutlingen +72760 Reutlingen +72135 Dettenhausen +71144 Steinenbronn +72667 Schlaitdorf +76359 Marxzell +76307 Karlsbad +75334 Straubenhardt +75305 Neuenbürg +75210 Keltern +75217 Birkenfeld +75335 Dobel +75328 Schömberg +75394 Oberreichenbach +75339 Höfen an der Enz +75449 Wurmberg +75181 Pforzheim +75173 Pforzheim +75179 Pforzheim +75180 Pforzheim +75233 Tiefenbronn +75175 Pforzheim +75331 Engelsbrand +71299 Wimsheim +75378 Bad Liebenzell +75242 Neuhausen +71292 Friolzheim +75382 Althengstett +75397 Simmozheim +71263 Weil der Stadt +75399 Unterreichenbach +76131 Karlsruhe +76228 Karlsruhe +76139 Karlsruhe +76327 Pfinztal +76227 Karlsruhe +76646 Bruchsal +76356 Weingarten +76337 Waldbronn +76297 Stutensee +76229 Karlsruhe +75196 Remchingen +75045 Walzbachtal +75203 Königsbach-Stein +75236 Kämpfelbach +76703 Kraichtal +75438 Knittlingen +75433 Maulbronn +75223 Niefern-Öschelbronn +75015 Bretten +75228 Ispringen +75249 Kieselbronn +75038 Oberderdingen +75417 Mühlacker +75248 Ölbronn-Dürrn +75245 Neulingen +75053 Gondelsheim +75239 Eisingen +75059 Zaisenhausen +75177 Pforzheim +75443 Ötisheim +75172 Pforzheim +71065 Sindelfingen +71067 Sindelfingen +71069 Sindelfingen +75446 Wiernsheim +71106 Magstadt +71229 Leonberg +71272 Renningen +71277 Rutesheim +71287 Weissach +71254 Ditzingen +71282 Hemmingen +71735 Eberdingen +71297 Mönsheim +75395 Ostelsheim +71120 Grafenau +71063 Sindelfingen +71296 Heimsheim +70599 Stuttgart +70327 Stuttgart +70329 Stuttgart +70184 Stuttgart +70839 Gerlingen +70569 Stuttgart +70197 Stuttgart +70825 Korntal-Münchingen +70567 Stuttgart +70565 Stuttgart +70195 Stuttgart +70806 Kornwestheim +70437 Stuttgart +71638 Ludwigsburg +71636 Ludwigsburg +71686 Remseck am Neckar +73760 Ostfildern +71696 Möglingen +71701 Schwieberdingen +70563 Stuttgart +70629 Stuttgart/Leinfelden-Echterdingen +70173 Stuttgart +70597 Stuttgart +70199 Stuttgart +70180 Stuttgart +70178 Stuttgart +70193 Stuttgart +70176 Stuttgart +70619 Stuttgart +70186 Stuttgart +70182 Stuttgart +70499 Stuttgart +70435 Stuttgart +70192 Stuttgart +70174 Stuttgart +70191 Stuttgart +70469 Stuttgart +70378 Stuttgart +70376 Stuttgart +70190 Stuttgart +70374 Stuttgart +70188 Stuttgart +70372 Stuttgart +70439 Stuttgart +74363 Güglingen +74389 Cleebronn +74343 Sachsenheim +74357 Bönnigheim +74397 Pfaffenhofen +74374 Zaberfeld +75031 Eppingen +75057 Kürnbach +75056 Sulzfeld +75428 Illingen +75447 Sternenfels +71706 Markgröningen +71665 Vaihingen an der Enz +71739 Oberriexingen +74372 Sersheim +74395 Mundelsheim +74385 Pleidelsheim +74321 Bietigheim-Bissingen +74379 Ingersheim +74382 Neckarwestheim +74348 Lauffen am Neckar +74223 Flein +74360 Ilsfeld +74388 Talheim +74369 Löchgau +74354 Besigheim +74399 Walheim +74366 Kirchheim am Neckar +74391 Erligheim +71642 Ludwigsburg +71672 Marbach am Neckar +71711 Steinheim, Murr +71691 Freiberg am Neckar +71732 Tamm +71634 Ludwigsburg +71679 Asperg +71640 Ludwigsburg +71726 Benningen am Neckar +74392 Freudental +74394 Hessigheim +74376 Gemmrigheim +72800 Eningen +72813 St. Johann +72532 Gomadingen +72829 Engstingen +72531 Hohenstein +72574 Bad Urach +89079 Ulm +89150 Laichingen +89160 Dornstadt +89558 Böhmenkirch +72525 Münsingen +73453 Abtsgmünd +73252 Lenningen +74426 Bühlerzell +73479 Ellwangen (Jagst) +73489 Jagstzell +74564 Crailsheim +74523 Schwäbisch Hall +74545 Michelfeld +74535 Mainhardt +89604 Allmendingen +89601 Schelklingen +72537 Mehrstetten +72535 Heroldstatt +72581 Dettingen an der Erms +72555 Metzingen +72658 Bempflingen +72582 Grabenstetten +72584 Hülben +72639 Neuffen +73277 Owen +72636 Frickenhausen +72660 Beuren +72622 Nürtingen +72666 Neckartailfingen +73230 Kirchheim unter Teck +73240 Wendlingen am Neckar +73249 Wernau (Neckar) +73257 Köngen +73765 Neuhausen auf den Fildern +73770 Denkendorf +72649 Wolfschlugen +73274 Notzingen +72585 Riederich +72664 Kohlberg +72661 Grafenberg +73268 Erkenbrechtsweiler +72655 Altdorf +72663 Großbettlingen +72669 Unterensingen +73265 Dettingen unter Teck +72644 Oberboihingen +73037 Göppingen +73035 Göppingen +73345 Hohenstadt/Drackenstein +72587 Römerstein +72589 Westerheim +73349 Wiesensteig +73235 Weilheim an der Teck +73272 Neidlingen +73344 Gruibingen +73347 Mühlhausen im Täle +73092 Heiningen +73087 Bad Boll +73101 Aichelberg +73266 Bissingen an der Teck +73278 Schlierbach +73110 Hattenhofen +73275 Ohmden +73271 Holzmaden +73119 Zell unter Aichelberg +73105 Dürnau +73108 Gammelshausen +89143 Blaubeuren +89155 Erbach +89605 Altheim +89180 Berghülen +89614 Öpfingen +89134 Blaustein +89610 Oberdischingen +89077 Ulm +89195 Staig +89171 Illerkirchberg +89129 Langenau +89081 Ulm +89075 Ulm +89185 Hüttisheim +89073 Ulm +73072 Donzdorf +89191 Nellingen +89173 Lonsee +89188 Merklingen +73340 Amstetten +73342 Bad Ditzenbach +73312 Geislingen an der Steige +73326 Deggingen +73079 Süßen +73329 Kuchen +73337 Bad Überkingen +73333 Gingen an der Fils +73114 Schlat +73107 Eschenbach +89177 Ballendorf +89179 Beimerstetten +89197 Weidenstetten +89189 Neenstetten +89183 Breitingen +89174 Altheim (Alb) +89555 Steinheim am Albuch +89547 Gerstetten +89198 Westerstetten +89182 Bernstadt +73734 Esslingen am Neckar +73730 Esslingen am Neckar +73733 Esslingen am Neckar +70736 Fellbach +70734 Fellbach +71336 Waiblingen +71334 Waiblingen +73663 Berglen +71404 Korb +73207 Plochingen +73779 Deizisau +71384 Weinstadt +71394 Kernen im Remstal +73666 Baltmannsweiler +73650 Winterbach +73630 Remshalden +71364 Winnenden +73728 Esslingen am Neckar +73732 Esslingen am Neckar +73776 Altbach +73773 Aichwald +71332 Waiblingen +71409 Schwaikheim +73269 Hochdorf +73262 Reichenbach an der Fils +73033 Göppingen +73553 Alfdorf, Schillinghof +73642 Welzheim +73655 Plüderhausen +73660 Urbach +73614 Schorndorf +73061 Ebersbach an der Fils +73066 Uhingen +73095 Albershausen +73098 Rechberghausen +73099 Adelberg +73117 Wangen +73547 Lorch +73635 Rudersberg +73669 Lichtenwald +73104 Börtlingen +74245 Löwenstein +74232 Abstatt +74199 Untergruppenbach +71717 Beilstein +71543 Wüstenrot, Beilstein-Stocksberg +71729 Erdmannhausen +71737 Kirchberg +71720 Oberstenfeld +71723 Großbottwar +71522 Backnang +71397 Leutenbach +71563 Affalterbach +71576 Burgstetten +71546 Aspach +71579 Spiegelberg +71570 Oppenweiler +71573 Allmersbach im Tal +71540 Murrhardt +73667 Kaisersbach +71554 Weissach im Tal +71566 Althütte +71560 Sulzbach an der Murr +71577 Großerlach +71549 Auenwald +74538 Rosengarten +74420 Oberrot +73529 Stadt Schwäbisch Gmünd +73525 Stadt Schwäbisch Gmünd +73527 Schwäbisch Gmünd, Täferrot +73569 Eschach, Obergröningen +73550 Waldstetten +73113 Ottenbach +73111 Lauterstein +73565 Spraitbach +73054 Eislingen/Fils +73084 Salach +73557 Mutlangen +73575 Leinzell +73574 Iggingen +73577 Ruppertshofen +73102 Birenbach +73116 Wäschenbeuren +73568 Durlangen, Weggen-Ziegelhütte, Leinhäusle +73430 Aalen +73431 Aalen +73434 Aalen +73433 Aalen +89551 Königsbronn +73460 Hüttlingen +73457 Essingen +73540 Heubach +73566 Bartholomä +73579 Schechingen +73560 Böbingen an der Rems +73563 Mögglingen +73447 Oberkochen +73571 Göggingen +73572 Heuchlingen +74427 Fichtenberg +74405 Gaildorf +74417 Gschwend +74429 Sulzbach-Laufen +74423 Obersontheim +74541 Vellberg +74544 Michelbach an der Bilz +73491 Neuler +74424 Bühlertann +73486 Adelmannsfelden +73494 Rosenberg +74597 Stimpfach +74586 Frankenhardt +76698 Ubstadt-Weiher +76669 Bad Schönborn +76694 Forst +76689 Karlsdorf-Neuthard +76709 Kronau +76707 Hambrücken +76676 Graben-Neudorf +68753 Waghäusel +68794 Oberhausen-Rheinhausen +68789 Sankt Leon-Rot +68804 Altlußheim +69190 Walldorf +68799 Reilingen +74722 Buchen +68809 Neulußheim +76684 Östringen +69234 Dielheim +69168 Wiesloch +74939 Zuzenhausen +74889 Sinsheim +74918 Angelbachtal +69254 Malsch +69242 Mühlhausen +69231 Rauenberg +68782 Brühl +68542 Heddesheim +68219 Mannheim +68259 Mannheim +68723 Schwetzingen +68309 Mannheim +68775 Ketsch +68199 Mannheim +68239 Mannheim +68766 Hockenheim +68163 Mannheim +68526 Ladenburg +68169 Mannheim +68549 Ilvesheim +69123 Heidelberg +67166 Otterstadt +68159 Mannheim +68161 Mannheim +68167 Mannheim +68165 Mannheim +68535 Edingen-Neckarhausen +68229 Mannheim +69251 Gaiberg +69117 Heidelberg +69469 Weinheim +69198 Schriesheim +69221 Dossenheim +69257 Wiesenbach +69253 Heiligkreuzsteinach +69118 Heidelberg +69115 Heidelberg +69259 Wilhelmsfeld +69245 Bammental +69181 Leimen +69207 Sandhausen +69121 Heidelberg +69434 Hirschhorn, Brombach, Heddesbach +69151 Neckargemünd +69493 Hirschberg an der Bergstraße +69250 Schönau +74909 Meckesheim +69226 Nußloch +69214 Eppelheim +69124 Heidelberg +69126 Heidelberg +69256 Mauer +69120 Heidelberg +74193 Schwaigern +74252 Massenbachhausen +74906 Bad Rappenau +74930 Ittlingen +74912 Kirchardt +75050 Gemmingen +74915 Waibstadt +74924 Neckarbischofsheim +74927 Eschelbronn +74921 Helmstadt-Bargen +74177 Bad Friedrichshall +74211 Leingarten +74074 Heilbronn +74206 Bad Wimpfen +74226 Nordheim +74080 Heilbronn +74936 Siegelsbach +74078 Heilbronn +74172 Neckarsulm +74229 Oedheim +74081 Heilbronn +74831 Gundelsheim +74254 Offenau +74076 Heilbronn +74855 Haßmersheim +74928 Hüffenhardt +74842 Billigheim +74072 Heilbronn +74257 Untereisesheim +69412 Eberbach +69436 Schönbrunn +69439 Zwingenberg +74925 Epfenbach +74931 Lobbach +74858 Aglasterhausen +74867 Neunkirchen +74869 Schwarzach +74847 Obrigheim +74933 Neidenstein +74937 Spechbach +74934 Reichartshausen +69427 Mudau +69429 Waldbrunn +69437 Neckargerach +74821 Mosbach +74865 Neckarzimmern +74834 Elztal +74838 Limbach +74864 Fahrenbach +74862 Binau +68305 Mannheim +68519 Viernheim +68307 Mannheim +69514 Laudenbach +69502 Hemsbach +64646 Heppenheim (Bergstraße) +64754 Badisch Schöllenbach +74243 Langenbrettach +74249 Jagsthausen +74239 Hardthausen am Kocher +74248 Ellhofen +74219 Möckmühl +74189 Weinsberg +74861 Neudenau +74251 Lehrensteinsfeld +74259 Widdern +74246 Eberstadt +74182 Obersulm +74196 Neuenstadt am Kocher +74626 Bretzfeld +97877 Wertheim +97922 Lauda-Königshofen +97996 Niederstetten +97990 Weikersheim +74613 Öhringen +74673 Mulfingen +74572 Blaufelden +74575 Schrozberg +74214 Schöntal +74731 Walldürn +74736 Hardheim +74235 Erlenbach +74653 Künzelsau, Ingelfingen +74629 Pfedelbach +74635 Kupferzell +74632 Neuenstein +74638 Waldenburg +74679 Weißbach +74670 Forchtenberg +74676 Niedernhall +74639 Zweiflingen +74255 Roigheim +74850 Schefflenz +74706 Osterburken +74743 Seckach +74740 Adelsheim +74749 Rosenberg +97944 Boxberg +97959 Assamstadt +74238 Krautheim +74747 Ravenstein +74744 Ahorn +74532 Ilshofen +74542 Braunsbach +74549 Wolpertshausen +74547 Untermünkheim +74595 Langenburg +74585 Rot am See +74582 Gerabronn +74589 Satteldorf +74592 Kirchberg an der Jagst +74599 Wallhausen +97980 Bad Mergentheim +97999 Igersheim +74677 Dörzbach +97993 Creglingen +97896 Freudenberg, Collenberg +97900 Külsheim +63928 Eichenbühl +74746 Höpfingen +97941 Tauberbischofsheim +97956 Werbach +97953 Königheim +97957 Wittighausen +97947 Grünsfeld +97950 Großrinderfeld +73441 Bopfingen +73450 Neresheim +73485 Unterschneidheim +73495 Stödtlen +89176 Asselfingen +89192 Rammingen +89568 Hermaringen +89537 Giengen an der Brenz +89168 Niederstotzingen +89522 Heidenheim an der Brenz +89542 Herbrechtingen +89567 Sontheim an der Brenz +89561 Dischingen +89518 Heidenheim an der Brenz +73432 Aalen +89520 Heidenheim an der Brenz +89564 Nattheim +73463 Westhausen +73466 Lauchheim +73469 Riesbürg +73467 Kirchheim am Ries +73492 Rainau +73488 Ellenberg +73499 Wört +74579 Fichtenau +73497 Tannhausen +74594 Kreßberg +87538 Fischen im Allgäu +88145 Hergatz +87534 Oberstaufen +87452 Altusried +88142 Wasserburg (Bodensee) +88131 Lindau (Bodensee) +88149 Nonnenhorn +88138 Sigmarszell +88178 Heimenkirch +88175 Scheidegg +88167 Röthenbach (Allgäu) +88179 Oberreute +88171 Weiler-Simmerberg +87547 Missen-Wilhams +88161 Lindenberg im Allgäu +89257 Illertissen +89231 Neu-Ulm +89275 Elchingen +89250 Senden +89269 Vöhringen +89233 Neu-Ulm +89081 Ulm +63924 Kleinheubach, Rüdenau +63920 Großheubach +63916 Amorbach +63931 Kirchzell +63897 Miltenberg +63937 Weilbach +63856 Bessenbach +63762 Großostheim +63820 Elsenfeld +63743 Aschaffenburg +63849 Leidersbach +63853 Mömlingen +63785 Obernburg a.Main +63843 Niedernberg +63868 Großwallstadt +63834 Sulzbach am Main +63839 Kleinwallstadt +63840 Hausen +63933 Mönchberg +63906 Erlenbach a.Main +63939 Wörth a.Main +63934 Röllbach +63911 Klingenberg a. Main +63925 Laudenbach +97440 Werneck +97840 Hafenlohr, Rothenbuch +97845 Neustadt a. Main +97753 Karlstadt +97836 Bischbrunn +74731 Walldürn +97285 Röttingen, Tauberrettersheim +97243 Bieberehren +97896 Freudenberg, Collenberg +63936 Schneeberg +63928 Eichenbühl +63927 Bürgstadt +63930 Neunkirchen +97277 Neubrunn +63874 Dammbach +63872 Heimbuchenthal +97909 Stadtprozelten +97901 Altenbuch +97906 Faulbach +97852 Schollbrunn +63860 Rothenbuch, Rothenbucher Forst +63879 Weibersbrunn, Rohrbrunner Forst +63857 Waldaschaff, Waldaschaffer Forst +63875 Mespelbrunn +97903 Collenberg +97904 Dorfprozelten +63863 Eschau +97855 Triefenstein +97839 Esselbach +97828 Marktheidenfeld +97892 Kreuzwertheim +97854 Steinfeld +97837 Erlenbach b. Marktheidenfeld +97907 Hasloch +97851 Rothenfels +97849 Roden +97842 Karbach +97292 Üttingen, Holzkirchen +97264 Helmstadt +97270 Kist, Irtenberger Wald +97234 Reichenberg, Guttenberger Wald +97244 Bütthard +97237 Altertheim +97268 Kirchheim +97256 Geroldshausen +97271 Kleinrinderfeld +97084 Würzburg +97318 Kitzingen +97199 Ochsenfurt +97252 Frickenhausen +97286 Winterhausen +97255 Gelchsheim, Sonderhofen +97246 Eibelstadt +97239 Aub +97283 Riedenheim +97232 Giebelstadt +97253 Gaukönigshofen +97289 Thüngen +97267 Himmelstadt +97857 Urspringen +97225 Zellingen +97282 Retzstadt +97834 Birkenfeld +97291 Thüngershem +97276 Margetshöchheim +97280 Remlingen +97259 Greußenheim +97274 Leinach +97265 Hettstadt +97297 Waldbüttelbrunn +97204 Höchberg +97295 Waldbrunn, Irtenberger Wald +97249 Eisingen +97299 Zell a. Main +97250 Erlabrunn +97076 Würzburg +97078 Würzburg +97080 Würzburg +97082 Würzburg +97337 Dettelbach +97241 Bergtheim, Oberpleichfeld +97222 Rimpar +97074 Würzburg +97262 Hausen b. Würzburg +97294 Unterpleichfeld +97230 Estenfeld +97273 Kürnach +97228 Rottendorf +97236 Randersacker +97288 Theilheim +97209 Veitshöchheim +97261 Güntersleben +97070 Würzburg +97072 Würzburg +97218 Gerbrunn +82467 Garmisch-Partenkirchen +87541 Bad Hindelang +87561 Oberstdorf +6993 Mittelberg +82481 Mittenwald +82475 Garmisch-Partenkirchen (Schneefernerhaus) +87509 Immenstadt im Allgäu +87544 Blaichach +87545 Burgberg im Allgäu +87549 Rettenberg +87448 Waltenhofen +87527 Sonthofen +87637 Seeg +86399 Bobingen +86473 Ziemetshausen +86947 Weil +86971 Peiting +86807 Buchloe +86929 Penzing +86853 Langerringen +87480 Weitnau +87477 Sulzberg +87642 Halblech +87645 Schwangau +87463 Dietmannsried +87754 Kammlach +87459 Pfronten +87497 Wertach +87484 Nesselwang +87466 Oy-Mittelberg +87439 Kempten (Allgäu) +87435 Kempten (Allgäu) +87487 Wiggensbach +87474 Buchenberg +87764 Legau +87437 Kempten (Allgäu) +87488 Betzigau +87634 Obergünzburg +87647 Unterthingau +87648 Aitrang +87493 Lauben +87490 Haldenwang +87496 Untrasried +87499 Wildpoldsried +87471 Durach +87657 Görisried +87629 Füssen +87659 Hopferau +87669 Rieden am Forggensee +82409 Wildsteig +87616 Marktoberdorf +87675 Stötten am Auerberg +87663 Lengenwang +87494 Rückholz +87672 Roßhaupten +87651 Bidingen +87640 Biessenhofen +87674 Ruderatshofen +86984 Prem +86987 Schwabsoien +86975 Bernbeuren +86980 Ingenried +86956 Schongau +86983 Lechbruck +86972 Altenstadt +86986 Schwabbruck +86989 Steingaden +86977 Burggen +87763 Lautrach +87749 Hawangen +87781 Ungerhausen +87734 Benningen +87760 Lachen +87700 Memmingen +87784 Westerheim +87752 Holzgünz +87766 Memmingerberg +87758 Kronburg +87730 Bad Grönenbach +87789 Woringen +87787 Wolfertschwenden +87736 Böhen +87724 Ottobeuren +87751 Heimertingen +87761 Lauben +87740 Buxheim +87779 Trunkelsberg +87742 Apfeltrach +87778 Stetten +87776 Sontheim +87719 Mindelheim +87733 Markt Rettenbach +87671 Ronsberg +87782 Unteregg +87653 Eggenthal +87654 Friesenried +87746 Erkheim +86488 Breitenthal +86498 Kettershausen +89299 Unterroth +89293 Kellmünz a.d. Iller +89287 Bellenberg +89297 Roggenburg +89296 Osterberg +89290 Buch +89281 Altenstadt +87767 Niederrieden +87737 Boos +87773 Pleß +87770 Oberschönegg +87755 Kirchhaslach +87727 Babenhausen +87743 Egg an der Günz +87748 Fellheim +87785 Winterrieden +89294 Oberroth +86381 Krumbach (Schwaben) +86489 Deisenhausen +86491 Ebershausen +86480 Aletshausen +86470 Thannhausen +86483 Balzhausen +86513 Ursberg +87745 Eppishausen +87757 Kirchheim in Schwaben +87775 Salgen +87739 Breitenbrunn +87772 Pfaffenhausen +87769 Oberrieden +86860 Jengen +86879 Wiedergeltingen +86842 Türkheim +86825 Bad Wörishofen +86869 Oberostendorf +87600 Kaufbeuren +87650 Baisweil +87660 Irsee +87666 Pforzen +87668 Rieden +87656 Germaringen +87677 Stöttwang +87665 Mauerstetten +87679 Westendorf +87662 Kaltental +86932 Pürgen +86978 Hohenfurch +86940 Schwifting +86859 Igling +86899 Landsberg a. Lech +86946 Vilgertshofen +86875 Waal +86920 Denklingen +86934 Reichling +86925 Fuchstal +86981 Kinsau +86944 Unterdießen +86863 Langenneufnach +86479 Aichen +86877 Walkertshofen +86868 Mittelneufnach +86856 Hiltenfingen +86850 Fischach +86872 Scherstetten +86865 Markt Wald +86845 Großaitingen +86833 Ettringen +86866 Mickhausen +86874 Tussenhausen +86854 Amberg +86830 Schwabmünchen +86871 Rammingen +86511 Schmiechen +86343 Königsbrunn +86862 Lamerdingen +86517 Wehringen +86507 Oberottmarshausen +86857 Hurlach +86836 Untermeitingen +86937 Scheuring +86931 Prittriching +86916 Kaufering +82490 Farchant +82442 Saulgrub +82438 Eschenlohe +82445 Schwaigen +82496 Oberau +82487 Oberammergau +82497 Unterammergau +82488 Ettal +82491 Grainau +85221 Dachau +85232 Bergkirchen +85764 Oberschleißheim +85452 Moosinning +82293 Mittelstetten +82362 Weilheim i. OB +82431 Kochel a. See +82541 Münsing +83661 Lenggries +83623 Dietramszell +83676 Jachenau +83708 Kreuth +83700 Rottach-Egern +82499 Wallgau +82444 Schlehdorf +82493 Krün +82494 Krün +82432 Walchensee +82441 Ohlstadt +82389 Böbing +82405 Wessobrunn +82398 Polling +82380 Peißenberg +82401 Rottenbuch +82436 Eglfing +82383 Hohenpeißenberg +82386 Huglfing +82433 Bad Kohlgrub +82435 Bayersoien +82449 Uffing a. Staffelsee +82387 Antdorf +82390 Eberfing +82392 Habach +82402 Seeshaupt +82393 Iffeldorf +82404 Sindelsdorf +82395 Obersöchering +82418 Murnau a. Staffelsee +82439 Großweil +82447 Spatzenhausen +82377 Penzberg +82549 Königsdorf +82547 Eurasburg +83671 Benediktbeuern +83673 Bichl +83670 Bad Heilbrunn +83646 Bad Tölz, Wackersberg +83677 Reichersbeuern +83674 Gaißach +83666 Waakirchen +83703 Gmund a. Tegernsee +83627 Warngau +83607 Holzkirchen +83684 Tegernsee +83707 Bad Wiessee +83679 Sachsenkam +86974 Apfeldorf +82407 Wielenbach +82399 Raisting +86928 Hofstetten +86949 Windach +86935 Rott +86919 Utting a. Ammersee +86911 Dießen a. Ammersee +86943 Thaining +86938 Schondorf a. Ammersee +86923 Finning +82343 Pöcking +82335 Berg +82211 Herrsching a. Ammersee +82234 Weßling +82229 Seefeld +82131 Gauting +82266 Inning a. Ammersee +82327 Tutzing +82346 Andechs +82319 Starnberg +82340 Feldafing +82396 Pähl +82347 Bernried +86415 Mering +86504 Merching +82297 Steindorf +82279 Eching a. Ammersee +82285 Hattenhofen +82288 Kottgeisering +82278 Althegnenberg +82272 Moorenweis +82276 Adelshofen +82287 Jesenwang +82269 Geltendorf +82299 Türkenfeld +86492 Egling a.d. Paar +86926 Greifenberg +86922 Eresing +86941 Eresing +82205 Gilching +82349 Pentenried +82152 Planegg/Krailling +82237 Wörthsee +82290 Landsberied +82140 Olching +82256 Fürstenfeldbruck +82284 Grafrath +82216 Maisach +82281 Egenhofen +82296 Schöngeising +82291 Mammendorf +82178 Puchheim +82110 Germering +82239 Alling +82275 Emmering +82294 Oberschweinbach +82223 Eichenau +82061 Neuried +82538 Geretsried +82057 Icking +82069 Schäftlarn +82067 Schäftlarn +82065 Baierbrunn +82544 Egling +82064 Straßlach-Dingharting +82515 Wolfratshausen +82031 Grünwald +82049 Pullach i. Isartal +85662 Hohenbrunn +85630 Grasbrunn +85635 Höhenkirchen-Siegertsbrunn +85653 Aying +85649 Brunnthal +85521 Ottobrunn/Riemerling +82024 Taufkirchen +82054 Sauerlach +82041 Oberhaching +83624 Otterfing +83626 Valley +85757 Karlsfeld +80639 München +80995 München +80997 München +80999 München +80638 München +80687 München +81243 München +81475 München +81241 München +81379 München +81245 München +81249 München +82194 Gröbenzell +82166 Gräfelfing +81479 München +81477 München +81476 München +80689 München +81375 München +81377 München +80686 München +80339 München +80634 München +81369 München +81373 München +81247 München +80993 München +80637 München +80992 München +81547 München +81545 München +81543 München +85551 Kirchheim b. München +85622 Feldkirchen +85737 Ismaning +85640 Putzbrunn +85579 Neubiberg +85774 Unterföhring +85609 Aschheim +85748 Garching b. München +80937 München +80805 München +80939 München +81827 München +81737 München +81925 München +81929 München +81829 München +81735 München +81549 München +81739 München +82008 Unterhaching +80335 München +80333 München +80469 München +80336 München +80337 München +80636 München +80538 München +80539 München +80331 München +81371 München +81541 München +81667 München +81539 München +81669 München +81679 München +81673 München +81677 München +81671 München +81675 München +81927 München +81825 München +85540 Haar +80935 München +80802 München +80799 München +80807 München +80796 München +80804 München +80801 München +80809 München +80797 München +80798 München +80803 München +80933 München +85665 Moosach +83727 Schliersee +83735 Bayrischzell +84405 Dorfen +84419 Schwindegg +83088 Kiefersfelden +83308 Trostberg +83324 Ruhpolding +83278 Traunstein +83730 Fischbachau +83737 Irschenberg +83714 Miesbach +83734 Hausham +83629 Weyarn +83075 Bad Feilnbach +83043 Bad Aibling +83115 Neubeuern +83059 Kolbermoor +83026 Rosenheim +83131 Nußdorf a. Inn +83126 Flintsbach a. Inn +83080 Oberaudorf +83122 Samerberg +83064 Raubling +83098 Brannenburg +83101 Rohrdorf +83229 Aschau i. Chiemgau +83233 Bernau a. Chiemsee +83209 Prien a. Chiemsee, Herrenchiemssee +83083 Riedering +83112 Frasdorf +83259 Schleching +83224 Grassau +83246 Unterwössen +83377 Vachendorf +83236 Übersee +83250 Marquartstein +83346 Bergen +83355 Grabenstätt +83242 Reit im Winkl +85625 Glonn +85667 Oberpframmern +85567 Grafing b. München +85658 Egmating +85617 Aßling +83052 Bruckmühl +83104 Tuntenhausen +83620 Feldkirchen-Westerham +83135 Schechen +83024 Rosenheim +83539 Pfaffing +83556 Griesstätt +83543 Rott a. Inn +83533 Edling +83553 Frauenneuharting +83569 Vogtareuth +83109 Großkarolinenfeld +83134 Prutting +83022 Rosenheim +83071 Stephanskirchen +83550 Emmering +83561 Ramerberg +85464 Finsing +85469 Walpertskirchen +85467 Neuching +85669 Pastetten +85457 Wörth +85659 Forstern +85570 Markt Schwaben +85604 Zorneding +85614 Kirchseeon +85586 Poing +85646 Anzing +85661 Forstinning +85598 Baldham +85591 Vaterstetten +85652 Pliening +85560 Ebersberg +85664 Hohenlinden +85599 Parsdorf/Hergolding +85656 Buch a. Buchrain +85643 Steinhöring +84435 Lengdorf +84424 Isen +84427 Sankt Wolfgang +83562 Rechtmehring +83558 Maitenbeth +83544 Albaching +83527 Haag i. OB +83129 Höslwang +83125 Eggstätt +83137 Schonstett +83139 Söchtenau +83093 Bad Endorf +83512 Wasserburg a. Inn +83549 Eiselfing +83253 Rimsting +83128 Halfing +83123 Amerang +83254 Breitbrunn a. Chiemsee +83530 Schnaitsee +83119 Obing +83132 Pittenhart +83257 Gstadt a. Chiemsee +83361 Kienberg +83256 Chiemsee +83365 Nußdorf +83301 Traunreut +83358 Seeon-Seebruck +83370 Seeon-Seebruck +83352 Altenmarkt a.d. Alz +83374 Traunreut +83371 Stein a.d. Traun +83376 Seeon-Seebruck +83339 Chieming +83368 St. Georgen +84544 Aschau a. Inn +84431 Heldenstein +84555 Jettenbach +84437 Reichertsheim +84539 Ampfing +83546 Gars am Inn +83567 Unterreit +83547 Babensham +83564 Soyen +83559 Gars a. Inn +83536 Gars a. Inn +83555 Gars a. Inn +84565 Oberneukirchen +84549 Engelsberg +84574 Taufkirchen +84453 Mühldorf a. Inn +84562 Mettenheim +84570 Polling +84513 Töging a. Inn +84478 Waldkraiburg +84559 Kraiburg a. Inn +83342 Tacherting +84579 Unterneukirchen +84550 Feichten a.d. Alz +84518 Garching a.d. Alz +84577 Tüßling +83486 Ramsau b. Berchtesgaden +84375 Kirchdorf a. Inn +83454 Anger +83317 Teisendorf +83313 Siegsdorf +84524 Neuötting +83471 Berchtesgaden & Schönau +83483 Bischofswiesen +83364 Neukirchen am Teisenberg +83458 Schneizlreuth +83334 Inzell +83404 Ainring +83435 Bad Reichenhall +83487 Marktschellenberg +83457 Bayerisch Gmain +83451 Piding +83395 Freilassing +83413 Fridolfing +83349 Palling +83373 Taching a. See +83329 Waging a. See +83362 Surberg +83417 Kirchanschöring +83367 Petting +83379 Wonneberg +84529 Tittmoning +83410 Laufen +83416 Saaldorf +84543 Winhöring +84508 Burgkirchen an der Alz +84553 Halsbach +84503 Altötting +84558 Kirchweidach +84556 Kastl +84489 Burghausen +84547 Emmerting +84561 Mehring +84576 Teising +84387 Julbach +84533 Marktl +89352 Ellzee +89367 Waldstetten +89347 Bubesheim +89340 Leipheim +89359 Kötz +89312 Günzburg +89346 Bibertal +89335 Ichenhausen +89278 Nersingen +89284 Pfaffenhofen a.d. Roth +89264 Weißenhorn +86444 Affing +86450 Altenmünster +89438 Holzheim +89423 Gundelfingen a.d. Donau +89407 Dillingen a.d. Donau +89364 Rettenbach +86660 Tapfheim +86609 Donauwörth +86641 Rain +86688 Marxheim +91710 Gunzenhausen +91781 Weißenburg i. Bay. +91785 Pleinfeld +91743 Unterschwaningen +91550 Dinkelsbühl +89291 Holzheim +86519 Wiesenbach +86476 Neuburg a.d. Kammel +86505 Münsterhausen +89356 Haldenwang +89343 Jettingen-Scheppach +89350 Dürrlauingen +89358 Kammeltal +89368 Winterbach +89365 Röfingen +89349 Burtenbach +89361 Landensberg +89331 Burgau +89447 Zöschingen +89428 Syrgenstein +89415 Lauingen (Donau) +89429 Bachhagel +89435 Finningen +89446 Ziertheim +89426 Wittislingen +89437 Haunsheim +89441 Medlingen +89344 Aislingen +89355 Gundremmingen +89362 Offingen +89431 Bächingen a.d. Brenz +89353 Glött +86477 Adelsried +86465 Welden +86441 Zusmarshausen +86494 Emersacker +86424 Dinkelscherben +86486 Bonstetten +86514 Ustersbach +86497 Horgau +86459 Gessertshausen +86500 Kutzenhausen +86508 Rehling +86179 Augsburg +86199 Augsburg +86161 Augsburg +86156 Augsburg +86169 Augsburg +86167 Augsburg +86482 Aystetten +86356 Neusäß +86368 Gersthofen +86391 Stadtbergen +86456 Gablingen +86462 Langweid a. Lech +86420 Diedorf +86157 Augsburg +86159 Augsburg +86152 Augsburg +86150 Augsburg +86153 Augsburg +86154 Augsburg +86502 Laugna +89443 Schwenningen +89440 Lutzingen +89420 Höchstädt a.d. Donau +89434 Blindheim +86637 Wertingen +86647 Buttenwiesen +86447 Aindling +86485 Biberbach +86405 Meitingen +86707 Westendorf, Kühlenthal +86663 Asbach-Bäumenheim +86690 Mertingen +86692 Münster +86678 Ehingen +86672 Thierhaupten +86695 Nordendorf +86679 Ellgau +86698 Oberndorf a.Lech +86739 Ederheim +86757 Wallerstein +86745 Hohenaltheim +86720 Nördlingen +86735 Amerdingen +86742 Fremdingen +86747 Maihingen +86748 Marktoffingen +91614 Mönchsroth +91725 Ehingen +91726 Gerolfingen +91749 Wittelshofen +91634 Wilburgstetten +91744 Weiltingen +91602 Dürrwangen +91731 Langfurth +86753 Möttingen +86733 Alerheim +86751 Mönchsdeggingen +86759 Wechingen +86738 Deiningen +86650 Wemding +86685 Huisheim +86657 Bissingen +86655 Harburg +86756 Reimlingen +86709 Wolferstadt +86700 Otting +86681 Fünfstetten +86653 Monheim +86687 Kaisheim +86694 Niederschönenfeld +86682 Genderkingen +91799 Langenaltheim +86675 Buchdorf +86736 Auhausen +86754 Munningen +86744 Hainsfarth +86732 Oettingen i. Bay. +86750 Megesheim +91747 Westheim +91805 Polsingen +91719 Heidenheim +91728 Gnotzheim +91740 Röckingen +91717 Wassertrüdingen +86741 Ehingen a. Ries +91757 Treuchtlingen +91801 Markt Berolzheim +91723 Dittenheim +91792 Ellingen +91793 Alesheim +91802 Meinheim +91741 Theilenhofen +86551 Aichach +86568 Hollenbach +86577 Sielenbach +86510 Ried +86559 Adelzhausen +86438 Kissing +86495 Eurasburg +86316 Friedberg +86453 Dasing +85406 Zolling +85298 Scheyern +85229 Markt Indersdorf +85235 Odelzhausen +85110 Kipfenberg +85111 Adelschlag +84089 Aiglsbach +93309 Kelheim +93336 Altmannstein +92334 Berching +92363 Breitenbrunn +92345 Dietfurt a.d. Altmühl +86697 Oberhausen +86633 Neuburg an der Donau +91795 Dollnstein +91177 Thalmässing +86163 Augsburg +86165 Augsburg +86573 Obergriesbach +86576 Schiltberg +85302 Gerolsbach +85253 Erdweg +85247 Schwabhausen +85250 Altomünster +85259 Sulzemoos +85254 Sulzemoos +86567 Hilgertshausen-Tandern +86554 Pöttmes +86570 Inchenhofen +86574 Petersdorf +86674 Baar +86676 Ehekirchen +86666 Burgheim +86684 Holzheim +86556 Kühbach +86565 Gachenbach +86529 Schrobenhausen +86561 Aresing +86571 Langenmosen +86579 Waidhofen +86564 Brunnen +86562 Berg im Gau +86701 Rohrenfels +86669 Königsmoos +86668 Karlshuld +85777 Fahrenzhausen +85307 Paunzhausen +85411 Hohenkammer +85305 Jetzendorf +85293 Reichertshausen +85256 Vierkirchen +85258 Weichs +85238 Petershausen +85244 Röhrmoos +85778 Haimhausen +85241 Hebertshausen +85716 Unterschleißheim +85414 Kirchdorf a.d. Amper +85386 Eching +85376 Neufahrn b. Freising +85375 Neufahrn b. Freising +85391 Allershausen +85402 Kranzberg +85399 Hallbergmoos +85356 Freising +85354 Freising +85276 Pfaffenhofen a.d. Ilm +85309 Pörnbach +85296 Rohrbach +85107 Baar-Ebenhausen +85084 Reichertshofen +85304 Ilmmünster +85123 Karlskron +86558 Hohenwart +85395 Attenkirchen +85290 Geisenfeld +85301 Schweitenkirchen +85283 Wolnzach +84104 Rudelzhausen +84072 Au in der Hallertau +84048 Mainburg +86703 Rögling +86704 Tagmersheim +86643 Rennertshofen +91809 Wellheim +91804 Mörnsheim +91807 Solnhofen +85051 Ingolstadt +85049 Ingolstadt +85114 Buxheim +85128 Nassenfels +85117 Eitensheim +85122 Hitzhofen +85137 Walting +85072 Eichstätt +85116 Egweil +86706 Weichering +86673 Bergheim +85132 Schernfeld +91790 Nennslingen +91788 Pappenheim +91798 Höttingen +91180 Heideck +91796 Ettenstatt +85131 Pollenfeld +85125 Kinding +85135 Titting +91171 Greding +85077 Manching +85101 Lenting +85092 Kösching +85120 Hepberg +85134 Stammham +85098 Großmehring +85080 Gaimersheim +85139 Wettstetten +85055 Ingolstadt +85057 Ingolstadt +85053 Ingolstadt +85113 Böhmfeld +85088 Vohburg a.d. Donau +85126 Münchsmünster +85119 Ernsgaden +85129 Oberdolling +85104 Pförring +93349 Mindelstetten +93333 Neustadt a.d. Donau +85095 Denkendorf +92339 Beilngries +93339 Riedenburg +93155 Hemau +91625 Schnelldorf +91626 Schopfloch +91583 Schillingsfürst +91631 Wettringen +91601 Dombühl +91610 Insingen +91637 Wörnitz +96152 Burghaslach +96181 Rauhenebrach +91126 Schwabach +91564 Neuendettelsau +91555 Feuchtwangen +91522 Ansbach +91592 Buch a. Wald +91578 Leutershausen +91472 Ipsheim +91413 Neustadt a.d.Aisch +97355 Kleinlangheim +97320 Albertshofen +97215 Uffenheim +97346 Iphofen +97353 Wiesentheid +97348 Rödelsee +97483 Eltmann +91599 Dentlein a. Forst +91589 Aurach +91632 Wieseth +91596 Burk +91567 Herrieden +91572 Bechhofen +91620 Ohrenbach +91587 Adelshofen +91541 Rothenburg ob der Tauber +91607 Gebsattel +91608 Geslau +91635 Windelsbach +91628 Steinsfeld +91616 Neusitz +91605 Gallmersgarten +91593 Burgbernheim +91465 Ergersheim +91604 Flachslanden +91598 Colmberg +91611 Lehrberg +91617 Oberdachstetten +91471 Illesheim +91438 Bad Windsheim +91619 Obernzenn +91613 Marktbergel +91735 Muhr a. See +91722 Arberg +91737 Ornbau +91746 Weidenbach +91580 Petersaurach +91732 Merkendorf +91595 Burgoberbach +91586 Lichtenau +91623 Sachsen b. Ansbach +91639 Wolframs-Eschenbach +91183 Abenberg +91189 Rohr +91720 Absberg +91738 Pfofeld +91729 Haundorf +91174 Spalt +91575 Windsbach +91734 Mitteleschenbach +91629 Weihenzell +91622 Rügland +91590 Bruckberg +91452 Wilhermsdorf +91459 Markt Erlbach +91448 Emskirchen +90616 Neuhof a.d.Zenn +90619 Trautskirchen +90599 Dietenhofen +90768 Fürth +90587 Veitsbronn +90556 Cadolzburg +91560 Heilsbronn +90579 Langenzenn +90613 Großhabersdorf +90574 Roßtal +90513 Zirndorf +90614 Ammerndorf +91478 Markt Nordheim +97342 Obernbreit +97340 Marktbreit +97258 Ippesheim +97350 Mainbernheim +91443 Scheinfeld +91483 Oberscheinfeld +91474 Langenfeld +91463 Dietersheim +91477 Markt Bibart +91484 Sugenheim +97523 Schwanfeld +97509 Kolitzheim +97537 Wipfeld +97332 Volkach +97359 Schwarzach a. Main +97334 Sommerach +97279 Prosselsheim +97247 Eisenheim +96157 Ebrach +96160 Geiselwind +97516 Oberschwarzach +97511 Lülsfeld +97529 Sulzheim +97447 Gerolzhofen +97513 Michelau i. Steigerwald, Hundelshausen +97497 Dingolshausen +97357 Prichsenstadt +96193 Wachenroth +91486 Uehlfeld +91460 Baudenbach +91462 Dachsbach +91481 Münchsteinach +91466 Gerhardshofen +91480 Markt Taschendorf +91456 Diespeck +91468 Gutenstetten +91487 Vestenbergsgreuth +91489 Wilhelmsdorf +91469 Hagenbüchach +91475 Lonnerstadt +91315 Höchstadt a.d.Aisch +91325 Adelsdorf +91350 Gremsdorf +91341 Röttenbach +91093 Heßdorf +91074 Herzogenaurach +91091 Großenseebach +91086 Aurachtal +91085 Weisendorf +91097 Oberreichenbach +90617 Puschendorf +96138 Burgebrach +96154 Burgwindheim +96185 Schönbrunn i. Steigerwald +96132 Schlüsselfeld +96170 Lisberg +97514 Oberaurach +96050 Bamberg +96135 Stegaurach +96178 Pommersfelden +96194 Walsdorf +96172 Mühlhausen +96052 Bamberg +96163 Gundelsheim +96047 Bamberg +96158 Frensdorf +96173 Oberhaid +96191 Viereth-Trunstadt +96103 Hallstadt +96049 Bamberg +96175 Pettstadt +96120 Bischberg +90530 Wendelstein +91186 Büchenbach +90596 Schwanstetten +91187 Röttenbach +91166 Georgensgmünd +91154 Roth +92367 Pilsach +92318 Neumarkt i.d. OPf. +92277 Hohenburg +92249 Vilseck +92275 Hirschbach +92256 Hahnbach +92655 Grafenwöhr +95466 Weidenberg, Kirchenpingarten +95473 Creußen +95469 Speichersdorf +96142 Hollfeld +91320 Ebermannstadt +91257 Pegnitz +91278 Pottenstein +91362 Pretzfeld +90602 Pyrbaum +90518 Altdorf bei Nürnberg +92353 Postbauer-Heng +92342 Freystadt +91161 Hilpoltstein +90584 Allersberg +90482 Nürnberg +90480 Nürnberg +90491 Nürnberg +90411 Nürnberg +90489 Nürnberg +90419 Nürnberg +90431 Nürnberg +90427 Nürnberg +90471 Nürnberg +90455 Nürnberg +90451 Nürnberg +90453 Nürnberg +90763 Fürth +90562 Heroldsberg +90765 Fürth +90547 Stein +90522 Oberasbach +90469 Nürnberg +90449 Nürnberg +90766 Fürth +90762 Fürth +90429 Nürnberg +90439 Nürnberg +90441 Nürnberg +90443 Nürnberg +90402 Nürnberg +90403 Nürnberg +90478 Nürnberg +90459 Nürnberg +90461 Nürnberg +90425 Nürnberg +90409 Nürnberg +90408 Nürnberg +90475 Nürnberg +91227 Leinburg +91207 Lauf an der Pegnitz +90607 Rückersdorf +91242 Ottensoos +90552 Röthenbach an der Pegnitz +90571 Schwaig b. Nürnberg, Behringersdorfer Forst +90610 Winkelhaid +90537 Nürnberg-Feucht, Feuchter Forst +90559 Burgthann +90592 Schwarzenbruck +90473 Nürnberg +92364 Deining +92361 Berngau +92360 Mühlhausen +92369 Sengenthal +92358 Seubersdorf i.d. OPf. +92355 Velburg +92331 Parsberg +91238 Engelthal/Offenhausen +92348 Berg b.Neumarkt i.d.OPf. +92283 Lauterhofen +91230 Happurg +91224 Pommelsbrunn +91236 Alfeld +91244 Reichenschwand +91249 Weigendorf +91217 Hersbruck +91239 Henfenfeld +92262 Birgland +92237 Sulzbach-Rosenberg +92289 Ursensollen +92280 Kastl +92278 Illschwang +92260 Ammerthal +92259 Neukirchen b. Sulzbach-Rosen +91056 Erlangen +91058 Erlangen +91054 Erlangen +91080 Uttenreuth, Marloffstein +91096 Möhrendorf/Mark +91090 Effeltrich +91361 Pinzberg +91094 Langensendelbach +91353 Hausen +91352 Hallerndorf +91301 Forchheim +91077 Neunkirchen a. Brand +91099 Poxdorf +91336 Heroldsbach +91083 Baiersdorf +91088 Bubenreuth +91052 Erlangen +91334 Hemhofen +91358 Kunreuth +91322 Gräfenberg +91286 Obertrubach +91349 Egloffstein +91356 Kirchehrenbach +91327 Gößweinstein +91355 Hiltpoltstein +91338 Igensdorf +91359 Leutenbach +91233 Neunkirchen am Sand +90542 Eckental +91220 Schnaittach +91245 Simmelsdorf +91369 Wiesenthau +91367 Weißenohe +96110 Scheßlitz +96146 Altendorf +96123 Litzendorf +96114 Hirschaid +96167 Königsfeld +96129 Strullendorf +96117 Memmelsdorf +96155 Buttenheim +91365 Weilersbach +91330 Eggolsheim +91332 Heiligenstadt i. OFr. +95515 Plankenfels +91347 Aufseß +91346 Wiesenttal +91364 Unterleinleiter +91344 Waischenfeld +91235 Velden/Hartenstein +91287 Plech +91282 Betzenstein +91284 Neuhaus a.d.Pegnitz +91241 Kirchensittenbach +91247 Vorra +92265 Edelsfeld +92281 Königstein +92268 Etzelwang +92676 Eschenbach i.d. OPf. +91275 Auerbach i.d. OPf. +91281 Kirchenthumbach +95503 Hummeltal +95447 Bayreuth +95491 Ahorntal +95490 Mistelgau +95488 Eckersdorf +95496 Glashütten +95494 Gesees +95511 Mistelbach +95517 Emtmannsberg +95448 Bayreuth +95519 Vorbach +91289 Schnabelwaid +85368 Moosburg a.d. Isar +85416 Langenbach +85410 Haag a.d. Amper +85417 Marzling +85456 Wartenberg +85459 Berglern +85435 Erding +85462 Eitting +85461 Bockhorn +85447 Fraunberg +85465 Langenpreising +85445 Oberding +84061 Ergoldsbach +84056 Rottenburg a.d. Laaber +84095 Furth +84066 Mallersdorf-Pfaffenberg +84036 Landshut +84175 Gerzen +84144 Geisenhausen +84051 Essenbach +84183 Niederviehbach +84164 Moosthenning +84152 Mengkofen +84568 Pleiskirchen +94333 Geiselhöring +94315 Straubing +94419 Reisbach +94431 Pilsting +93077 Bad Abbach +93107 Thalmassing +93197 Zeitlarn +93086 Wörth an der Donau +93128 Regenstauf +93185 Michelsneukirchen +84186 Vilsheim +84169 Altfraunhofen +84174 Eching +84171 Baierbach +84172 Buch a. Erlbach +84184 Tiefenbach +84434 Kirchberg +84416 Taufkirchen (Vils) +84439 Steinkirchen +84432 Hohenpolding +85419 Mauern +85408 Gammelsdorf +85413 Hörgertshausen +85405 Nandlstadt +84091 Attenhofen +84106 Volkenschwand +84101 Obersüßbach +84076 Pfeffenhausen +84034 Landshut +84079 Bruckberg +84098 Hohenthann +84107 Weihmichl +84032 Landshut, Altdorf +84030 Ergolding, Landshut +84028 Landshut +84155 Bodenkirchen +84137 Vilsbiburg +84189 Wurmsham +84149 Velden +84428 Buchbach +84564 Oberbergkirchen +84181 Neufraunhofen +84494 Neumarkt-Sankt Veit +84546 Egglkofen +84573 Schönberg +84140 Gangkofen +84323 Massing +84103 Postau +84187 Weng +84166 Adlkofen +84100 Niederaichbach +84178 Kröning +84109 Wörth a.d. Isar +84168 Aham +84130 Dingolfing +84177 Gottfrieding +84160 Frontenhausen +84180 Loiching +84163 Marklkofen +94437 Mamming +84094 Elsendorf +93345 Hausen +93326 Abensberg +93358 Train +93352 Rohr i. NB +93342 Saal a.d. Donau +93354 Siegenburg +93359 Wildenberg +93348 Kirchdorf +84085 Langquaid +84097 Herrngiersdorf +84069 Schierling +84088 Neufahrn i. NB +93356 Teugn +93351 Painten +93346 Ihrlerstein +93343 Essing +93195 Wolfsegg +93180 Deuerling +93188 Pielenhofen +93164 Laaber, Brunn +93176 Beratzhausen +93152 Nittendorf +93161 Sinzing +93105 Tegernheim +93080 Pentling +93053 Regensburg +93059 Regensburg +93055 Regensburg +93173 Wenzenbach +93138 Lappersdorf +93096 Köfering +93051 Regensburg +93083 Obertraubling +93186 Pettendorf +93170 Bernhardswald +93057 Regensburg +93049 Regensburg +93047 Regensburg +84092 Bayerbach bei Ergoldsbach +84082 Laberweinting +93089 Aufhausen +93099 Mötzing +93104 Sünching +93101 Pfakofen +93095 Hagelstadt +94368 Perkam +94351 Feldkirchen +94330 Aiterhofen +94348 Atting +94369 Rain +94339 Leiblfing +93087 Alteglofsheim +93092 Barbing +93102 Pfatter +93177 Altenthann +93179 Brennberg +93093 Donaustauf +93098 Mintraching +93090 Bach an der Donau +93073 Neutraubling +93109 Wiesent +93192 Wald +94345 Aholfing +94350 Falkenfels +94344 Wiesenfelden +94365 Parkstetten +94356 Kirchroth +94377 Steinach +93191 Rettenbach +93167 Falkenstein +84332 Hebertsfelden +84335 Mitterskirchen +84329 Wurmannsquick +84552 Geratskirchen +84339 Unterdietfurt +84307 Eggenfelden +84326 Falkenberg +84571 Reischach +84567 Erlbach +84347 Pfarrkirchen +84337 Schönau +94327 Bogen +94428 Eichendorf +94081 Fürstenzell +94496 Ortenburg +94486 Osterhofen +94255 Böbrach +94259 Kirchberg +94227 Zwiesel +94253 Bischofsmais +94256 Drachselsried +84384 Wittibreut +84364 Bad Birnbach +84371 Triftern +84359 Simbach a. Inn +84389 Postmünster +84367 Tann +84333 Malgersdorf +94405 Landau a.d. Isar +94436 Simbach +94424 Arnstorf +84381 Johanniskirchen +84385 Egglham +84378 Dietersburg +94501 Aldersbach +94574 Wallerfing +94550 Künzing +94439 Roßbach +94149 Kößlarn +94148 Kirchham +94086 Griesbach i. Rottal +94094 Rotthalmünster +94140 Ering +94166 Stubenberg +94137 Bayerbach +94099 Ruhstorf a.d. Rott +94060 Pocking +94167 Tettenweis +94072 Bad Füssing +94152 Neuhaus a. Inn +94544 Hofkirchen +94474 Vilshofen an der Donau +94575 Windorf +94542 Haarbach +94161 Ruderting +94529 Aicha vorm Wald +94032 Passau +94127 Neuburg a. Inn +94154 Neukirchen vorm Wald +94034 Passau +94036 Passau +94113 Tiefenbach +94559 Niederwinkling +94342 Straßkirchen +94363 Oberschneiding +94553 Mariaposching +94522 Wallersdorf +94569 Stephansposching +94563 Otzing +94562 Oberpöring +94533 Buchhofen +94505 Bernried +94560 Offenberg +94539 Grafling +94554 Moos +94557 Niederalteich +94447 Plattling +94527 Aholming +94469 Deggendorf +94526 Metten +94353 Haibach +94374 Schwarzach +94366 Perasdorf +94359 Loitzendorf +94357 Konzell +94379 Sankt Englmar +94371 Rattenberg +94354 Haselbach +94375 Stallwang +94347 Ascha +94336 Hunderdorf +94360 Mitterfels +94372 Rattiszell +94362 Neukirchen +94267 Prackenbach +94250 Achslach +94262 Kollnburg +94239 Zachenberg +94265 Patersdorf +94234 Viechtach +94244 Teisnach +94535 Eging a. See +94577 Winzer +94508 Schöllnach +94532 Außernzell +94491 Hengersberg +94571 Schaufling +94551 Lalling +94541 Grattersdorf +94547 Iggensbach +94579 Zenting +94572 Schöfweg +94530 Auerbach +94104 Tittling +94538 Fürstenstein +94481 Grafenau +94169 Thurmansbang +94513 Schönberg +94536 Eppenschlag +94518 Spiegelau +94568 Sankt Oswald +94163 Saldenburg +94548 Innernzell +94157 Perlesreut +94261 Kirchdorf i. Wald +94264 Langdorf +94209 Regen +94249 Bodenmais +94269 Rinchnach +94252 Bayerisch Eisenstein +94566 Riedlhütte +94258 Frauenau +93133 Burglengenfeld +93183 Kallmünz +93182 Duggendorf +92366 Hohenfels +92287 Schmidmühlen +93149 Nittenau +93426 Roding +92253 Schnaittenbach +92263 Ebermannsdorf +92242 Hirschau +92286 Rieden +92421 Schwandorf +92539 Schönsee +92439 Bodenwöhr +92431 Neunburg vorm Wald +92445 Neukirchen-Balbini +95643 Tirschenreuth +95679 Waldershof +92714 Pleystein +92637 Weiden in der OPf., Theisseil +93142 Maxhütte-Haidhof +93158 Teublitz +92442 Wackersdorf +92449 Steinberg +92272 Freudenberg +92245 Kümmersbruck +92266 Ensdorf +92284 Poppenricht +92224 Amberg +92546 Schmidgaden +92533 Wernberg-Köblitz +92507 Nabburg +92548 Schwarzach b. Nabburg +92521 Schwarzenfeld +92551 Stulln +92269 Fensterbach +92536 Pfreimd +93189 Reichenbach +93194 Walderbach +93199 Zell +92436 Bruck i.d. OPf. +93483 Pösing +93491 Stamsried +93413 Cham +93489 Schorndorf +93455 Traitsching +93482 Pemfling +92444 Rötz +92555 Trausnitz +92552 Teunz +92543 Guteneck +92447 Schwarzhofen +92545 Niedermurach +92540 Altendorf +92542 Dieterskirchen +92723 Tännesberg +93488 Schönthal +93464 Tiefenbach +92526 Oberviechtach +92557 Weiding +92554 Thanstein +92559 Winklarn +92271 Freihung +92274 Gebenbach +92700 Kaltenbrunn +92690 Pressath +92665 Altenstadt a.d. Waldnaab +92708 Mantel +92694 Etzenricht +92720 Schwarzenbach +92702 Kohlberg +92711 Parkstein +92729 Weiherhammer +92712 Pirk +92660 Neustadt a.d. Waldnaab +92706 Luhe-Wildenau +92718 Schirmitz +95506 Kastl +95700 Neusorg +95505 Immenreuth +95508 Kulmain +95683 Ebnath +95478 Kemnath +92724 Trabitz +95514 Neustadt a. Kulm +95676 Wiesau +95689 Fuchsmühl +95704 Pullenreuth +95688 Friedenfels +92670 Windischeschenbach +92681 Erbendorf +92703 Krummennaab +92715 Püchersreuth +92717 Reuth b. Erbendorf +92699 Bechtsried +92709 Moosbach +92727 Waldthurn +92648 Vohenstrauß +92696 Flossenbürg +92697 Georgenberg +92685 Floß +92705 Leuchtenberg +92726 Waidhaus +92693 Eslarn +92721 Störnstein +95703 Plößberg +95685 Falkenberg +95671 Bärnau +95666 Mitterteich +95695 Mähring +93468 Miltach +93437 Furth i. Wald +93476 Blaibach +93497 Willmering +93466 Chamerau +93477 Gleißenberg +93499 Zandt +93473 Arnschwang +93494 Waffenbrunn +93495 Weiding +93486 Runding +93449 Waldmünchen +93479 Grafenwiesen +93453 Neukirchen b. Hl. Blut +93480 Hohenwarth +93444 Kötzting +93485 Rimbach +93474 Arrach +93471 Arnbruck +93458 Eschlkam +93492 Treffelstein +92549 Stadlern +93462 Lam +93470 Lohberg +63796 Kahl am Main +63791 Karlstein am Main +63811 Stockstadt am Main +63826 Geiselbach +63773 Goldbach +63825 Schöllkrippen, Blankenbach +63741 Aschaffenburg +63755 Alzenau +63814 Mainaschaff +63867 Johannesberg +63776 Mömbris +63829 Krombach +63768 Hösbach +63801 Kleinostheim +63739 Aschaffenburg +63808 Haibach +63864 Glattbach +63869 Heigenbrücken +63846 Laufach +97843 Neuhütten +97833 Frammersbach +97859 Wiesthal +63871 Heinrichsthal +63877 Sailauf +63831 Wiesen, Wiesener Forst +63828 Kleinkahl +97762 Hammelburg +97772 Wildflecken +97778 Fellen +97657 Sandberg +97618 Hohenroth +97647 Nordheim v.d. Rhön +97848 Rechtenbach +97775 Burgsinn +97816 Lohr a. Main +97846 Partenstein +97788 Neuendorf +97794 Rieneck +97737 Gemünden a. Main +97799 Zeitlofs +97791 Obersinn +97785 Mittelsinn +97773 Aura i. Sinngrund +97797 Wartmannsroth +97776 Eußenheim +97783 Karsbach +97780 Gössenheim +97782 Gräfendorf +97727 Fuchsstadt +97729 Ramsthal +97717 Euerdorf +97725 Elfershausen +97535 Wasserlosen +97502 Euerbach +97450 Arnstein +97789 Oberleichtersbach +97795 Schondra +97769 Bad Brückenau +97723 Oberthulba +97786 Motten +97792 Riedenberg +97705 Burkardroth +97779 Geroda +97688 Bad Kissingen +97708 Bad Bocklet +97659 Schönau a.d. Brend +97650 Fladungen +97656 Oberelsbach +97653 Bischofsheim a.d. Rhön +97714 Oerlenbach +97526 Sennfeld +97464 Niederwerrn +97506 Grafenrheinfeld +97534 Waigolshausen +97490 Poppenhausen +97422 Schweinfurt +97469 Gochsheim +97508 Grettstadt +97493 Bergrheinfeld +97525 Schwebheim +97456 Dittelbrunn +97424 Schweinfurt +97505 Geldersheim +97532 Üchtelhausen +97520 Röthlein +96106 Ebern +96176 Pfarrweisach +96250 Ebensfeld +97453 Schonungen +97486 Königsberg i. Bay. +97494 Bundorf +97461 Hofheim i. UFr. +97421 Schweinfurt +97499 Donnersdorf +97488 Stadtlauringen +97478 Knetzgau +97539 Wonfurt +97503 Gädheim +97437 Haßfurt +97519 Riedbach +97491 Aidhausen +97531 Theres +97720 Nüdlingen +97702 Münnerstadt +97711 Maßbach +97724 Burglauer +97616 Bad Neustadt an der Saale +97517 Rannungen +97631 Bad Königshofen i. Grabfeld +97633 Sulzfeld +96166 Kirchlauter +96151 Breitbrunn +97496 Burgpreppach +97500 Ebelsbach +97475 Zeil a. Main +97522 Sand a. Main +96188 Stettfeld +96274 Itzgrund +96184 Rentweinsdorf +96190 Untermerzbach +96182 Reckendorf +96148 Baunach +96179 Rattelsdorf +96169 Lauter +96149 Breitengüßbach +96164 Kemmern +96161 Gerach +96126 Maroldsweisach +97528 Sulzdorf a.d. Lederhecke +96482 Ahorn +96486 Lautertal +96450 Coburg +96479 Weitramsdorf +96145 Seßlach +96484 Meeder +96476 Rodach b. Coburg +96269 Großheirath +97640 Oberstreu +97645 Ostheim v.d. Rhön +97638 Mellrichstadt +97654 Bastheim +96215 Lichtenfels +96231 Staffelstein +96199 Zapfendorf +96196 Wattendorf +07926 Gefell +95236 Stammbach +95369 Untersteinach +95326 Kulmbach +95336 Mainleus +96337 Ludwigsstadt +96369 Weißenbrunn +96224 Burgkunstadt +96187 Stadelhofen +95359 Kasendorf +96197 Wonsees +96260 Weismain +96272 Hochstadt a. Main +96264 Altenkunstadt +96465 Neustadt b. Coburg +96242 Sonnefeld +96253 Untersiemau +96237 Ebersdorf b. Coburg +96271 Grub a. Forst +96472 Rödental +96279 Weidhausen b. Coburg +96247 Michelau i. OFr. +96489 Niederfüllbach +96487 Dörfles-Esbach +96332 Pressig +96317 Kronach +96268 Mitwitz +96328 Küps +96342 Stockheim +96277 Schneckenlohe +96275 Marktzeuln +96257 Redwitz a.d. Rodach +95367 Trebgast +95361 Ködnitz +95339 Neuenmarkt +95512 Neudrossenfeld +95349 Thurnau +95445 Bayreuth +95500 Heinersreuth +95463 Bindlach +95444 Bayreuth +95239 Zell +95352 Marktleugast +95502 Himmelkron +95358 Guttenberg +95509 Marktschorgast +95362 Kupferberg +95499 Harsdorf +95493 Bischofsgrün +95497 Goldkronach +95482 Gefrees +95460 Bad Berneck im Fichtelgebirge +95485 Warmensteinach +95364 Ludwigschorgast +95179 Geroldsgrün +95346 Stadtsteinach +95365 Rugendorf +95355 Presseck +96365 Nordhalben +96352 Wilhelmsthal +96346 Wallenfels +96364 Marktrodach +96349 Steinwiesen +96358 Teuschnitz +95131 Schwarzenbach a. Wald +95188 Issigau +95152 Selbitz +95197 Schauenstein +95180 Berg +95233 Helmbrechts +95213 Münchberg +95119 Naila +95138 Bad Steben +95356 Grafengehaig +96361 Steinbach a. Wald +96355 Tettau +96367 Tschirn +95192 Lichtenberg +95682 Brand +95168 Marktleuthen +95709 Tröstau +95163 Weißenstadt +95195 Röslau +95158 Kirchenlamitz +95632 Wunsiedel +95234 Sparneck +95694 Mehlmeisel +95686 Fichtelberg +95697 Nagel +95701 Pechbrunn +95691 Hohenberg a.d. Eger +95199 Thierstein +95707 Thiersheim +95186 Höchstädt +95680 Bad Alexandersbad +95100 Selb +95659 Arzberg +95615 Marktredwitz +95189 Köditz +95032 Hof +95237 Weißdorf +95145 Oberkotzau +95182 Döhlau +95191 Leupoldsgrün +95030 Hof +95126 Schwarzenbach a.d. Saale +95176 Konradsreuth +95185 Gattendorf +95183 Feilitzsch +95028 Hof +95173 Schönwald +95194 Regnitzlosau +95111 Rehau +95692 Konnersreuth +95698 Neualbenreuth +95652 Waldsassen +95706 Schirnding +94116 Hutthurm +94139 Breitenberg +94107 Untergriesbach +94136 Thyrnau +94051 Hauzenberg +94121 Salzweg +94124 Büchlberg +94130 Obernzell +94164 Sonnen +94110 Wegscheid +94078 Freyung +94556 Neuschönau +94142 Fürsteneck +94545 Hohenau +94133 Röhrnbach +94146 Hinterschmiding +94151 Mauth +94065 Waldkirchen +94143 Grainet +94160 Ringelai +94118 Jandelsbrunn +94145 Haidmühle +94089 Neureichenau +94158 Philippsreut +28357 Bremen +28197 Bremen +28816 Stuhr +28259 Bremen +28195 Bremen +28199 Bremen +28201 Bremen +28277 Bremen +28779 Bremen +28777 Bremen +28755 Bremen +28237 Bremen +28719 Bremen +28217 Bremen +28239 Bremen +28759 Bremen +28757 Bremen +28717 Bremen +28215 Bremen +28219 Bremen +28307 Bremen +28211 Bremen +28309 Bremen +28329 Bremen +28327 Bremen +28876 Oyten +28279 Bremen +28203 Bremen +28207 Bremen +28205 Bremen +28325 Bremen +28209 Bremen +28213 Bremen +28355 Bremen +28359 Bremen +27572 Bremerhaven +27570 Bremerhaven +27578 Bremerhaven +27568 Bremerhaven, Bremen +27580 Bremerhaven +27576 Bremerhaven +27574 Bremerhaven +68623 Lampertheim +68647 Biblis +64589 Stockstadt am Rhein +65468 Trebur +64560 Riedstadt +64839 Münster +64385 Reichelsheim (Odenwald) +69239 Neckarsteinach +69483 Wald-Michelbach +69434 Hirschhorn, Brombach, Heddesbach +69151 Neckargemünd +69412 Eberbach +64760 Oberzent +64757 Unter-Hainbrunn +68519 Viernheim +68642 Bürstadt +68649 Groß-Rohrheim +64579 Gernsheim +64625 Bensheim +64646 Heppenheim (Bergstraße) +64683 Einhausen +64653 Lorsch +64673 Zwingenberg +69517 Gorxheimertal +69509 Mörlenbach +69488 Birkenau +64658 Fürth +64678 Lindenfels +64686 Lautertal (Odenwald) +64668 Rimbach +64689 Grasellenbach +69518 Abtsteinach +64319 Pfungstadt +64521 Groß-Gerau +64665 Alsbach-Hähnlein +64584 Biebesheim am Rhein +64404 Bickenbach +64331 Weiterstadt +64572 Büttelborn +64295 Darmstadt +64347 Griesheim +64289 Darmstadt +64293 Darmstadt +64397 Modautal +64846 Groß-Zimmern +64380 Roßdorf +64342 Seeheim-Jugenheim +64297 Darmstadt +64287 Darmstadt +64354 Reinheim +64409 Messel +64291 Darmstadt +64372 Ober-Ramstadt +64285 Darmstadt +64401 Groß-Bieberau +64807 Dieburg +64407 Fränkisch-Crumbach +64367 Mühltal +64405 Fischbachtal +64283 Darmstadt +64711 Erbach +64732 Bad König +64753 Brombachtal +64720 Michelstadt +64756 Mossautal +64853 Otzberg +64823 Groß-Umstadt +64850 Schaafheim +64747 Breuberg +64739 Höchst i. Odw. +64395 Brensbach +64750 Lützelbach +64832 Babenhausen +35690 Dillenburg +65510 Hünstetten, Idstein +61279 Grävenwiesbach +35708 Haiger +35753 Greifenstein +35781 Weilburg +65606 Villmar +65391 Lorch +65375 Oestrich-Winkel +65366 Geisenheim +65321 Heidenrod +65385 Rüdesheim am Rhein +65388 Schlangenbad +65307 Bad Schwalbach +65582 Diez, Hambach, Aull +65329 Hohenstein +65347 Eltville am Rhein +65344 Eltville am Rhein +65399 Kiedrich +65346 Eltville am Rhein +65232 Taunusstein +65343 Eltville am Rhein +65396 Walluf +65345 Eltville am Rhein +65195 Wiesbaden +65201 Wiesbaden +65199 Wiesbaden +65197 Wiesbaden +65428 Rüsselsheim +65474 Bischofsheim +65462 Ginsheim-Gustavsburg +65439 Flörsheim am Main +65817 Eppstein +65239 Hochheim am Main +65719 Hofheim am Taunus +65779 Kelkheim +65191 Wiesbaden +65183 Wiesbaden +65205 Wiesbaden +65187 Wiesbaden +65207 Wiesbaden +65193 Wiesbaden +65203 Wiesbaden +55246 Wiesbaden +55252 Mainz-Kastel +65185 Wiesbaden +65189 Wiesbaden +65326 Aarbergen +65597 Hünfelden +65611 Brechen +65529 Waldems +65527 Niedernhausen +65520 Bad Camberg +61479 Glashütten +61276 Weilrod +65618 Selters +65604 Elz +65554 Limburg +65556 Limburg +65550 Limburg +35794 Mengerskirchen +65589 Hadamar +65594 Runkel +65599 Dornburg +65614 Beselich +65620 Waldbrunn +35799 Merenberg +65549 Limburg +65553 Limburg +65555 Limburg +65551 Limburg +65552 Limburg +65627 Elbtal +35606 Solms +35619 Braunfels +35638 Leun +35789 Weilmünster +35792 Löhnberg +35796 Weinbach +35759 Driedorf +35767 Breitscheid +35683 Dillenburg +35688 Dillenburg +35684 Dillenburg +35687 Dillenburg +35630 Ehringshausen +35745 Herborn +35756 Mittenaar +35764 Sinn +35768 Siegbach +35686 Dillenburg +35685 Dillenburg +35689 Dillenburg +35236 Breidenbach +57319 Bad Berleburg +35713 Eschenburg +35716 Dietzhölztal +65451 Kelsterbach +65479 Raunheim +65812 Bad Soden am Taunus +65795 Hattersheim +65824 Schwalbach am Taunus +65931 Frankfurt am Main +65929 Frankfurt am Main +65760 Eschborn +60549 Frankfurt am Main +60488 Frankfurt am Main +60529 Frankfurt am Main +64569 Nauheim +64546 Mörfelden-Walldorf +64390 Erzhausen +63263 Neu-Isenburg +61239 Ober-Mörlen +61191 Rosbach v.d. Höhe +35287 Amöneburg +35327 Ulrichstein +36304 Alsfeld +36320 Kirtorf +35305 Grünberg +35321 Laubach +35410 Hungen +35447 Reiskirchen +35510 Butzbach +61250 Usingen +63654 Büdingen +63691 Ranstadt +63607 Wächtersbach +65830 Kriftel +65835 Liederbach am Taunus +65843 Sulzbach (Taunus) +65936 Frankfurt am Main +65934 Frankfurt am Main +65933 Frankfurt am Main +60489 Frankfurt am Main +63065 Offenbach am Main +63069 Offenbach am Main +63073 Offenbach am Main +63071 Offenbach am Main +60598 Frankfurt am Main +60435 Frankfurt am Main +60431 Frankfurt am Main +60386 Frankfurt am Main +60388 Frankfurt am Main +60528 Frankfurt am Main +60389 Frankfurt am Main +60314 Frankfurt am Main +60599 Frankfurt am Main +64859 Eppertshausen +63477 Maintal +63165 Mühlheim am Main +63329 Egelsbach +63225 Langen +63303 Dreieich +63150 Heusenstamm +63128 Dietzenbach +63322 Rödermark +60325 Frankfurt am Main +60326 Frankfurt am Main +60487 Frankfurt am Main +60486 Frankfurt am Main +60327 Frankfurt am Main +60323 Frankfurt am Main +60316 Frankfurt am Main +60322 Frankfurt am Main +60329 Frankfurt am Main +60594 Frankfurt am Main +60313 Frankfurt am Main +60311 Frankfurt am Main +60318 Frankfurt am Main +60385 Frankfurt am Main +60596 Frankfurt am Main +60320 Frankfurt am Main +60308 Frankfurt +60310 Frankfurt am Main (Taunusturm) +60306 Frankfurt am Main, Opernturm +63067 Offenbach am Main +63075 Offenbach am Main +61267 Neu-Anspach +61462 Königstein im Taunus +61440 Oberursel (Taunus) +61389 Schmitten +61476 Kronberg im Taunus +61273 Wehrheim +61350 Bad Homburg v.d. Höhe +61381 Friedrichsdorf +60439 Frankfurt am Main +61449 Steinbach (Taunus) +61348 Bad Homburg v.d. Höhe +60438 Frankfurt am Main +61203 Reichelsheim (Wetterau) +61184 Karben +61206 Wöllstadt +61231 Bad Nauheim +61138 Niederdorfelden +61352 Bad Homburg v.d. Höhe +61118 Bad Vilbel +61194 Niddatal +61169 Friedberg (Hessen) +61137 Schöneck +60437 Frankfurt am Main +60433 Frankfurt am Main +63452 Hanau +63456 Hanau +63454 Hanau +63457 Hanau +63110 Rodgau +63179 Obertshausen +63517 Rodenbach +63538 Großkrotzenburg +63512 Hainburg +63500 Seligenstadt +63526 Erlensee +63533 Mainhausen +63450 Hanau +63579 Freigericht +63776 Mömbris +61130 Nidderau +61197 Florstadt +63674 Altenstadt +63683 Ortenberg +63695 Glauburg +63694 Limeshain +63486 Bruchköbel +63543 Neuberg +63546 Hammersbach +63505 Langenselbold +63549 Ronneburg +63699 Kefenrod +63571 Gelnhausen +63584 Gründau +63589 Linsengericht +63594 Hasselroth +35576 Wetzlar +35581 Wetzlar +35582 Wetzlar +35578 Wetzlar +35398 Gießen +35625 Hüttenberg +35633 Lahnau +35641 Schöffengrund +35428 Langgöns +35647 Waldolms +35580 Wetzlar +35579 Wetzlar +35394 Gießen +35392 Gießen +61200 Wölfersheim +35415 Pohlheim +35423 Lich +35440 Linden +35463 Fernwald +35516 Münzenberg +35519 Rockenberg +35584 Wetzlar +35585 Wetzlar +35614 Aßlar +35644 Hohenahr +35649 Bischoffen +35075 Gladenbach +35080 Bad Endbach +35102 Lohra +35444 Biebertal +35452 Heuchelheim +35586 Wetzlar +35583 Wetzlar Garbeinheim +35043 Marburg +35396 Gießen +35085 Ebsdorfergrund +35096 Weimar (Lahn) +35112 Fronhausen +35418 Buseck +35435 Wettenberg +35457 Lollar +35460 Staufenberg +35469 Allendorf +35390 Gießen +61209 Echzell +63667 Nidda +63679 Schotten +63697 Hirzenhain +63688 Gedern +35260 Stadtallendorf +35315 Homberg (Ohm) +35325 Mücke +35329 Gemünden (Felda) +35466 Rabenau +36325 Feldatal +36326 Antrifttal +36329 Romrod +63599 Biebergemünd +63637 Jossgrund +63639 Flörsbachtal +36115 Hilders, Ehrenberg +36088 Hünfeld +36132 Eiterfeld +36145 Hofbieber +36154 Hosenfeld +36100 Petersberg +36381 Schlüchtern +36272 Niederaula +63628 Bad Soden-Salmünster +36396 Steinau an der Straße +63619 Bad Orb +63636 Brachttal +63633 Birstein +36391 Sinntal +36355 Grebenhain +36358 Herbstein +36369 Lautertal +36399 Freiensteinau +36041 Fulda +36039 Fulda +36103 Flieden +36119 Neuhof +36137 Großenlüder +36148 Kalbach +36318 Schwalmtal +36323 Grebenau +36341 Lauterbach +36367 Wartenberg +36287 Breitenbach am Herzberg +36110 Schlitz +36151 Burghaun +36364 Bad Salzschlirf +36166 Haunetal +36043 Fulda +36093 Künzell +36124 Eichenzell +36129 Gersfeld +36157 Ebersburg +36160 Dipperz +36163 Poppenhausen +36037 Fulda +36167 Nüsttal +36169 Rasdorf +36142 Tann +35719 Angelburg +35216 Biedenkopf +35232 Dautphetal +35239 Steffenberg +35088 Battenberg +35116 Hatzfeld +35083 Wetter +35282 Rauschenberg +34537 Bad Wildungen +34549 Edertal +34508 Willingen (Upland) +34630 Gilserberg +34632 Jesberg +35041 Marburg +35091 Cölbe +35117 Münchhausen +35094 Lahntal +35099 Burgwald +35119 Rosenthal +35039 Marburg +35037 Marburg +59969 Bromskirchen, Hallenberg +35108 Allendorf +35066 Frankenberg +35104 Lichtenfels +35274 Kirchhain +35288 Wohratal +35285 Gemünden +35279 Neustadt +34599 Neuental +34613 Schwalmstadt +34628 Willingshausen +34513 Waldeck +34516 Vöhl +35110 Frankenau +35114 Haina +34560 Fritzlar +34582 Borken +34596 Bad Zwesten +34311 Naumburg +34497 Korbach +34519 Diemelsee +34431 Marsberg +34454 Bad Arolsen +34477 Twistetal +34471 Volkmarsen +34289 Zierenberg +34308 Bad Emstal +34466 Wolfhagen +34474 Diemelstadt +34396 Liebenau (Hessen) +34479 Breuna +34576 Homberg +34621 Frielendorf +34626 Neukirchen (Knüll) +34633 Ottrau +34637 Schrecksbach +34639 Schwarzenborn +36280 Oberaula +34369 Hofgeismar +37216 Witzenhausen, Gutsbezirk +36251 Bad Hersfeld, Ludwigsau +34281 Gudensberg +34593 Knüllwald +34323 Malsfeld +36199 Rotenburg an der Fulda +36211 Alheim +36214 Nentershausen +36205 Sontra +37235 Hessisch Lichtenau +37242 Bad Sooden-Allendorf +34298 Helsa +34320 Söhrewald +36275 Kirchheim (Hessen) +36286 Neuenstein (Hessen) +34305 Niedenstein +34587 Felsberg +34590 Wabern +34212 Melsungen +34286 Spangenberg +34302 Guxhagen +34326 Morschen +34327 Körle +36179 Bebra +36217 Ronshausen +36277 Schenklengsfeld +36282 Hauneck +36289 Friedewald +36208 Wildeck +36266 Heringen +36269 Philippsthal +36284 Hohenroda +36219 Cornberg +37290 Meißner +37284 Waldkappel +37269 Eschwege +37276 Meinhard +37287 Wehretal +37293 Herleshausen +37296 Ringgau +34128 Kassel +34131 Kassel +34132 Kassel +34134 Kassel +34270 Schauenburg +34295 Edermünde +34225 Baunatal +34292 Ahnatal +34379 Calden +34246 Vellmar +34314 Espenau +34317 Habichtswald +34130 Kassel +34123 Kassel +34121 Kassel +34233 Kassel, Fuldatal +34125 Kassel +34355 Staufenberg +34253 Lohfelden +34260 Kaufungen +34277 Fuldabrück +34266 Niestetal +34329 Nieste +34346 Hann. Münden, Gutsbezirk Reinhardswald +34119 Kassel +34117 Kassel +34127 Kassel +34388 Trendelburg +34393 Grebenstein +34399 Oberweser +34376 Immenhausen +34359 Reinhardshagen +37218 Witzenhausen +37215 Witzenhausen +37217 Witzenhausen +37213 Witzenhausen +37249 Neu-Eichenberg +37247 Großalmerode +37297 Berkatal +37214 Witzenhausen +34385 Bad Karlshafen +37194 Bodenfelde, Wahlsburg +37281 Wanfried +37299 Weißenborn +27499 Neuwerk +21037 Hamburg +21149 Hamburg +21147 Hamburg +21129 Hamburg +21075 Hamburg +21077 Hamburg +21079 Hamburg +21107 Hamburg +21109 Hamburg +22113 Hamburg, Oststeinbek +21073 Hamburg +22609 Hamburg +22547 Hamburg +22559 Hamburg +22869 Schenefeld +22587 Hamburg +22589 Hamburg +22549 Hamburg +20539 Hamburg +20457 Hamburg +20537 Hamburg +22397 Hamburg +22605 Hamburg +22763 Hamburg +22335 Hamburg +22399 Hamburg +22417 Hamburg +22419 Hamburg +22453 Hamburg +22457 Hamburg +22459 Hamburg +22043 Hamburg +22111 Hamburg +22179 Hamburg +22391 Hamburg +22765 Hamburg +22767 Hamburg +22529 Hamburg +22527 Hamburg +22523 Hamburg +22307 Hamburg +22041 Hamburg +20359 Hamburg +22761 Hamburg +22607 Hamburg +22525 Hamburg +20357 Hamburg +20253 Hamburg +22769 Hamburg +20255 Hamburg +20259 Hamburg +20257 Hamburg +20095 Hamburg +20459 Hamburg +20097 Hamburg +20148 Hamburg +20146 Hamburg +20149 Hamburg +20144 Hamburg +20099 Hamburg +20354 Hamburg +20355 Hamburg +20249 Hamburg +20251 Hamburg +22087 Hamburg +22085 Hamburg +22301 Hamburg +22299 Hamburg +22303 Hamburg +22297 Hamburg +20535 Hamburg +22177 Hamburg +22089 Hamburg +22083 Hamburg +22081 Hamburg +22305 Hamburg +22049 Hamburg +22455 Hamburg +22415 Hamburg +22337 Hamburg +22339 Hamburg +22309 Hamburg +21031 Hamburg +21033 Hamburg +21035 Hamburg +21039 Hamburg +21029 Hamburg +22359 Hamburg +22143 Hamburg +22145 Hamburg +22159 Hamburg +22117 Hamburg +22119 Hamburg +22393 Hamburg +22115 Hamburg +22045 Hamburg +22047 Hamburg +22147 Hamburg +22149 Hamburg +22175 Hamburg +22395 Hamburg +19273 Amt Neuhaus, Stapel +19372 Spornitz +19294 Neu Kaliß +19288 Ludwigslust +19300 Grabow u.a. +19249 Lübtheen +19303 Dömitz +19357 Karstädt, Dambeck, Klüß +17209 Wredenhagen +19376 Marnitz, Siggelkow +17258 Feldberger Seenlandschaft +17255 Wesenberg +17235 Neustrelitz +17237 Möllenbeck +17248 Rechlin +17252 Mirow +19217 Rehna, Carlow u.a. +19246 Zarrentin +19258 Boizenburg, Gresse, Greven u.a. +23942 Dassow +19260 Vellahn +23923 Schönberg +18230 Rerik, Bastorf, Biendorf +18236 Kröpelin, Carinerland +19079 Banzkow, Sukow +19374 Domsühl, Mestlin, Obere Warnow u.a. +19089 Crivitz, Friedrichsruhe u.a. +19069 Lübstorf +19417 Warin +19243 Wittenburg u.a. +19230 Hagenow u.a. +19306 Neustadt-Glewe +23936 Grevesmühlen, Stepenitztal, Upahl u.a. +23948 Klütz +23974 Neuburg-Steinhausen, Hornstorf +23996 Bad Kleinen u.a. +23966 Wismar, Groß Krankow u.a. +23970 Wismar +19077 Rastow +19205 Gadebusch +19209 Lützow +19057 Schwerin +19071 Brüsewitz +19075 Pampow +19073 Wittenförden u.a. +19370 Parchim +19086 Plate +19067 Leezen +19065 Pinnow +19061 Schwerin +19063 Schwerin +19055 Schwerin +19053 Schwerin +19059 Schwerin +19412 Brüel +23968 Barnekow, Gägelow u.a. +23946 Boltenhagen +23972 Dorf Mecklenburg, Lübow u.a. +23992 Neukloster +23999 Insel Poel +18233 Neubukow, Ravensberg u.a. +18225 Kühlungsborn +18195 Tessin, Grammow u.a. +18276 Reimershagen, Lohmen, Zehna, Hägerfelde u.a. +17179 Gnoien u.a. +18299 Laage, Wardow u.a. +18246 Bützow u.a. +17192 Waren/ Müritz +17207 Röbel/Müritz +17194 Grabowhöfe, Moltzow u.a. +17213 Malchow u.a. +18334 Bad Sülze +18337 Marlow +19395 Plau am See +19406 Sternberg +19399 Goldberg +19386 Lübz, Passow +18196 Dummerstorf +17214 Nossentiner Hütte +18279 Lalendorf, Langhagen +18292 Krakow, Dobbin-Linstow u.a. +17166 Dahmen, Groß Wokern, Teterow +18249 Bernitt, Qualitz, Warnow, Zernin u.a. +18258 Schwaan u.a. +18273 Güstrow +18211 Retschow, Admannshagen-Bargeshagen u.a. +18209 Bad Doberan, Bartenshagen-Parkentin u.a. +18239 Satow +18198 Stäbelow, Kritzmow +18069 Rostock, Lambrechtshagen +18106 Rostock +18059 Rostock, Papendorf u.a. +18055 Rostock +18147 Rostock +18146 Rostock +18057 Rostock +17168 Jördenstorf, Prebberede u.a. +18184 Roggentin, Broderstorf u.a. +18182 Rostock, Gelbensande, Rövershagen u.a. +18190 Sanitz +17348 Woldegk +17099 Friedland, Galenbeck, Datzetal +17039 Sponholz, Neunkirchen u.a. +17349 Groß Miltzow +17098 Friedland +17094 Cölpin +17121 Loitz +17153 Stavenhagen +17091 Rosenow +17139 Malchin +17089 Burow +17129 Bentzin +17111 Demmin u.a. +17391 Krien u.a. +17495 Karlsburg +17392 Spantekow +18513 Glewitz +18516 Rakow +18519 Miltzow u.a. +18507 Grimmen +18461 Franzburg, Richtenberg u.a. +17219 Möllenhagen +17217 Penzlin +17033 Neubrandenburg +17087 Altentreptow +17036 Neubrandenburg +17034 Neubrandenburg +17159 Dargun +17154 Neukalen +17109 Demmin +18465 Tribsees +17126 Jarmen +17506 Gützkow +17489 Greifswald +17491 Greifswald +17493 Greifswald +17498 Neuenkirchen +18311 Ribnitz-Damgarten +18356 Barth +18107 Elmenhorst/Lichtenhagen, Rostock +18119 Rostock +18109 Rostock +18181 Rostock, Graal-Müritz +18347 Dierhagen +18320 Ahrenshagen-Daskow, Trinwillershagen u.a. +18317 Saal +18375 Prerow a. Darß +18314 Löbnitz +18469 Velgast +18556 Dranske +18528 Bergen/ Rügen +18581 Putbus +18574 Garz/ Rügen +18569 Gingst +18573 Samtens +18551 Sagard +18442 Niepars +18445 Prohn +18437 Stralsund +18510 Wittenhagen +18374 Zingst a. Darß +18435 Stralsund +18439 Stralsund +18565 Hiddensee +17328 Penkun u.a. +17440 Kröslin, Krummin, Lassan u.a. +17406 Usedom u.a. +17373 Ueckermünde +17321 Löcknitz, Rothenklempenow +17375 Vogelsang-Warsin, Meiersberg, Mönkebude u.a. +17322 Blankensee, Grambow u.a. +17367 Eggesin +17309 Pasewalk u.a. +17337 Uckerland, Groß Luckow, Schönhausen +17390 Klein Bünzow +17398 Ducherow +17509 Lubmin +17335 Strasburg +17379 Ferdinandshof u.a. +17329 Krackow, Nadrensee +17358 Torgelow +17389 Anklam +17449 Karlshagen +17438 Wolgast +17429 Benz, Heringsdorf u.a. +17419 Seebad Ahlbeck +17454 Zinnowitz +17459 Koserow +17424 Ostseebad Heringsdorf +18586 Sellin +18609 Binz +18546 Sassnitz +49847 Itterbeck/Wielen +49824 Ringe, Laar, Emlichheim +26757 Borkum +37127 Dransfeld u.a. +37170 Uslar +37176 Nörten-Hardenberg +37181 Hardegsen +37120 Bovenden +37133 Friedland +34355 Staufenberg +34346 Hann. Münden, Gutsbezirk Reinhardswald +37079 Göttingen +37124 Rosdorf +37139 Adelebsen +37085 Göttingen +37077 Göttingen +37075 Göttingen +37081 Göttingen +37083 Göttingen +37136 Seulingen, Waake u.a. +37130 Gleichen +37073 Göttingen +48527 Nordhorn +49811 Lingen +48465 Engden, Isterberg, Schüttorf u.a. +49828 Esche, Georgsdorf, Lage, Neuenhaus, Osterwald +49843 Uelsen, Halle, Gölenkamp, Getelo +48529 Nordhorn +48531 Nordhorn +49835 Wietmarschen +48455 Bad Bentheim +48488 Emsbüren +48499 Salzbergen +48480 Lünne, Schapen, Spelle +49832 Andervenne, Beesten, Freren, Messingen, Thuine +49328 Melle +49090 Osnabrück +49086 Osnabrück +49586 Merzen, Neuenkirchen +49143 Bissendorf +49163 Bohmte +49179 Ostercappeln +49134 Wallenhorst +49565 Bramsche +49434 Neuenkirchen-Vörden +49078 Osnabrück +49170 Hagen am Teutoburger Wald +49205 Hasbergen +49219 Glandorf +49599 Voltlage +49076 Osnabrück +49082 Osnabrück +49124 Georgsmarienhütte +49176 Hilter +49186 Bad Iburg +49196 Bad Laer +49201 Dissen +49214 Bad Rothenfelde +49326 Melle +49324 Melle +49080 Osnabrück +49088 Osnabrück +49191 Belm +49084 Osnabrück +49074 Osnabrück +49448 Lemförde u.a. +49152 Bad Essen +49846 Hoogstede +49849 Wilsum +26835 Hesel, Neukamperfehn u.a. +26897 Esterwegen u.a. +49777 Stavern +49744 Geeste +49733 Haren +49767 Twist +49751 Sögel u.a. +49762 Sustrum, Lathen u.a. +49779 Oberlangen, Niederlangen +26802 Moormerland +26817 Rhauderfehn +26826 Weener +26847 Detern +26871 Papenburg +49774 Lähden +49809 Lingen +49808 Lingen +49716 Meppen +49740 Haselünne +49838 Lengerich u.a. +49770 Herzlake, Dohren +49844 Bawinkel +26892 Dörpen, Lehe u.a. +26906 Dersum +26907 Walchum +26899 Rhede (Ems) +26909 Neubörger, Neulehe +26903 Surwold +26904 Börger +26831 Bunde +26844 Jemgum +26789 Leer +26810 Westoverledingen +26845 Nortmoor +49626 Berge, Bippen +49584 Fürstenau +49457 Drebber +49577 Kettenkamp, Eggermühlen, Ankum +49757 Werlte, Vrees, Lahn +26939 Ovelgönne +26160 Bad Zwischenahn +26169 Friesoythe +26180 Rastede +26188 Edewecht +26655 Westerstede +26689 Apen +26931 Elsfleth +26670 Uplengen +27793 Wildeshausen +27804 Berne +27798 Hude (Oldenburg) +27801 Dötlingen +26676 Barßel +26683 Saterland +26842 Ostrhauderfehn +49593 Bersenbrück +49393 Lohne +49624 Löningen +49637 Menslage +49401 Damme +49439 Steinfeld +49635 Badbergen +49661 Cloppenburg +49681 Garrel +49685 Emstek +49696 Molbergen +49594 Alfhausen +49638 Nortrup +49699 Lindern (Oldenburg) +49632 Essen (Oldenburg) +49688 Lastrup +49610 Quakenbrück +49597 Rieste +49451 Holdorf +49596 Gehrde +49413 Dinklage +49356 Diepholz +49459 Lembruch, Burlage +49456 Bakum +49692 Cappeln (Oldenburg) +49377 Vechta +49424 Goldenstedt +49429 Visbek +26901 Lorup, Rastdorf +26219 Bösel +26849 Filsum +26197 Großenkneten +26203 Wardenburg +26209 Hatten +26131 Oldenburg (Oldenburg) +26129 Oldenburg (Oldenburg) +26127 Oldenburg (Oldenburg) +26215 Wiefelstede +26133 Oldenburg (Oldenburg) +26135 Oldenburg (Oldenburg) +26125 Oldenburg (Oldenburg) +26123 Oldenburg (Oldenburg) +26122 Oldenburg +26121 Oldenburg (Oldenburg) +31737 Rinteln +31855 Aerzen +31840 Hessisch Oldendorf +31812 Bad Pyrmont +32361 Preußisch Oldendorf +31606 Warmsen +31603 Diepenau +31675 Bückeburg +31683 Obernkirchen +31707 Heeßen, Bad Eilsen +31749 Auetal +31711 Luhden +31710 Buchholz +32423 Minden +31604 Raddestorf +31553 Auhagen, Sachsenhagen +31556 Wölpinghausen +31715 Meerbeck +31702 Lüdersfeld +31691 Helpsen, Seggebruch +31717 Nordsehl +31688 Nienstädt +31718 Pollhagen +31719 Wiedensahl +31547 Rehburg-Loccum +31712 Niederwöhren +31655 Stadthagen +31708 Ahnsen +31693 Hespe +31714 Lauenhagen +37691 Boffzen, Derental +37603 Holzminden +37697 Lauenförde +37699 Fürstenberg +31515 Wunstorf +31860 Emmerthal +31020 Salzhemmendorf +31863 Coppenbrügge +31174 Schellerten +31167 Bockenem +31311 Uetze +37586 Dassel +37194 Bodenfelde, Wahlsburg +31535 Neustadt am Rübenberge +30890 Barsinghausen +30974 Wennigsen (Deister) +31832 Springe +30982 Pattensen +30855 Langenhagen +37632 Holzen, Eschershausen, Eimen +38723 Seesen +37574 Einbeck, Kreiensen +30916 Isernhagen +37627 Stadtoldendorf u.a. +31868 Ottenstein +37619 Bodenwerder, Pegestorf, Kirchbrak, Hehlen +37647 Brevörde, Polle, Vahlbruch +37649 Heinsen +31089 Duingen +37620 Halle +37635 Lüerdissen +37640 Golmbach +37633 Dielmissen +37639 Bevern +37643 Negenborn +37642 Holenberg +37186 Moringen +37589 Kalefeld +37154 Northeim +37191 Katlenburg-Lindau +31079 Sibbesse +31028 Gronau (Leine) +31073 Grünenplan, Delligsen +31061 Alfeld (Leine) +31084 Freden (Leine) +31162 Bad Salzdetfurth +37581 Bad Gandersheim +31195 Lamspringe +31867 Pohle, Lauenau, Messenkamp, Hülsede u.a. +31848 Bad Münder am Deister +31789 Hameln +31787 Hameln +31785 Hameln +31008 Elze +31036 Eime +31542 Bad Nenndorf +31558 Hagenburg +31698 Lindhorst +31699 Beckedorf +31700 Heuerßen +31552 Apelern, Rodenberg +31559 Haste, Hohnhorst +31555 Suthfeld +30823 Garbsen +30827 Garbsen +30826 Garbsen +30926 Seelze +30952 Ronnenberg +30989 Gehrden +30455 Hannover +30453 Hannover +30419 Hannover +31171 Nordstemmen +31157 Sarstedt +31180 Giesen +31139 Hildesheim +31249 Hohenhameln +31135 Hildesheim +31141 Hildesheim +31177 Harsum +31137 Hildesheim +31199 Diekholzen +31188 Holle +31191 Algermissen +31134 Hildesheim +30165 Hannover +30966 Hemmingen +30880 Laatzen +30459 Hannover +30853 Langenhagen +30175 Hannover +30655 Hannover +30173 Hannover +30659 Hannover +30629 Hannover +30519 Hannover +30539 Hannover +30559 Hannover +30457 Hannover +30521 Hannover +30451 Hannover +30167 Hannover +30449 Hannover +30169 Hannover +30159 Hannover +30163 Hannover +30161 Hannover +30171 Hannover +30851 Langenhagen +30179 Hannover +30177 Hannover +30625 Hannover +30627 Hannover +30657 Hannover +31319 Sehnde +31275 Lehrte +31303 Burgdorf +49453 Barver, Dickel, Hemsloh, Rehden, Wetschen, Wehrblecker Heide +49406 Barnstorf, Eydelstedt, Drentwede +49419 Wagenfeld +31632 Husum +31608 Marklohe +27404 Zeven, Elsdorf +27367 Sottrum, Reeßum, Bötersen u.a. +27412 Tarmstedt, Breddorf u.a. +27628 Hagen +27777 Ganderkesee +27711 Osterholz-Scharmbeck +28865 Lilienthal +28870 Ottersberg +31600 Uchte +27245 Bahrenborstel, Barenburg, Kirchdorf +27259 Freistatt, Varrel, Wehrbleck +27232 Sulingen +27243 Harpstedt, Groß Ippener, Colnrade u.a. +27239 Twistringen +27251 Neuenkirchen, Scholen +27252 Schwaförden +27211 Bassum +27257 Affinghausen und Sudwalde +27248 Ehrenburg +28857 Syke +31621 Pennigsehl +31595 Steyerberg +31592 Stolzenau +27249 Maasen, Mellinghausen +27246 Borstel +31618 Liebenau +31629 Estorf +31628 Landesbergen +31633 Leese +31582 Nienburg/Weser +31619 Binnen +31613 Wietzen +27254 Siedenburg, Staffhorst +27327 Martfeld, Schwarme +27305 Bruchhausen-Vilsen, Süstedt +27330 Asendorf +27324 Eystrup, Hassel u.a. +31627 Rohrsen +27333 Schweringen, Warpe, Bücken +31626 Haßbergen +31609 Balge +27318 Hoya, Hoyerhagen, Hilgermissen +27313 Dörverden +31623 Drakenburg +27755 Delmenhorst +27753 Delmenhorst +27751 Delmenhorst +28816 Stuhr +28844 Weyhe +27749 Delmenhorst +28790 Schwanewede +27809 Lemwerder +27729 Hambergen, Holste u.a. +27721 Ritterhude +27321 Thedinghausen, Emtinghausen +27339 Riede +28832 Achim +28876 Oyten +28325 Bremen +27283 Verden +27299 Langwedel +27337 Blender +27726 Worpswede +28879 Grasberg +31638 Stöckse +31637 Rodewald +31636 Linsburg +31634 Steimbke +29643 Neuenkirchen +21385 Oldendorf (Luhe), Amelinghausen, Rehlingen +21244 Buchholz in der Nordheide +21255 Tostedt, Kakenstorf u.a. +29303 Bergen, Lohheide u.a. +29693 Hodenhagen u.a. +29664 Walsrode, Ostenholz +29227 Celle +29229 Celle, Wittbeck +29223 Celle +27386 Bothel, Kirchwalsede u.a. +29308 Winsen (Aller) +29649 Wietzendorf +27383 Scheeßel +29633 Munster +21258 Heidenau +29640 Schneverdingen, Heimbuch +29646 Bispingen +29683 Bad Fallingbostel, Osterheide +27356 Rotenburg +29320 Hermannsburg +29328 Faßberg +30938 Burgwedel +29690 Schwarmstedt u.a. +30900 Wedemark +31622 Heemsen +27336 Rethem (Aller), Häuslingen, Frankenfeld +27308 Kirchlinteln +29323 Wietze +30669 Langenhagen (Flughafen) +29225 Celle +29313 Hambühren +29336 Nienhagen +29352 Adelheidsdorf +29221 Celle +27374 Visselhövede +29699 Bomlitz +27419 Sittensen u.a. +27389 Fintel, Lauenbrück u.a. +29614 Soltau +21259 Otter +21256 Handeloh +21274 Undeloh +21261 Welle +21272 Eggestorf +21376 Salzhausen +21271 Hanstedt, Asendorf +21442 Toppenstedt +21438 Brackel +21439 Marxen +21266 Jesteburg +37445 Walkenried +37412 Herzberg, Elbingerode, Hörden +37434 Gieboldehausen, Rhumequelle +37115 Duderstadt +37431 Bad Lauterberg +37441 Bad Sachsa +37539 Bad Grund +37197 Hattorf +37199 Wulften +37520 Osterode am Harz +37444 Braunlage +38170 Schöppenstedt +38678 Clausthal-Zellerfeld, Oberschulenberg +38667 Bad Harzburg, Torfhaus +38690 Goslar +38312 Dorstadt, Flöthe, Börßum u.a. +38315 Schladen-Werla +38536 Meinersen +38551 Ribbesbüttel +38709 Wildemann +38707 Altenau, Schulenberg +38729 Hahausen, Lutter, Wallmoden +38685 Langelsheim +38279 Sehlde +38644 Goslar +38642 Goslar +38259 Salzgitter +38704 Liebenburg +38640 Goslar +38700 Braunlage +31185 Söhlde +38228 Salzgitter +38272 Burgdorf +38271 Baddeckenstedt +38274 Elbe +38275 Haverlah +38277 Heere +31246 Lahstedt +38268 Lengede +38304 Wolfenbüttel +38122 Braunschweig +38120 Braunschweig +38229 Salzgitter +38239 Salzgitter +38159 Vechelde +38226 Salzgitter +31234 Edemissen +31224 Peine +31228 Peine +31241 Ilsede +31226 Peine +38110 Braunschweig +38116 Braunschweig +38114 Braunschweig +38112 Braunschweig +38176 Wendeburg +38179 Schwülper +38528 Adenbüttel +38530 Didderse +38533 Vordorf +38542 Leiferde +38543 Hillerse +38531 Rötgesbüttel +38118 Braunschweig +38302 Wolfenbüttel +38300 Wolfenbüttel +38126 Braunschweig +38173 Sickte, Dettum u.a. +38321 Denkte +38324 Kissenbrück +38319 Remlingen +38322 Hedeper +38327 Semmenstedt +38162 Cremlingen +38329 Wittmar +38124 Braunschweig +38373 Süpplingen, Frellstedt +38154 Königslutter am Elm +38375 Räbke +38325 Roklum +38381 Jerxheim +38382 Beierstedt +38384 Gevensleben +38387 Söllingen +38364 Schöningen +38376 Süpplingenburg +38378 Warberg +38100 Braunschweig +38102 Braunschweig +38106 Braunschweig +38104 Braunschweig +38108 Braunschweig +38442 Wolfsburg +38527 Meine +38165 Lehre +38547 Calberlah +38550 Isenbüttel +38553 Wasbüttel +38554 Weyhausen +38368 Rennau, Querenhorst, Mariental, Grasleben +38440 Wolfsburg +38444 Wolfsburg +38446 Wolfsburg +38448 Wolfsburg +38458 Velpke +38461 Danndorf +38462 Grafhorst +38464 Groß Twülpstedt +38350 Helmstedt +38372 Büddenstedt +38379 Wolsdorf +38459 Bahrdorf +29331 Lachendorf +29339 Wathlingen +29342 Wienhausen +29353 Ahnsbeck +29356 Bröckel +29358 Eicklingen +29364 Langlingen +29556 Suderburg +21354 Bleckede +19273 Amt Neuhaus, Stapel +29378 Wittingen +29386 Hankensbüttel, Obernholz, Dedelstorf +29348 Eschede +29473 Göhrde +29559 Wrestedt +21445 Wulfsen +29565 Wriedel +29355 Beedenbostel +29365 Sprakensehl +29456 Hitzacker (Elbe) +29562 Suhlendorf +29587 Natendorf +29499 Zernien +29525 Uelzen +29351 Eldingen +38518 Gifhorn +38559 Wagenhoff, Ringelah +29362 Hohne +29369 Ummern +38539 Müden (Aller) +29393 Groß Oesingen +29578 Eimke +29345 Unterlüß +29359 Habighorst +29361 Höfer +29367 Steinhorst +29379 Wittingen +29392 Wesendorf +29396 Schönewörde +29399 Wahrenholz +38524 Sassenburg +38556 Bokensdorf +38557 Osloß +38479 Tappenbeck +38470 Parsau +38465 Brome +38467 Bergfeld +38468 Ehra-Lessien +38477 Jembke +38476 Barwedel +38471 Rühen +38473 Tiddische +38474 Tülau +29389 Bad Bodenteich +29394 Lüder +29465 Schnega +29468 Bergen +29594 Soltendiek +29582 Hanstedt +29574 Ebstorf +29581 Gerdau +29593 Schwienau +29576 Barum +21394 Kirchgellersen, Westergellersen, Südergellersen +21391 Reppenstedt, Lüneburg +21358 Mechtersen +21441 Garstedt +21444 Vierhöfen +21386 Betzendorf +21388 Soderstorf +21406 Melbeck, Barnstedt +21335 Lüneburg +21379 Scharnebeck, Echem, Lüdersburg, Rullstorf +21339 Lüneburg +21337 Lüneburg +21357 Bardowick, Wittorf, Barum +21360 Vögelsen +21365 Adendorf +21407 Deutsch Evern +29553 Bienenbüttel +21409 Embsen +21403 Wendisch Evern +29549 Bad Bevensen +29579 Emmendorf +29584 Himbergen +29588 Oetzen +29590 Rätzlingen +29591 Römstedt +29599 Weste +29459 Clenze +29496 Waddeweitz +29571 Rosche +29597 Stoetze +29585 Jelmstorf +21368 Dahlenburg +21397 Barendorf, Vastorf +21398 Neetze +21400 Reinstorf +21401 Thomasburg +29575 Altenmedingen +21369 Nahrendorf +21371 Tosterglope +29490 Neu Darchau +29491 Prezelle +29471 Gartow +29476 Gusborn +29481 Karwitz +29439 Lüchow +29451 Dannenberg +29462 Wustrow +29482 Küsten +29487 Luckau (Wendland) +29479 Jameln +29475 Gorleben +29484 Langendorf +29485 Lemgow +29488 Lübbow +29494 Trebel +29497 Woltersdorf +29472 Damnatz +29478 Höhbeck +29493 Schnackenburg +26427 Esens, Neuharlingersiel u.a. +26629 Großefehn +26723 Emden +26736 Krummhörn +26571 Juist, Memmert +26506 Norden +26721 Emden +26725 Emden +26529 Upgant-Schott, Osteel u.a. +26624 Südbrookmerland +26759 Hinte +26632 Ihlow +26607 Aurich +26605 Aurich +26603 Aurich +26524 Hage, Halbemond u.a. +26532 Großheide +26548 Norderney +26553 Dornum +26487 Blomberg, Neuschoo +26556 Westerholt, Schweindorf u.a. +26489 Ochtersum +26579 Baltrum +26465 Langeoog +26639 Wiesmoor +26954 Nordenham +26969 Butjadingen +26409 Wittmund +26452 Sande +26345 Bockhorn +26349 Jade +26446 Friedeburg +26340 Zetel +26419 Schortens +26434 Wangerland +26441 Jever +26316 Varel +26937 Stadland +26936 Stadland +26384 Wilhelmshaven +26382 Wilhelmshaven +26389 Wilhelmshaven +26386 Wilhelmshaven +26388 Wilhelmshaven +26474 Spiekeroog +26486 Wangerooge +26935 Stadland +26919 Brake +27616 Beverstedt +27624 Geestland +27446 Selsingen +27432 Bremervörde +27478 Cuxhaven +21709 Himmelpforten +27612 Loxstedt +27449 Kutenholz +21730 Balje +21706 Drochtersen +21712 Großenwörden +27619 Schiffdorf +27607 Geestland +27639 Wurster Nordseeküste +21776 Wanna +27442 Gnarrenburg +21775 Ihlienworth +21769 Lamstedt +21770 Mittelstenahe +21772 Stinstedt +21782 Bülkau +21789 Wingst +21726 Oldendorf +21727 Estorf +21745 Hemmoor +21755 Hechthausen +21756 Osten +27476 Cuxhaven +27472 Cuxhaven +21765 Nordleda +27474 Cuxhaven +21763 Neuenkirchen +21762 Otterndorf +21785 Neuhaus (Oste) +21781 Cadenberge +21732 Krummendeich +21734 Oederquart +21787 Oberndorf +21698 Harsefeld +21702 Ahlerstedt +21717 Fredenbeck +21435 Stelle +25377 Kollmar, Pagensand +21640 Horneburg +21614 Buxtehude +21635 Jork +21641 Apensen +21643 Beckdorf +21644 Sauensiek +21646 Halvesbostel +21649 Regesbostel +21684 Stade +21682 Stade +21683 Stade +21710 Engelschoff +21714 Hammah +21720 Grünendeich +21723 Hollern-Twielenfleth +21680 Stade +21739 Dollern +21129 Hamburg +21629 Neu Wulmstorf +21279 Hollenstedt, Drestedt u.a. +21224 Rosengarten +21647 Moisburg +21218 Seevetal +21228 Harmstorf +21227 Bendestorf +21220 Seevetal +21217 Seevetal +21729 Freiburg (Elbe) +21737 Wischhafen +21423 Winsen +21449 Radbruch +19258 Boizenburg, Gresse, Greven u.a. +21382 Brietlingen +21380 Artlenburg +21395 Tespe +21436 Marschacht +21447 Handorf +21522 Hohnstorf (Elbe), Hittbergen +53881 Euskirchen +52080 Aachen +52076 Aachen +53945 Blankenheim +53949 Dahlem +53940 Hellenthal +53925 Kall +53894 Mechernich +53902 Bad Münstereifel +53937 Schleiden +52388 Nörvenich +52393 Hürtgenwald +52156 Monschau +52152 Simmerath +52066 Aachen +52074 Aachen +52064 Aachen +52068 Aachen +52222 Stolberg (Rhld.) +52078 Aachen +52224 Stolberg +52159 Roetgen +52223 Stolberg (Rhld.) +53947 Nettersheim +52355 Düren +52351 Düren +52379 Langerwehe +52385 Nideggen +52372 Kreuzau +52396 Heimbach +53909 Zülpich +50374 Erftstadt +52391 Vettweiß +52538 Gangelt, Selfkant +52070 Aachen +52072 Aachen +52531 Übach-Palenberg +52134 Herzogenrath +50129 Bergheim +47447 Moers +47652 Weeze +47506 Neukirchen-Vluyn +47669 Wachtendonk +41812 Erkelenz +41372 Niederkrüchten +41379 Brüggen +52445 Titz +52428 Jülich +52441 Linnich +52511 Geilenkirchen +46519 Alpen +46569 Hünxe +52062 Aachen +52457 Aldenhoven +52499 Baesweiler +52146 Würselen +52477 Alsdorf +52249 Eschweiler +52525 Waldfeucht, Heinsberg +41849 Wassenberg +41179 Mönchengladbach +41836 Hückelhoven +41844 Wegberg +41366 Schwalmtal +52349 Düren +52353 Düren +50181 Bedburg +50189 Elsdorf +52382 Niederzier +52459 Inden +50171 Kerpen +50170 Kerpen +50169 Kerpen +50127 Bergheim +50126 Bergheim +52399 Merzenich +41061 Mönchengladbach +41068 Mönchengladbach +41169 Mönchengladbach +41189 Mönchengladbach +41199 Mönchengladbach +41238 Mönchengladbach +41069 Mönchengladbach +41352 Korschenbroich +41363 Jüchen +41065 Mönchengladbach +41236 Mönchengladbach +41239 Mönchengladbach +41460 Neuss +41464 Neuss +41515 Grevenbroich +41516 Grevenbroich +41517 Grevenbroich +41542 Dormagen +41468 Neuss +41469 Neuss +41470 Neuss +41472 Neuss +41564 Kaarst +41569 Rommerskirchen +41466 Neuss +41751 Viersen +47638 Straelen +41334 Nettetal +47627 Kevelaer-Kervenheim +47625 Kevelaer-Wetten +47626 Kevelaer-Winnekendonk +47623 Kevelaer-Mitte +47624 Kevelaer-Twisteden +47608 Geldern +41748 Viersen +41749 Viersen +41063 Mönchengladbach +41066 Mönchengladbach +47804 Krefeld +47839 Krefeld +47929 Grefrath +47906 Kempen +47918 Tönisvorst +47877 Willich +41747 Viersen +41462 Neuss +47800 Krefeld +47802 Krefeld +47803 Krefeld +47807 Krefeld +47809 Krefeld +47829 Krefeld +40667 Meerbusch +40668 Meerbusch +40670 Meerbusch +47226 Duisburg +47239 Duisburg +47259 Duisburg +40474 Düsseldorf +40549 Düsseldorf +47798 Krefeld +47805 Krefeld +47799 Krefeld +47229 Duisburg +47665 Sonsbeck +47661 Issum +47475 Kamp-Lintfort +47509 Rheurdt +47647 Kerken +46535 Dinslaken +47198 Duisburg +47199 Duisburg +47441 Moers +47443 Moers +47445 Moers +47179 Duisburg +47139 Duisburg +47495 Rheinberg +46562 Voerde (Niederrhein) +47228 Duisburg +47178 Duisburg +47559 Kranenburg +47533 Kleve +46399 Bocholt +46395 Bocholt +46487 Wesel +47574 Goch +47546 Kalkar +46446 Emmerich am Rhein +46514 Schermbeck +46499 Hamminkeln +46414 Rhede +48691 Vreden +47551 Bedburg-Hau +47589 Uedem +46459 Rees +46419 Isselburg +46509 Xanten +46485 Wesel +46483 Wesel +46397 Bocholt +53332 Bornheim +53359 Rheinbach +53879 Euskirchen +53919 Weilerswist +53913 Swisttal +53177 Bonn +53125 Bonn +53175 Bonn +53227 Bonn +53229 Bonn +53225 Bonn +53117 Bonn +53127 Bonn +53123 Bonn +53844 Troisdorf +53859 Niederkassel +53347 Alfter +53340 Meckenheim +53343 Wachtberg +53757 Sankt Augustin +53115 Bonn +53129 Bonn +53113 Bonn +53119 Bonn +53121 Bonn +53111 Bonn +53604 Bad Honnef +53639 Königswinter +53773 Hennef (Sieg) +53179 Bonn +53173 Bonn +53783 Eitorf +51570 Windeck +57290 Neunkirchen +57299 Burbach +50935 Köln +50829 Köln +50737 Köln +50859 Köln +50739 Köln +50933 Köln +50997 Köln +50259 Pulheim +50226 Frechen +50354 Hürth +50321 Brühl +51688 Wipperfürth +51766 Engelskirchen +51789 Lindlar +58553 Halver +58566 Kierspe +50858 Köln +50931 Köln +50969 Köln +50937 Köln +50939 Köln +50823 Köln +50825 Köln +50827 Köln +53842 Troisdorf +50735 Köln +50996 Köln +50999 Köln +51105 Köln +51109 Köln +51061 Köln +51149 Köln +51147 Köln +51069 Köln +51067 Köln +51107 Köln +51143 Köln +51429 Bergisch Gladbach +51469 Bergisch Gladbach +51465 Bergisch Gladbach +51427 Bergisch Gladbach +50389 Wesseling +51145 Köln +50674 Köln +50678 Köln +50677 Köln +50679 Köln +50968 Köln +50733 Köln +50668 Köln +50676 Köln +50670 Köln +50672 Köln +50667 Köln +51063 Köln +51103 Köln +51065 Köln +40764 Langenfeld +50769 Köln +50765 Köln +51371 Leverkusen +41539 Dormagen +41540 Dormagen +41541 Dormagen +40595 Düsseldorf +40589 Düsseldorf +40721 Hilden, Düsseldorf +40724 Hilden +40593 Düsseldorf +40599 Düsseldorf +40223 Düsseldorf +40225 Düsseldorf +40221 Düsseldorf +40789 Monheim am Rhein +50767 Köln +40591 Düsseldorf +40597 Düsseldorf +42697 Solingen +42699 Solingen +42657 Solingen +42719 Solingen +42653 Solingen +51377 Leverkusen +51373 Leverkusen +51381 Leverkusen +51467 Bergisch Gladbach +42651 Solingen +42659 Solingen +42349 Wuppertal +42781 Haan +51519 Odenthal +51399 Burscheid +42799 Leichlingen +51379 Leverkusen +51375 Leverkusen +40723 Hilden +42655 Solingen +53840 Troisdorf +53809 Ruppichteroth +53804 Much +53819 Neunkirchen-Seelscheid +53797 Lohmar +53721 Siegburg +51503 Rösrath +51491 Overath +51645 Gummersbach +51545 Waldbröl +51588 Nümbrecht +51674 Wiehl +42855 Remscheid +42857 Remscheid +42859 Remscheid +42897 Remscheid +42899 Remscheid +42477 Radevormwald +42499 Hückeswagen +51515 Kürten +42929 Wermelskirchen +42853 Remscheid +51647 Gummersbach +51709 Marienheide +51643 Gummersbach +40545 Düsseldorf +40547 Düsseldorf +40219 Düsseldorf +40489 Düsseldorf +40629 Düsseldorf +40627 Düsseldorf +40625 Düsseldorf +40880 Ratingen +40882 Ratingen +40885 Ratingen +47269 Duisburg +45219 Essen +47055 Duisburg +45479 Mülheim an der Ruhr +45481 Mülheim an der Ruhr +45470 Mülheim an der Ruhr +40699 Erkrath +42579 Heiligenhaus +40472 Düsseldorf +40468 Düsseldorf +40477 Düsseldorf +40229 Düsseldorf +40231 Düsseldorf +40479 Düsseldorf +40211 Düsseldorf +40210 Düsseldorf +40212 Düsseldorf +40213 Düsseldorf +40215 Düsseldorf +40227 Düsseldorf +40233 Düsseldorf +40235 Düsseldorf +40237 Düsseldorf +40239 Düsseldorf +40217 Düsseldorf +40470 Düsseldorf +40476 Düsseldorf +40878 Ratingen +47249 Duisburg +47279 Duisburg +40883 Ratingen +42117 Wuppertal +42111 Wuppertal +42113 Wuppertal +42327 Wuppertal +42551 Velbert +42555 Velbert +42553 Velbert +45529 Hattingen +45257 Essen +45259 Essen +45239 Essen +45133 Essen +45149 Essen +40822 Mettmann +42489 Wülfrath +42329 Wuppertal +42115 Wuppertal +42105 Wuppertal +42109 Wuppertal +42549 Velbert +47053 Duisburg +47051 Duisburg +47059 Duisburg +46244 Bottrop +46539 Dinslaken +46049 Oberhausen +46119 Oberhausen +46117 Oberhausen +46147 Oberhausen +46149 Oberhausen +46145 Oberhausen +47169 Duisburg +47167 Duisburg +47119 Duisburg +47166 Duisburg +47137 Duisburg +47138 Duisburg +45478 Mülheim an der Ruhr +45473 Mülheim an der Ruhr +45359 Essen +46242 Bottrop +46240 Bottrop +46236 Bottrop +47057 Duisburg +47058 Duisburg +45476 Mülheim an der Ruhr +45475 Mülheim an der Ruhr +45472 Mülheim an der Ruhr +45468 Mülheim an der Ruhr +46047 Oberhausen +46045 Oberhausen +45357 Essen +46537 Dinslaken +45147 Essen +45145 Essen +45881 Gelsenkirchen +45896 Gelsenkirchen +45892 Gelsenkirchen +45699 Herten +45701 Herten +45894 Gelsenkirchen +45964 Gladbeck +45966 Gladbeck +45883 Gelsenkirchen +44653 Herne +44649 Herne +45897 Gelsenkirchen +44879 Bochum +45134 Essen +45138 Essen +45144 Essen +45139 Essen +45327 Essen +45279 Essen +45276 Essen +45329 Essen +45356 Essen +45141 Essen +44867 Bochum +44869 Bochum +45307 Essen +45886 Gelsenkirchen +45891 Gelsenkirchen +45136 Essen +45131 Essen +45130 Essen +45128 Essen +45355 Essen +45143 Essen +45127 Essen +45326 Essen +45277 Essen +45289 Essen +45309 Essen +45884 Gelsenkirchen Rotthausen +45879 Gelsenkirchen +44866 Bochum +46238 Bottrop +45968 Gladbeck +45899 Gelsenkirchen +45889 Gelsenkirchen +45888 Gelsenkirchen +42119 Wuppertal +42279 Wuppertal +42281 Wuppertal +42287 Wuppertal +42369 Wuppertal +42389 Wuppertal +42399 Wuppertal +45525 Hattingen +45527 Hattingen +58456 Witten +58256 Ennepetal +58332 Schwelm +45549 Sprockhövel +58300 Wetter (Ruhr) +58285 Gevelsberg +42103 Wuppertal +42107 Wuppertal +42283 Wuppertal +42285 Wuppertal +42275 Wuppertal +42277 Wuppertal +42289 Wuppertal +58119 Hagen +58093 Hagen +58089 Hagen +58135 Hagen +58099 Hagen +58091 Hagen +58339 Breckerfeld +58579 Schalksmühle +58239 Schwerte +58313 Herdecke +58097 Hagen +58095 Hagen +44359 Dortmund +44388 Dortmund +58453 Witten +45665 Recklinghausen +45659 Recklinghausen +45661 Recklinghausen +44579 Castrop-Rauxel +44577 Castrop-Rauxel +44581 Castrop-Rauxel +44894 Bochum +44628 Herne +44625 Herne +58455 Witten +44809 Bochum +44791 Bochum +44795 Bochum +44797 Bochum +44799 Bochum +44801 Bochum +44803 Bochum +45731 Waltrop +44793 Bochum +44807 Bochum +44787 Bochum +44789 Bochum +58452 Witten +44805 Bochum +44892 Bochum +44651 Herne +44652 Herne +44623 Herne +44629 Herne +45657 Recklinghausen +45663 Recklinghausen +44575 Castrop-Rauxel +44627 Herne +44369 Dortmund +44149 Dortmund +44227 Dortmund +44141 Dortmund +44339 Dortmund +44265 Dortmund +44287 Dortmund +44143 Dortmund +44379 Dortmund +44147 Dortmund +44309 Dortmund +44329 Dortmund +44269 Dortmund +44267 Dortmund +44229 Dortmund +44139 Dortmund +44532 Lünen +44536 Lünen +58454 Witten +44225 Dortmund +44137 Dortmund +44263 Dortmund +44357 Dortmund +44145 Dortmund +44135 Dortmund +44328 Dortmund +51580 Reichshof +51597 Morsbach +59757 Arnsberg +58708 Menden +57250 Netphen +57271 Hilchenbach +57319 Bad Berleburg +57368 Lennestadt +57399 Kirchhundem +57413 Finnentrop +57482 Wenden +57392 Schmallenberg +59872 Meschede +59889 Eslohe +58809 Neuenrade +58840 Plettenberg +59846 Sundern +59909 Bestwig +58540 Meinerzhagen +59192 Bergkamen +57080 Siegen +57078 Siegen +57072 Siegen +57223 Kreuztal +57258 Freudenberg +58515 Lüdenscheid +57489 Drolshagen +51702 Bergneustadt +58849 Herscheid +57439 Attendorn +57462 Olpe +57074 Siegen +57076 Siegen +57234 Wilnsdorf +57334 Bad Laasphe +57339 Erndtebrück +58509 Lüdenscheid +58513 Lüdenscheid +58640 Iserlohn +58644 Iserlohn +58642 Iserlohn +58636 Iserlohn +58675 Hemer +58791 Werdohl +58762 Altena +58769 Nachrodt-Wiblingwerde +58507 Lüdenscheid +58511 Lüdenscheid +58638 Iserlohn +58710 Menden +58802 Balve +44319 Dortmund +59199 Bönen +59427 Unna +59425 Unna +58730 Fröndenberg/Ruhr +59439 Holzwickede +59174 Kamen +44289 Dortmund +59423 Unna +58706 Menden +59069 Hamm +59759 Arnsberg +59755 Arnsberg +58739 Wickede (Ruhr) +59469 Ense +59457 Werl +59514 Welver +59823 Arnsberg +59821 Arnsberg +59581 Warstein +59519 Möhnesee +59494 Soest +59505 Bad Sassendorf +59602 Rüthen +59597 Erwitte +59609 Anröchte +33154 Salzkotten +33142 Büren +33165 Lichtenau +59964 Medebach +59955 Winterberg +34414 Warburg +34439 Willebadessen +59969 Bromskirchen, Hallenberg +59929 Brilon +59939 Olsberg +34431 Marsberg +59590 Geseke +33181 Wünnenberg +34434 Borgentreich +46284 Dorsten +46282 Dorsten +46348 Raesfeld +46286 Dorsten +48485 Neuenkirchen +48496 Hopsten +48268 Greven +48477 Hörstel +46325 Borken +46359 Heiden +45721 Haltern am See +59348 Lüdinghausen +59368 Werne +59387 Ascheberg +48249 Dülmen +48727 Billerbeck +48720 Rosendahl +48341 Altenberge +48624 Schöppingen +48739 Legden +48683 Ahaus +48703 Stadtlohn +48607 Ochtrup +48493 Wettringen +45772 Marl +45770 Marl +45768 Marl +48734 Reken +46354 Südlohn +46342 Velen +48712 Gescher +48653 Coesfeld +45711 Datteln +45739 Oer-Erkenschwick +59399 Olfen +44534 Lünen +59379 Selm +59394 Nordkirchen +48308 Senden +48301 Nottuln +48163 Münster +48161 Münster +48329 Havixbeck +48619 Heek +48599 Gronau +48565 Steinfurt +48366 Laer +48612 Horstmar +48629 Metelen +48432 Rheine +48282 Emsdetten +48356 Nordwalde +48431 Rheine +48429 Rheine +59075 Hamm +59077 Hamm +59067 Hamm +48317 Drensteinfurt +33790 Halle (Westfalen) +49479 Ibbenbüren +32351 Stemwede +49497 Mettingen +49504 Lotte +49525 Lengerich +59269 Beckum +59320 Ennigerloh +59510 Lippetal +48231 Warendorf +48291 Telgte +48346 Ostbevern +48351 Everswinkel +48324 Sendenhorst +59227 Ahlen +59229 Ahlen +59063 Hamm +59071 Hamm +59073 Hamm +59065 Hamm +48165 Münster +48159 Münster +48167 Münster +48157 Münster +48155 Münster +48147 Münster +48153 Münster +48151 Münster +48149 Münster +48143 Münster +48145 Münster +59302 Oelde +59329 Wadersloh +33449 Langenberg +33397 Rietberg +33378 Rheda-Wiedenbrück +59558 Lippstadt +59555 Lippstadt +59556 Lippstadt +59557 Lippstadt +33442 Herzebrock-Clarholz +33775 Versmold +33428 Harsewinkel +48336 Sassenberg +48361 Beelen +33649 Bielefeld +33332 Gütersloh +33334 Gütersloh +33803 Steinhagen +33330 Gütersloh +49545 Tecklenburg +48369 Saerbeck +49549 Ladbergen +49536 Lienen +49509 Recke +49477 Ibbenbüren +49492 Westerkappeln +33824 Werther (Westf.) +33829 Borgholzhausen +33129 Delbrück +33189 Schlangen +33161 Hövelhof +33758 Schloß Holte-Stukenbrock +32369 Rahden +32339 Espelkamp +32469 Petershagen +32657 Lemgo +32689 Kalletal +32683 Barntrup +33106 Paderborn +33102 Paderborn +33178 Borchen +33098 Paderborn +33104 Paderborn +33100 Paderborn +33175 Bad Lippspringe +33647 Bielefeld +33699 Bielefeld +33415 Verl +33615 Bielefeld +33659 Bielefeld +33689 Bielefeld +33335 Gütersloh +33619 Bielefeld +33617 Bielefeld +33609 Bielefeld +33719 Bielefeld +33607 Bielefeld +33333 Bertelsmann +33604 Bielefeld +33602 Bielefeld +33605 Bielefeld +33818 Leopoldshöhe +33813 Oerlinghausen +32108 Bad Salzuflen +32107 Bad Salzuflen +32758 Detmold +32760 Detmold +32791 Lage +32832 Augustdorf +33014 Bad Driburg +33184 Altenbeken +33039 Nieheim +32805 Horn-Bad Meinberg +32839 Steinheim +33034 Brakel +37696 Marienmünster +32756 Detmold +32825 Blomberg +32694 Dörentrup +31812 Bad Pyrmont +32676 Lügde +32816 Schieder-Schwalenberg +33739 Bielefeld +33729 Bielefeld +32051 Herford +32120 Hiddenhausen +32130 Enger +32139 Spenge +32257 Bünde +32278 Kirchlengern +32289 Rödinghausen +32609 Hüllhorst +33613 Bielefeld +33611 Bielefeld +32052 Herford +32049 Herford +32105 Bad Salzuflen +32547 Bad Oeynhausen +32549 Bad Oeynhausen +32545 Bad Oeynhausen +32584 Löhne +32602 Vlotho +32312 Lübbecke +32361 Preußisch Oldendorf +32429 Minden +32479 Hille +32457 Porta Westfalica +32699 Extertal +32423 Minden +32427 Minden +32425 Minden +37671 Höxter +37688 Beverungen +54294 Trier +54331 Pellingen +54518 Niersbach, Sehlem, Plein u.a. +54298 Welschbillig, Igel, Aach +54636 Rittersdorf u.a. +54675 Körperich u.a. +54439 Saarburg +54457 Wincheringen +54308 Langsur +54441 Ayl, Trassem u.a. +54453 Nittel +54456 Tawern +54316 Pluwig +54329 Konz +54451 Irsch +54450 Freudenburg +54455 Serrig +54314 Zerf +54429 Schillingen +54296 Trier +54317 Osburg, Gusterath, Farschweiler, Kasel u.a. +54459 Wiltingen +54332 Wasserliesch +54634 Bitburg +54310 Ralingen +54669 Bollendorf +54668 Ferschweiler +54646 Bettingen +54666 Irrel +54309 Newel +54292 Trier +54311 Trierweiler +54344 Kenn +54306 Kordel +54293 Trier +54338 Schweich +54313 Zemmer +54318 Mertesdorf +54664 Preist +54662 Speicher +54290 Trier +54295 Trier +66957 Vinningen, Trulben, Ruppertsweiler u.a. +76744 Wörth +66996 Fischbach, Erfweiler u.a. +76891 Bruchweiler-Bärenbach u.a. +76887 Bad Bergzabern u.a. +76889 Klingenmünster u.a. +76872 Steinweiler +76768 Berg +76870 Kandel +76779 Scheibenhardt +76777 Neupotz +76767 Hagenbach +76776 Neuburg am Rhein +76764 Rheinzabern +76751 Jockgrim +55758 Niederwörresbach +54426 Malborn +66482 Zweibrücken +66500 Hornbach +66989 Höheinöd, Petersberg u.a. +66509 Rieschweiler-Mühlbach +66497 Contwig +66506 Maßweiler +66919 Obernheim-Kirchenarnbach u.a. +66501 Kleinbundenbach +66954 Pirmasens +66484 Battweiler u.a. +66504 Bottenbach +66917 Wallhalben u.a. +66987 Thaleischweiler-Fröschen +66503 Dellfeld +66507 Reifenberg +66909 Herschweiler-Pettersheim +66903 Altenkirchen +66871 Pfeffelbach +66914 Waldmohr +66904 Brücken (Pfalz) +66916 Breitenbach +66901 Schönenberg-Kübelberg +66869 Kusel +66887 Rammelsbach u.a. +66882 Hütschenhausen +66892 Bruchmühlbach-Miesau +66877 Ramstein-Miesenbach +66894 Bechhofen +66907 Glan-Münchweiler +66879 Steinwenden u.a. +66851 Mittelbrunn, Queidersbach u.a. +66849 Landstuhl +54413 Gusenburg +54411 Deuselbach, Hermeskeil, Rorodt +54421 Reinsfeld +54427 Kell am See +55765 Birkenfeld u.a. +55767 Brücken, Oberbrombach u.a. +54422 Neuhütten +54340 Leiwen u.a. +54320 Waldrach +54346 Mehring +54341 Fell +54524 Klausen +54498 Piesport +54347 Neumagen-Dhron +54523 Hetzerath, Dierscheid, Heckenmünster +54528 Salmtal +54349 Trittenheim +54343 Föhren +56843 Irmenach +56841 Traben-Trarbach +54424 Thalfang +54470 Bernkastel-Kues u.a. +54484 Maring-Noviand +54487 Wintrich +54472 Monzelfeld, Hochscheid u.a. +54486 Mülheim (Mosel) +54497 Morbach +55777 Berschweiler bei Baumholder +55779 Heimbach +55768 Hoppstädten-Weiersbach +55743 Idar-Oberstein +55774 Baumholder +55776 Berglangenbach, Ruschberg u.a. +66885 Altenglan +67749 Offenbach-Hundheim +67746 Langweiler +67756 Hinzweiler +67754 Eßweiler +67744 Medard, Rathskirchen u.a. +67742 Lauterecken u.a. +55606 Kirn +55608 Bergen +67745 Grumbach +55621 Hundsbach +55491 Büchenbeuren +55481 Kirchberg u.a. +55483 Dickenschied u.a. +55487 Sohren +55624 Rhaunen +55756 Herrstein +54483 Kleinich +55627 Merxheim +55619 Hennweiler +55618 Simmertal +55629 Seesbach +55566 Sobernheim +55499 Riesweiler +55490 Gemünden +55471 Tiefenbach u.a. +55626 Bundenbach +76848 Wilgartswiesen +66953 Pirmasens +66976 Rodalben +66955 Pirmasens +66981 Münchweiler an der Rodalb +66969 Lemberg +66978 Clausen +66994 Dahn +66999 Hinterweidenthal +67716 Heltersberg +67714 Waldfischbach-Burgalben +67657 Kaiserslautern +67663 Kaiserslautern +67304 Eisenberg (Pfalz) +67307 Göllheim +67098 Bad Dürkheim +67487 Maikammer +67435 Neustadt an der Weinstraße +55234 Albig +55270 Ober-Olm +67808 Steinbach, Weitersweiler, Bennhausen, Mörsfeld, Würzweiler, Ruppertsecken u.a. +76857 Albersweiler, Silz u.a. +76846 Hauenstein +76855 Annweiler am Trifels +76835 Rhodt u.a. +67480 Edenkoben +66862 Kindsbach +67705 Trippstadt u.a. +67655 Kaiserslautern +67688 Rodenbach +67685 Weilerbach u.a. +67731 Otterbach, Otterberg +67661 Kaiserslautern +67659 Kaiserslautern +67734 Katzweiler +67697 Otterberg +67735 Mehlbach +67706 Krickenbach +67707 Schopp +67718 Schmalenberg +67715 Geiselberg +67686 Mackenbach +67681 Sembach +67678 Mehlingen +67693 Waldleiningen, Fischbach +67305 Ramsen +67471 Elmstein +67319 Wattenheim +67475 Weidenthal +67468 Frankenstein, Neidenfels, Frankeneck +67691 Hochspeyer +67677 Enkenbach-Alsenborn +67680 Neuhemsbach +76863 Herxheim +76829 Landau in der Pfalz +76877 Offenbach an der Queich +76879 Hochstadt +76833 Frankweiler +76865 Insheim +76831 Billigheim-Ingenheim, Birkweiler +67483 Edesheim +67489 Kirrweiler (Pfalz) +67482 Venningen +67377 Gommersheim +76771 Hördt +76761 Rülzheim +76770 Hatzenbühl +76726 Germersheim +76774 Leimersheim +76756 Bellheim +76773 Kuhardt +67360 Lingenfeld +67363 Lustadt +67366 Weingarten (Pfalz) +67354 Römerberg +67373 Dudenhofen +67346 Speyer +67376 Harthausen +67378 Zeiskam +67361 Freisbach +67368 Westheim (Pfalz) +67365 Schwegenheim +67466 Lambrecht (Pfalz) +67273 Weisenheim am Berg +67472 Esthal +67317 Altleiningen +67256 Weisenheim am Sand +67146 Deidesheim +67157 Wachenheim an der Weinstraße +67271 Kindenheim +67434 Neustadt an der Weinstraße +67433 Neustadt an der Weinstraße +67473 Lindenberg +67152 Ruppertsberg +67316 Carlsberg +67147 Forst an der Weinstraße +67169 Kallstadt +67149 Meckenheim +67158 Ellerstadt +67161 Gönnheim +67136 Fußgönheim +67071 Ludwigshafen am Rhein +67105 Schifferstadt +67459 Böhl-Iggelheim +67227 Frankenthal (Pfalz) +67125 Dannstadt-Schauernheim +67065 Ludwigshafen am Rhein +67112 Mutterstadt +67117 Limburgerhof +67245 Lambsheim +67126 Hochdorf-Assenheim +67067 Ludwigshafen am Rhein +67374 Hanhofen +67069 Ludwigshafen am Rhein +67063 Ludwigshafen am Rhein +67454 Haßloch +67150 Niederkirchen bei Deidesheim +67159 Friedelsheim +67127 Rödersheim-Gronau +67167 Erpolzheim +67251 Freinsheim +67133 Maxdorf +67134 Birkenheide +67699 Schneckenhausen +67737 Olsbrücken +67759 Nußbach +67827 Becherbach +67823 Obermoschel, Schiersfeld +67724 Gundersweiler, Gonbach u.a. +67752 Wolfstein +67806 Rockenhausen, Bisterschied u.a. +67829 Callbach +67753 Rothselberg u.a. +67748 Odenbach +55590 Meisenheim +55592 Rehborn +67822 Winterborn, Waldgrehweiler, Niedermoschel u.a. +67700 Niederkirchen +67757 Kreimbach-Kaulbach +67732 Hirschhorn +67701 Schallodenbach +67728 Münchweiler an der Alsenz +67819 Kriegsfeld +67729 Sippersfeld +67725 Börrstadt +67295 Bolanden +67814 Dannenfels +67722 Winnweiler +67292 Kirchheimbolanden +67811 Dielkirchen +67294 Bischheim u.a. +67821 Alsenz +67813 Schwarzengraben, St. Alban, Gerbach +67817 Imsbach +67727 Lohnsfeld +67824 Feilbingert +55595 Hargesheim +55452 Guldental +55596 Waldböckelheim +55496 Argenthal +55569 Monzingen +55585 Norheim u.a. +55571 Odernheim am Glan +55442 Stromberg +55568 Staudernheim +55444 Seibersbach +67826 Hallgarten +55459 Aspisheim, Grolsheim +55411 Bingen am Rhein +55437 Ockenheim +55599 Gau-Bickelheim +55457 Gensingen +55576 Sprendlingen +55597 Wöllstein +55545 Bad Kreuznach +55546 Hackenheim +55450 Langenlonsheim +55583 Bad Kreuznach +55559 Bretzenheim +55543 Bad Kreuznach +55593 Rüdesheim +67297 Marnheim +67308 Albisheim (Pfrimm) +67310 Hettenleidelheim +67269 Grünstadt +67283 Obrigheim (Pfalz) +67278 Bockenheim an der Weinstraße +67280 Ebertsheim +67592 Flörsheim-Dalsheim +67590 Monsheim +67598 Gundersheim +67591 Offstein +67816 Dreisen, Standenbühl +67311 Tiefenthal +67281 Kirchheim an der Weinstraße +67246 Dirmstein +67229 Gerolsheim +67574 Osthofen +67599 Gundheim +67550 Worms +67258 Heßheim +67240 Bobenheim-Roxheim +67551 Worms +67259 Beindersheim +67547 Worms +67593 Westhofen, Bermersheim +67595 Bechtheim +67549 Worms +55286 Wörrstadt +55435 Gau-Algesheim +55291 Saulheim +55239 Gau-Odernheim +55578 Wallertheim +55288 Armsheim +55271 Stadecken-Elsheim +55232 Alzey +55268 Nieder-Olm +55237 Flonheim +67586 Hillesheim +67596 Dittelsheim-Heßloch +67575 Eich +67583 Guntersblum +67577 Alsheim +67585 Dorn-Dürkheim +67578 Gimbsheim +55129 Mainz Ebersheim, Hechtsheim +55296 Harxheim +55276 Oppenheim +55283 Nierstein +55299 Nackenheim +55294 Bodenheim +55278 Mommenheim +67587 Wintersheim +67582 Mettenheim +67166 Otterstadt +67141 Neuhofen +67165 Waldsee +67122 Altrip +67059 Ludwigshafen am Rhein +67061 Ludwigshafen am Rhein +67580 Hamm +54531 Manderscheid +54533 Bettenfeld, Niederöfflingen u.a. +54570 Pelm, Neroth u.a. +54597 Pronsfeld +54619 Üttfeld +54617 Lützkampen +54673 Neuerburg u.a. +54689 Daleiden, Preischeid u.a. +54687 Arzfeld +54608 Bleialf +54616 Winterspelt +54614 Schönecken +54612 Lasel +54649 Waxweiler +54526 Landscheid +54647 Dudeldorf +54655 Kyllburg +54657 Badem, Gindorf, Neidenbach +54529 Spangdahlem +54611 Hallschlag +54595 Prüm +54589 Stadtkyll +54574 Birresborn +54584 Jünkerath +54579 Üxheim +54585 Esch +54576 Hillesheim +54568 Gerolstein +54610 Büdesheim +54578 Walsdorf, Nohn u.a. +54587 Lissendorf +54586 Schüller +54516 Wittlich +54534 Großlittgen +54558 Gillenfeld +54552 Mehren u.a. +56288 Kastellaun +56290 Beltheim +56858 Peterswald-Löffelscheid u.a. +53506 Ahrbrück, Heckenbach, Hönningen, Kesseling, Rech +56825 Gevenich +56861 Reil +56856 Zell (Mosel) +56812 Cochem +56864 Bad Bertrich +56859 Bullay, Alf, Zell +56867 Briedel +56826 Lutzerath +56850 Enkirch u.a. +56862 Pünderich +56814 Ediger-Eller +54492 Zeltingen-Rachtig, Erden, Lösnich u.a. +54536 Kröv +54538 Bausendorf +54539 Ürzig +56767 Uersfeld +54550 Daun +53520 Reifferscheid, Kaltenborn, Wershofen u.a. +53539 Bodenbach, Kelberg, Kirsbach u.a. +53518 Adenau, Kottenborn u.a. +53534 Barweiler, Bauler, Hoffeld, Pomster, Wiesemscheid, Wirft +56727 Mayen +56759 Kaisersesch +56729 Ettringen +56761 Düngenheim +56769 Retterath +56766 Ulmen +56828 Alflen +56823 Büchel +56253 Treis-Karden +56865 Blankenrath u.a. +56820 Senheim +56869 Mastershausen +56821 Ellenz-Poltersdorf +56281 Emmelshausen +56291 Leiningen +56283 Wildenbungert, Gondershausen, Nörtershausen u.a. +55469 Simmern/Hunsrück u.a. +56294 Münstermaifeld +56751 Polch +56753 Mertloch, Welling u.a. +56754 Binningen +56743 Mendig +56818 Klotten +56829 Pommern +56254 Müden +56736 Kottenheim +56299 Ochtendung +56073 Koblenz +56072 Koblenz +56075 Koblenz +56295 Lonnig +56220 Urmitz +56332 Lehmen, Niederfell, Oberfell, Wolken u.a. +56330 Kobern-Gondorf +56154 Boppard +56323 Waldesch, Hünenfeld +56333 Winningen +53533 Antweiler, Aremberg, Dorsel, Eichenbach, Aremberg, Fuchshofen und Müsch +53505 Altenahr, Berg, Kalenborn, Kirchsahr +56651 Niederzissen +56745 Bell +56746 Kempenich +53426 Schalkenbach, Königsfeld, Dedenbach +53501 Grafschaft +53474 Bad Neuenahr-Ahrweiler +53507 Dernau +53508 Mayschoß +56598 Rheinbrohl +56653 Wehr +53547 Breitscheid, Dattenberg, Hausen, Hümmerich, Kasbach-Ohlenberg, Roßbach u.a. +53545 Linz am Rhein, Ockenfels +53498 Bad Breisig, Waldorf, Gönnersdorf +53424 Remagen +53489 Sinzig +53557 Bad Hönningen +56656 Brohl-Lützing +56659 Burgbrohl +56626 Andernach +56645 Nickenich +56642 Kruft +56630 Kretz +56070 Koblenz +56317 Urbach +56307 Dürrholz +56587 Straßenhaus +56564 Neuwied +56566 Neuwied +56589 Niederbreitbach +56567 Neuwied +56581 Melsbach +56584 Anhausen +56579 Rengsdorf +56588 Waldbreitbach, Hasuen +56599 Leutesdorf +56637 Plaidt +56218 Mülheim-Kärlich +56575 Weißenthurm +56648 Saffig +53560 Vettelschloß, Kretzhaus (Linz am Rhein) +53572 Bruchhausen, Unkel +53562 Sankt Katharinen (Landkreis Neuwied) +53578 Windhagen +53579 Erpel +53619 Rheinbreitbach +57635 Weyerbusch +57632 Flammersfeld +57641 Oberlahr +56305 Puderbach +56593 Horhausen (Westerwald) +53567 Asbach, Buchholz +53577 Neustadt (Wied) +56594 Willroth +56348 Bornich, Patersberg +56349 Kaub +56329 Sankt Goar +56346 Sankt Goarshausen u.a. +55422 Bacharach, Breitscheid +55413 Weiler bei Bingen +55494 Rheinböllen +55430 Oberwesel +55432 Niederburg +55497 Ellern (Hunsrück), Schnorbach +56379 Singhofen +56357 Miehlen u.a. +56412 Nentershausen, Hübingen, Niederelbert u.a. +55425 Waldalgesheim +65391 Lorch +55424 Münster-Sarmsheim +56077 Koblenz +56076 Koblenz +56132 Bad Ems Umland +56130 Bad Ems +56340 Osterspai +56355 Nastätten u.a. +56321 Rhens +56341 Kamp-Bornhofen-Filsen +56377 Nassau +56112 Lahnstein +56338 Braubach +56322 Spay +56068 Koblenz +56133 Fachbach, Exklave Lahnstein +56368 Katzenelnbogen +56370 Schönborn +65558 Burgschwalbach +65582 Diez, Hambach, Aull +55127 Mainz +55126 Mainz +55218 Ingelheim am Rhein +55262 Heidesheim am Rhein +55263 Wackernheim +55257 Budenheim +55120 Mainz +55122 Mainz +55131 Mainz +55128 Mainz +55130 Mainz +55124 Mainz +55118 Mainz +55116 Mainz +65629 Niederneisen +65623 Hahnstätten u.a. +65626 Birlenbach +56179 Vallendar +56206 Hilgert +56276 Großmaischeid +56237 Nauort +56235 Ransbach-Baumbach +56244 Freilingen, Freirachdorf u.a. +56242 Selters (Westerwald) +56271 Kleinmaischeid +56269 Dierdorf +56337 Eitelborn +56316 Raubach +56424 Mogendorf, Ebernhahn, Staudt u.a. +56249 Herschbach +56170 Bendorf +56191 Weitersburg +56410 Montabaur +56428 Dernbach (Westerwald) +56203 Höhr-Grenzhausen +56204 Hillscheid +56422 Wirges, Stadt +56182 Urbar (bei Koblenz) +56335 Neuhäusel +56427 Siershahn +56459 Bellingen, Kölbingen, Gemünden u.a. +56414 Meudt, Molsberg, Hundsangen, Niederahr u.a. +56457 Westerburg +65624 Altendiez +57612 Birnbach +57636 Mammelzen +57644 Hattert +57614 Steimel +57627 Hachenburg +57639 Oberdreis +57629 Malberg, Norken, Höchstenbach u.a. +57610 Altenkirchen (Westerwald) +57539 Fürthen +57589 Pracht +57537 Wissen, Hövels u.a. +57638 Neitersen +57577 Hamm (Sieg) +57647 Nistertal, Enspel +57648 Unnau +57562 Herdorf +57520 Emmerzhausen, Niederdreisbach, Steinebach +57567 Daaden +57583 Nauroth +57518 Betzdorf +56472 Nisterau u.a. +56462 Höhn +56470 Bad Marienberg (Westerwald) +57578 Elkenroth +57586 Weitefeld +57642 Alpenrod +57645 Nister +57580 Gebhardshain +56479 Oberrod u.a. +56477 Rennerod, Zehnhausen, Nister-Möhrendorf, Waigandshain +57587 Birken-Honigsessen +51598 Friesenhagen +57548 Kirchen (Sieg) +57572 Niederfischbach +57555 Mudersbach +57080 Siegen +57581 Katzwinkel (Sieg) +57584 Scheuerfeld +27498 Helgoland +25946 Amrum +25997 Hörnum (Sylt) +25980 Sylt +25996 Wenningstedt-Braderup (Sylt) +25999 Kampen (Sylt) +25767 Albersdorf +25704 Meldorf +25718 Friedrichskoog +25761 Büsum +25724 Neufeld, Schmedeswurth +25572 Sankt Margarethen +25541 Brunsbüttel +25693 Sankt Michaelisdonn,Gudendorf,Volsemenhusen,Trennewurth +25709 Kronprinzenkoog, Marne u.a. +25719 Barlt, Busenwurth +25715 Eddelak, Averlak, Dingen, Ramhusen +25785 Nordhastedt +25727 Süderhastedt +25712 Burg (Dithmarschen) +25729 Windbergen +24576 Bad Bramstedt +24568 Kaltenkirchen +23812 Wahlstedt +25548 Kellinghusen +25563 Wrist +25524 Itzehoe +22880 Wedel +25451 Quickborn +25377 Kollmar, Pagensand +25492 Heist +25436 Uetersen +25489 Haseldorf +25491 Hetlingen +25370 Seester +25371 Seestermühe +22113 Hamburg, Oststeinbek +25488 Holm +25482 Appen +25474 Ellerbek +25494 Borstel-Hohenraden +25499 Tangstedt +22869 Schenefeld +25421 Pinneberg +25462 Rellingen +25469 Halstenbek +25497 Prisdorf +25495 Kummerfeld +22844 Norderstedt +22846 Norderstedt +22848 Norderstedt +22851 Norderstedt +22457 Hamburg +22889 Tangstedt +22850 Norderstedt +25554 Wilster +25376 Borsfleth +25569 Kremperheide +25348 Glückstadt +25573 Beidenfleth, Klein Kampen +25576 Brokdorf +25599 Wewelsfleth +25597 Westermoor +25578 Dägeling, Neuenbrook +25361 Krempe, Grevenkop, Süderau, Muchelndorf +25358 Horst +25566 Lägerdorf +25364 Brande-Hörnerkirchen +25336 Elmshorn +25335 Elmshorn +25379 Herzhorn, Kamerlanderdeich +25368 Kiebitzreihe +25587 Münsterdorf +25588 Oldendorf +25582 Hohenaspe +25596 Wacken +25560 Schenefeld +25594 Vaale +25584 Holstenniendorf +25557 Hanerau-Hademarschen, Seefeld u.a. +25721 Eggstedt +25725 Schafstedt, Weidenhof, Bornholt +24594 Hohenwestedt +24819 Todenbüttel +25593 Reher +25551 Hohenlockstedt +25575 Beringstedt +25585 Lütjenwestedt, Tackesdorf +25591 Ottenbüttel +25590 Osterstedt +24632 Lentföhrden +25355 Barmstedt +25485 Hemdingen +25337 Elmshorn +25373 Ellerhoop +25365 Klein Offenseth-Sparrieshoop +24643 Struvenhütten +24558 Henstedt-Ulzburg +24641 Sievershütten +24640 Schmalfeld +24629 Kisdorf +24628 Hartenholm +25479 Ellerau +25486 Alveslohe +24649 Wiemersdorf +24622 Gnutz +24634 Padenstedt +24647 Wasbek +24613 Aukrug, Wiedenborstel +24644 Timmaspe +24616 Brokstedt +25579 Fitzbek +25581 Hennstedt +24598 Boostedt +24623 Großenaspe +24625 Großharrie +24539 Neumünster +24537 Neumünster +24620 Bönebüttel +24626 Groß Kummerfeld +24536 Neumünster +24534 Neumünster +25826 Sankt Peter-Ording +25923 Süderlügum, Braderup u.a. +24980 Schafflund, Meyn u.a. +25842 Langenhorn, Ockholm u.a. +25836 Garding, Osterhever, Poppenbüll u.a. +25845 Nordstrand, Elisabeth-Sophien-Koog, Südfall +25881 Tating, Westerhever, Tümlauer Koog +25849 Süderoog, Pellworm +25859 Hallig Hooge +25882 Tetenbüll +25870 Oldenswort +25832 Tönning +25774 Lunden +25792 Neuenkirchen +25764 Wesselburen +25746 Heide u.a. +25776 Sankt Annen, Rehm-Flehde-Bargen +25797 Wöhrden +25795 Weddingstedt +25779 Hennstedt +25788 Delve +25878 Drage, Seeth +25791 Linden, Barkenholm +25782 Tellingstedt +25770 Hemmingstedt +25879 Stapel +25813 Husum, Schwesing u.a. +25856 Hattstedt u.a. +25889 Uelvesbüll, Witzwort +25872 Ostenfeld +25873 Rantrum +25887 Winnert +25850 Behrendorf, Bondelum +25860 Horstedt +25885 Wester-Ohrstedt +25840 Friedrichstadt, Koldenbüttel u.a. +25876 Schwabstedt +25866 Mildstedt +25938 Föhr +25863 Langeneß +25920 Risum-Lindholm, Stedesand +25899 Niebüll +25869 Habel, Gröde +25867 Oland +25924 Rodenäs +25927 Neukirchen, Aventoft +25917 Leck +25852 Bordelum +25853 Ahrenshöft +25821 Bredstedt, Breklum u.a. +24992 Jörl +24969 Großenwiehe, Lindewitt +25884 Viöl +25862 Joldelund +25855 Haselund +25858 Högel +25864 Löwenstedt +25926 Ladelund +24994 Medelby +24799 Meggerdorf, Friedrichsholm, Friedrichsgraben u.a. +24803 Erfde +25794 Pahlen +25799 Wrohm +25786 Dellstedt +24805 Hamdorf +24806 Hohn +24800 Elsdorf-Westermühlen +24369 Waabs +24351 Damp +24848 Kropp u.a. +24855 Bollingstedt, Jübek +24983 Handewitt +24817 Tetenhusen +24881 Nübel +24808 Jevenstedt +24816 Hamweddel +24784 Westerrönfeld +24797 Breiholz, Tackesdorf-Nord +24813 Schülp bei Rendsburg +24809 Nübbel +24787 Fockbek +24782 Büdelsdorf, Rickert +24768 Rendsburg +24850 Schuby +24872 Groß Rheide +24861 Bergenhusen +24863 Börm +24896 Treia, Ahrenviölfeld +24899 Wohlde +24867 Dannewerk +24869 Dörpstedt +24870 Ellingstedt +24876 Hollingstedt +24887 Silberstedt, Schwittschau +24791 Alt Duvenstedt +24811 Owschlag u.a. +24357 Fleckeby u.a. +24884 Selk, Geltdorf, Hahnekrug +24857 Fahrdorf +24878 Jagel, Lottorf +24837 Schleswig +24866 Busdorf +24882 Schaalby, Geelbek +24259 Westensee +24793 Bargstedt, Brammer, Oldenbüttel +24783 Osterrönfeld +24589 Nortorf +24646 Warder +24802 Emkendorf +24794 Borgstedt +24796 Bredenbek +24790 Schacht-Audorf +24220 Flintbek +24582 Bordesholm +24241 Blumenthal +24247 Mielkendorf +24254 Rumohr +24242 Felde +24239 Achterwehr +24631 Langwedel +24119 Kronshagen +24107 Kiel +24113 Kiel +24109 Melsdorf +24111 Kiel Russee +24363 Holtsee +24814 Sehestedt +24358 Ascheffel +24361 Groß Wittensee +24354 Kosel, Rieseby u.a. +24360 Barkelsby +24366 Loose +24364 Holzdorf +24340 Eckernförde +24864 Brodersby, Goltoft +24897 Ulsnis +24367 Osterby +24161 Altenholz +24251 Osdorf +24244 Felm +24229 Dänischenhagen +24214 Gettorf u.a. +24852 Eggebek, Langstedt, Sollerup, Süderhackstedt +24963 Tarp +24941 Flensburg +24976 Handewitt +24988 Oeversee +24997 Wanderup +24885 Sieverstedt +24991 Großsolt +24975 Husby +24986 Mittelangeln +24891 Struxdorf, Schnarup-Thumby +24860 Böklund u.a. +24890 Stolk +24894 Tolk, Twedt +24879 Idstedt +24943 Flensburg, Tastrup +24966 Sörup +24873 Havetoft +24937 Flensburg +24955 Harrislee +24939 Flensburg +24960 Glücksburg, Munkbrarup +24977 Langballig +24944 Flensburg +24989 Dollerup +24999 Wees +24888 Steinfeld +24405 Mohrkirch, Rügge +24392 Süderbrarup +24407 Rabenkirchen-Faulück +24395 Gelting +24996 Sterup +24972 Steinberg, Steinbergkirche +24893 Taarstedt +24409 Stoltebüll +24401 Böel +24402 Esgrus +24398 Dörphof +24376 Kappeln +24404 Maasholm, Schleimünde +24399 Arnis, Marienhof +25992 List +21039 Hamburg +21465 Reinbek +23701 Eutin, Süsel +23623 Ahrensbök +23845 Seth +21521 Aumühle +21514 Bröthen +23883 Sterley +23911 Ziethen +22946 Trittau +22941 Bargteheide, Delingsdorf u.a. +23617 Stockelsdorf +21502 Geesthacht +21481 Lauenburg/Elbe +21483 Gülzow +21493 Fuhlenhagen +21524 Brunstorf +21529 Kröppelshagen-Fahrendorf +21526 Hohenhorn +21527 Kollow +22145 Hamburg +21509 Glinde +22926 Ahrensburg +22949 Ammersbek +22962 Siek +22885 Barsbüttel +22927 Großhansdorf +23847 Lasbek +23898 Sandesneben u.a. +22929 Hamfelde, Kasseburg, Köthel, Rausdorf, Schönberg +22955 Hoisdorf +22961 Hoisdorf +22952 Lütjensee +22964 Steinburg +22965 Todendorf +22956 Grönwohld +22958 Kuddewörde +22969 Witzhave +22959 Linau +21516 Woltersdorf, Müssen u.a. +23896 Nusse +23909 Ratzeburg +23899 Gudow +23881 Breitenfelde, Lankau +23919 Berkenthin +23879 Mölln +23826 Bark +23795 Bad Segeberg +23829 Wittenborn +23863 Bargfeld-Stegen +23843 Bad Oldesloe +23816 Leezen +23866 Nahe +23867 Sülfeld +23869 Elmenhorst +22967 Tremsbüttel +23815 Geschendorf +23858 Reinfeld (Holstein) +23619 Zarpen +23818 Neuengörs +23820 Pronstorf +24326 Ascheberg +24638 Schmalensee +24601 Wankendorf +24619 Börnhöved +24610 Trappenkamp +24635 Rickling +24637 Schillsdorf +24306 Plön +23813 Nehms +23824 Tensfeld +23827 Wensin +23715 Bosau +23719 Glasau +23821 Rohlstorf +23823 Seedorf +23628 Krummesse, Klempau +23627 Groß Grönau +23860 Klein Wesenberg +23611 Bad Schwartau +23568 Lübeck Schlutup/St. Gertrud +23556 Lübeck +23560 Lübeck +23562 Lübeck +23564 Lübeck +23558 Lübeck +23552 Lübeck +23554 Lübeck St. Lorenz Nord +23566 Lübeck +23569 Lübeck +23570 Lübeck +23684 Scharbeutz, Süsel +23689 Pansdorf +23717 Kasseedorf +23629 Sarkwitz +23730 Neustadt +23683 Scharbeutz +23626 Ratekau +23669 Timmendorfer Strand +24223 Schwentinetal +24211 Preetz +24232 Schönkirchen +24245 Kirchbarkau +24250 Nettelsee +24149 Kiel +24145 Kiel +24146 Kiel +23758 Oldenburg in Holstein +24257 Hohenfelde +24321 Lütjenburg +23738 Lensahn +23714 Malente, Kirchnüchel +23743 Grömitz +24116 Kiel +24118 Kiel +24114 Kiel +24105 Kiel +24103 Kiel +24147 Kiel +24143 Kiel +24148 Kiel +24222 Schwentinental +24329 Grebin +24256 Fargau-Pratjau +24238 Selent +24106 Kiel +24253 Probsteierhagen +24235 Laboe +24226 Heikendorf +24159 Kiel +24248 Mönkeberg +24217 Schönberg (Holstein) +24327 Blekendorf +23744 Schönwalde am Bungsberg +23777 Heringsdorf +23749 Grube +23747 Dahme +23769 Fehmarn +23779 Neukirchen +23746 Kellenhusen +23775 Großenbrode +23774 Heiligenhafen +66740 Saarlouis +66798 Wallerfangen +66802 Überherrn +66706 Perl +66693 Mettlach +66679 Losheim +66701 Beckingen +66663 Merzig +66780 Rehlingen-Siersburg +66763 Dillingen/Saar +66359 Bous +66115 Saarbrücken +66126 Saarbrücken +66127 Saarbrücken +66787 Wadgassen +66333 Völklingen +66352 Großrosseln +66806 Ensdorf +66773 Schwalbach +66346 Püttlingen +66292 Riegelsberg +66128 Saarbrücken +66583 Spiesen-Elversberg +66271 Kleinblittersdorf +66121 Saarbrücken +66123 Saarbrücken +66131 Saarbrücken +66119 Saarbrücken +66113 Saarbrücken +66130 Saarbrücken +66133 Saarbrücken +66287 Quierschied +66280 Sulzbach/Saar +66386 Sankt Ingbert +66399 Mandelbachtal +66129 Saarbrücken +66117 Saarbrücken +66111 Saarbrücken +66125 Saarbrücken +66132 Saarbrücken +66571 Eppelborn +66265 Heusweiler +66793 Saarwellingen +66822 Lebach +66809 Nalbach +66839 Schmelz +66636 Tholey +66709 Weiskirchen +66687 Wadern +66578 Schiffweiler +66564 Ottweiler +66589 Merchweiler +66540 Neunkirchen +66557 Illingen +66646 Marpingen +66606 Sankt Wendel +66640 Namborn +66649 Oberthal +66299 Friedrichsthal +66539 Neunkirchen +66453 Gersheim +66440 Blieskastel +66459 Kirkel +66424 Homburg +66538 Neunkirchen +66450 Bexbach +66629 Freisen +66620 Nonnweiler +66625 Nohfelden +07952 Pausa-Mühltroff +08606 Oelsnitz +08626 Adorf +08261 Schöneck/Vogtl. +08267 Klingenthal +08645 Bad Elster +08648 Bad Brambach +08258 Markneukirchen +08248 Klingenthal/Sa. +07919 Kirschkau, Pausa-Mühltroff +08538 Weischlitz u.a. +08539 Rosenbach +08543 Pöhl +08547 Jößnitz +08548 Rosenbach +08527 Plauen, Rößnitz +08525 Plauen +08523 Plauen +08529 Plauen +07985 Elsterberg +08541 Neuensalz +08209 Auerbach/Vogtl. +08228 Rodewisch +08236 Ellefeld +08223 Falkenstein/Vogtl. +08233 Treuen +08239 Bergen +08485 Lengenfeld +08147 Crinitzberg +08107 Kirchberg +08262 Muldenhammer +08237 Steinberg +08304 Schönheide +08328 Stützengrün +08309 Eibenstock +08428 Langenbernsdorf +08427 Fraureuth +08412 Werdau +08459 Neukirchen/Pleiße +08468 Reichenbach/Vogtl. +08496 Neumark +08491 Netzschkau, Limbach +08499 Mylau +08115 Lichtentanne +08371 Glauchau +08112 Wilkau-Haßlau +08058 Zwickau +08060 Zwickau +08144 Hirschfeld +08056 Zwickau +08134 Langenweißbach, Wildenfels +08141 Reinsdorf +08132 Mülsen +08066 Zwickau +08289 Schneeberg +08064 Zwickau +08062 Zwickau +09619 Dorfchemnitz, Mulda, Sayda +08344 Grünhain-Beierfeld +08321 Zschorlau +08359 Breitenbrunn/Erzgeb. +08340 Schwarzenberg/Erzgeb. +08349 Johanngeorgenstadt +08315 Lauter-Bernsbach +08324 Bockau +09487 Schlettau +09481 Scheibenberg +09484 Oberwiesenthal +09456 Annaberg-Buchholz, Mildenau +09465 Sehma +09474 Crottendorf +08352 Raschau +08118 Hartenstein +09337 Callenberg, Hohenstein-Ernstthal, Bernsdorf +09356 St. Egidien +09387 Jahnsdorf +09385 Lugau/Erzgeb. +09366 Stollberg/Erzgeb. +09399 Niederwürschnitz +09376 Oelsnitz/Erzgebirge +08297 Zwönitz +08294 Lößnitz +08301 Schlema +08280 Aue +09350 Lichtenstein +09355 Gersdorf +09394 Hohndorf +09123 Chemnitz +09439 Amtsberg +09488 Wiesa +09235 Burkhardtsdorf +09468 Geyer +09427 Ehrenfriedersdorf +09419 Thum +09423 Gelenau/Erzgeb. +09392 Auerbach +09221 Neukirchen/Erzgeb. +09430 Drebach +09380 Thalheim/Erzgebirge +09390 Gornsdorf +09125 Chemnitz +09477 Jöhstadt +09496 Marienberg +09471 Bärenstein +09573 Leubsdorf, Gornau, Augustusburg +09575 Eppendorf +09432 Großolbersdorf +09429 Wolkenstein +09437 Börnichen, Gornau +09514 Pockau-Lengefeld (Lengefeld) +09579 Grünhainichen +09509 Pockau-Lengefeld (Pockau) +09518 Großrückerswalde +09405 Zschopau, Gornau +09434 Zschopau +09618 Brand-Erbisdorf, Großhartmannsdorf +09548 Seiffen/Erzgeb. +09526 Olbernhau, Pfaffroda, Heidersdorf +04654 Frohburg +04651 Bad Lausick +04668 Grimma +04509 Delitzsch, Krostitz u.a. +04838 Eilenburg u.a. +08451 Crimmitschau +08393 Meerane +08373 Remse +08396 Waldenburg +04613 Lucka +04539 Groitzsch +04565 Regis-Breitingen +04564 Böhlen +04575 Neukieritzsch, Deutzen +04523 Pegau, Elstertrebnitz +04442 Zwenkau +04552 Borna +04571 Rötha +04567 Kitzscher +04420 Markranstädt +04416 Markkleeberg +04129 Leipzig +04159 Leipzig +04179 Leipzig +04249 Leipzig +04277 Leipzig +04207 Leipzig +04105 Leipzig +04178 Leipzig +04435 Schkeuditz +04158 Leipzig +04205 Leipzig +04229 Leipzig +04209 Leipzig +04177 Leipzig +04109 Leipzig +04107 Leipzig +04275 Leipzig +04157 Leipzig +04155 Leipzig +04821 Brandis +04451 Borsdorf +04827 Machern +04463 Großpösna +04683 Naunhof +04318 Leipzig +04356 Leipzig +04288 Leipzig +04103 Leipzig +04319 Leipzig +04328 Leipzig +04349 Leipzig +04347 Leipzig +04289 Leipzig +04425 Taucha +04279 Leipzig +04317 Leipzig +04299 Leipzig +04315 Leipzig +04316 Leipzig +04357 Leipzig +04329 Leipzig +04824 Brandis +04519 Rackwitz +04849 Bad Düben +09212 Limbach-Oberfrohna +09224 Chemnitz +09328 Lunzenau +09217 Burgstädt +09322 Penig +09232 Hartmannsdorf +09241 Mühlau +09243 Niederfrohna +09353 Oberlungwitz +09600 Weißenborn, Oberschöna +09661 Hainichen, Rossau, Striegistal +09306 Rochlitz +09669 Frankenberg +04808 Wurzen +04880 Dommitzsch +04774 Dahlen +04758 Oschatz +04886 Arzberg, Beilrode +04769 Mügeln +01609 Gröditz, Wülknitz, Röderaue +01561 Großenhain, Ebersbach u.a. +01665 Käbschütztal, Klipphausen, Diera-Zehren +01594 Riesa, Stauchitz, Hirschstein +01623 Lommatzsch +01723 Wilsdruff +04720 Döbeln, Großweitzschen u.a. +04703 Leisnig +04749 Ostrau +04862 Mockrehna +09114 Chemnitz +09228 Chemnitz +09247 Röhrsdorf +09117 Chemnitz +09128 Chemnitz +09126 Chemnitz +09113 Chemnitz +09127 Chemnitz +09131 Chemnitz +09116 Chemnitz +09249 Taura b. Burgstädt +09577 Niederwiesa +09236 Claußnitz +09244 Lichtenau +09648 Mittweida, Kriebstein +09120 Chemnitz +09119 Chemnitz +09122 Chemnitz +09112 Chemnitz +09111 Chemnitz +09130 Chemnitz +04643 Geithain +04680 Colditz +09326 Geringswalde +04736 Waldheim +04746 Hartha +09603 Großschirma +09557 Flöha +09569 Oederan +09633 Halsbrücke +09599 Freiberg +09638 Lichtenberg/Erzgeb. +09627 Bobritzsch +04741 Roßwein +09629 Reinsberg +01683 Nossen +09634 Reinsberg +04687 Trebsen/Mulde +04828 Bennewitz, Machern +04779 Wermsdorf +04860 Torgau, Dreiheide +04889 Belgern-Schildau +04861 Torgau +01616 Strehla +01591 Riesa +01589 Riesa +01587 Riesa +01619 Zeithain +01612 Nünchritz, Glaubitz +04874 Belgern-Schildau +09544 Neuhausen/Erzgeb. +09623 Frauenstein +01776 Hermsdorf/Erzgeb. +01773 Altenberg +01778 Altenberg +01774 Klingenberg +01738 Dorfhain +01737 Tharandt u.a. +01734 Rabenau +01762 Hartmannsdorf-Reichenau +01705 Freital +01689 Weinböhla +01471 Radeburg +01847 Lohmen +01744 Dippoldiswalde +01936 Königsbrück u.a. +01920 Elstra, Oßling u.a. +02991 Lauta +02979 Spreetal, Elsterheide +02699 Königswartha +02994 Bernsdorf +01257 Dresden +01825 Liebstadt +01809 Heidenau +01731 Kreischa +01768 Glashütte +01728 Bannewitz +01445 Radebeul +01640 Coswig +01468 Moritzburg +01662 Meißen +01156 Dresden +01169 Dresden +01108 Dresden +01069 Dresden +01129 Dresden +01328 Dresden +01277 Dresden +01326 Dresden +01189 Dresden +01259 Dresden +01187 Dresden +01465 Dresden +01109 Dresden +01099 Dresden +01159 Dresden +01139 Dresden +01458 Ottendorf-Okrilla +01217 Dresden +01157 Dresden +01067 Dresden +01127 Dresden +01097 Dresden +01239 Dresden +01237 Dresden +01219 Dresden +01279 Dresden +01309 Dresden +01307 Dresden +01324 Dresden +01848 Hohnstein +01824 Königstein/Sächs.Schw. +01819 Bahretal +01816 Bad Gottleuba-Berggießhübel +01829 Wehlen +01796 Pirna, Struppen, Dohma +01844 Neustadt i. Sa. +01814 Bad Schandau +01855 Sebnitz +01833 Stolpen, Dürrröhrsdorf-Dittersbach +01477 Arnsdorf b. Dresden +01896 Pulsnitz +01454 Radeberg, Wachau +01900 Großröhrsdorf, Bretnig-Hauswalde +01909 Großharthau, Frankenthal +01906 Burkau +01877 Bischofswerda u.a. +02633 Göda +01558 Großenhain +01917 Kamenz +02977 Hoyerswerda +02997 Wittichenau +02999 Lohsa +02627 Weißenberg, Hochkirch u.a. +02943 Weißwasser, Boxberg +02906 Niesky, Hohendubrau u.a. +02894 Reichenbach, Vierkirchen +02829 Markersdorf, Neißeaue u.a. +02957 Krauschwitz, Weißkeißel +02727 Ebersbach-Neugersdorf +02747 Herrnhut +02799 Großschönau +02794 Leutersdorf, Spitzkunnersdorf +02791 Oderwitz +02739 Kottmar +02796 Jonsdorf +02782 Seifhennersdorf +02779 Großschönau +01904 Neukirch/Lausitz +02692 Doberschau-Gaußig, Großpostwitz, Obergurig +02689 Sohland a. d. Spree +02681 Wilthen +02625 Bautzen +02733 Cunewalde +02730 Ebersbach-Neugersdorf +02708 Löbau, Kottmar u.a. +02742 Neusalza-Spremberg +02736 Beiersdorf, Oppach +02797 Oybin +02788 Zittau +02785 Olbersdorf +02899 Ostritz, Schönau-Berzdorf +02763 Zittau u.a. +02748 Bernstadt a. d. Eigen +02828 Görlitz +02826 Görlitz +02827 Görlitz +02694 Großdubrau, Malschwitz +02959 Schleife +02953 Bad Muskau, Groß Düben, Gablenz +02923 Hähnichen, Horka, Kodersdorf +02956 Rietschen +02929 Rothenburg/O.L. +06493 Ballenstedt, Harzgerode +06536 Südharz, Berga +06268 Querfurt, Obhausen, Mücheln u.a. +06542 Allstedt +06526 Sangerhausen +06528 Wallhausen, Blankenheim +06618 Naumburg +06246 Bad Lauchstädt +06179 Teutschenthal +06347 Gerbstedt +06343 Mansfeld +06193 Petersberg +06648 Eckartsberga +06647 Bad Bibra, Finne u.a. +06628 Lanitz-Hassel-Tal, Molauer Land +06632 Freyburg, Balgstädt +06571 Roßleben-Wiehe, Gehofen +06537 Kelbra (Kyffhäuser) +06642 Nebra, Kaiserpfalz +06636 Laucha an der Unstrut +06249 Mücheln/ Geiseltal +06638 Karsdorf +06279 Schraplau, Farnstädt +06295 Eisleben +06311 Helbra +06313 Hergisdorf +06308 Klostermansfeld, Benndorf +06317 Seegebiet Mansfelder Land +06198 Salzatal +06722 Droyßig, Wetterzeube +06258 Schkopau +06774 Muldestausee +06712 Zeitz, Gutenborn u.a. +06721 Meineweh, Osterfeld +06682 Teuchern +06667 Weißenfels, Stößen +06711 Zeitz +06729 Elsteraue +06679 Hohenmölsen +06686 Lützen +06242 Braunsbedra +06259 Frankleben +06217 Merseburg +06255 Mücheln +06688 Weißenfels +06231 Bad Dürrenberg +06237 Leuna +06110 Halle/ Saale +06132 Halle/ Saale +06120 Halle/ Saale +06118 Halle/ Saale +06128 Halle/ Saale +06114 Halle/ Saale +06108 Halle/ Saale +06126 Halle/ Saale +06124 Halle/ Saale +06122 Halle/ Saale +06780 Zörbig +06794 Sandersdorf-Brehna +06130 Halle/ Saale +06116 Halle/ Saale +06112 Halle/ Saale +06184 Kabelsketal +06188 Landsberg +06809 Roitzsch, Petersroda +06749 Bitterfeld-Wolfen +06808 Bitterfeld-Wolfen +06792 Sandersdorf-Brehna +06796 Sandersdorf-Brehna +06502 Thale, Blankenburg +38889 Blankenburg, Oberharz am Brocken +38855 Wernigerode, Nordharz +38871 Ilsenburg, Nordharz +38822 Halberstadt, Groß Quenstedt +38875 Oberharz am Brocken +38877 Oberharz am Brocken +38879 Wernigerode +38899 Oberharz am Brocken +38835 Osterwieck +38836 Badersleben u.a. +38895 Langenstein, Derenburg +06485 Quedlinburg, Ballenstedt +39291 Möckern, Schermen, Nedlitz u.a. +39517 Tangerhütte u.a. +39638 Gardelegen +39343 Erxleben, Nordgermersleben u.a. +39365 Harbke, Sommersdorf, Wefensleben, Ummendorf, Eilsleben +39326 Wolmirstedt u.a. +39164 Wanzleben-Börde +39397 Schwanebeck, Gröningen, Kroppenstedt +39345 Haldensleben, Flechtingen, Bülstringen u.a. +39649 Gardelegen +39359 Rätzlingen, Wegenstedt, Calvörde, Böddensell u.a. +06484 Quedlinburg +06456 Arnstein +06449 Aschersleben +39240 Calbe, Rosenburg u.a. +39288 Burg +06463 Falkenstein +06458 Hedersleben +06466 Seeland +06467 Seeland +06469 Seeland +06543 Falkenstein +39393 Hötensleben, Völpke, Ottleben u.a. +38838 Huy +38820 Halberstadt +38829 Harsleben +39387 Oschersleben (Bode) +38828 Wegeleben +39418 Staßfurt +39444 Hecklingen +06464 Seeland +06333 Hettstedt, Endorf +06406 Bernburg (Saale) +06420 Könnern +06408 Ilberstedt +06425 Alsleben/Saale, Plötzkau +06429 Nienburg (Saale) +39439 Güsten +39435 Egeln, Borne, Wolmirsleben u.a. +39446 Staßfurt +39171 Sülzetal +39448 Börde-Hakel +39443 Staßfurt +39221 Welsleben, Biere, Eickendorf u.a. +39218 Schönebeck +39217 Schönebeck (Elbe) +39122 Magdeburg +39646 Oebisfelde +39356 Weferlingen, Behnsdorf, Belsdorf u.a. +39167 Eichenbarleben, Irxleben, Niederndodeleben u.a. +39116 Magdeburg +39175 Biederitz, Gerwisch, Menz u.a. +39179 Barleben +39110 Magdeburg +39108 Magdeburg +39114 Magdeburg +39118 Magdeburg +39120 Magdeburg +39126 Magdeburg +39128 Magdeburg +39104 Magdeburg +39112 Magdeburg +39130 Magdeburg +39124 Magdeburg +39106 Magdeburg +39340 Haldensleben +38489 Beetzendorf, Rohrberg, Jübar +29413 Dähre, Diesdorf, Wallstawe +38486 Klötze, Apenburg-Winterfeld +39615 Seehausen, Werben, Leppin u.a. +39606 Osterburg, Altmärkische Höhe +39579 Rochau +39624 Kalbe +29410 Salzwedel +39576 Stendal +39619 Arendsee +39628 Bismark +29416 Kuhfelde +39629 Bismark +06366 Köthen +06388 Südliches Anhalt, Köthen +06869 Coswig (Anhalt) +06886 Wittenberg +06842 Dessau-Roßlau +06772 Gräfenhainichen +06785 Oranienbaum-Wörlitz +39264 Güterglück, Lindau, Deetz u.a. +39279 Loburg, Leitzkau +39319 Jerichow +39307 Genthin, Hohenseeden, Zabakuck u.a. +06369 Südliches Anhalt u.a. +06386 Osternienburger Land +14715 Milower Land, Schollene, Nennhausen u.a. +06901 Kemberg +06888 Wittenberg +06889 Wittenberg +06773 Gräfenhainichen +06846 Dessau-Roßlau +06847 Dessau-Roßlau +39249 Barby +06862 Dessau-Roßlau +06861 Dessau-Roßlau +39261 Zerbst/Anhalt +06385 Aken (Elbe) +06779 Raguhn-Jeßnitz +06800 Raguhn-Jeßnitz +06766 Bitterfeld-Wolfen +06803 Bitterfeld-Wolfen +06849 Dessau-Roßlau +06868 Coswig (Anhalt) +06844 Dessau-Roßlau +39245 Gommern, Dannigkow +39317 Elbe-Parey +06895 Zahna-Elster +06905 Bad Schmiedeberg +06917 Jessen (Elster) +06925 Annaburg +39590 Tangermünde +39596 Goldbeck, Arneburg u.a. +39539 Havelberg +39524 Sandau +98634 Wasungen +36404 Vacha, Unterbreizbach +36452 Kaltennordheim +36419 Geisa +99834 Gerstungen +99837 Berka/ Werra +37308 Heiligenstadt +36414 Unterbreizbach +36460 Krayenberggemeinde, Frauensee +37318 Arenshausen, Uder u.a. +98663 Bad Colberg-Heldburg +98553 Schleusingen u.a. +98617 Meiningen +98574 Schmalkalden +36433 Bad Salzungen +99330 Gräfenroda +99310 Arnstadt +99885 Luisenthal, Ohrdruf, Wolfis +98630 Römhild +98646 Hildburghausen +98530 Suhl, Marisfeld, Rohr u.a. +98631 Grabfeld +98590 Schwallungen +36466 Dermbach, Wiesenthal +36457 Stadtlengsfeld, Weilar, Urnshausen +98597 Breitungen/Werra +36456 Barchfeld-Immelborn +98639 Walldorf +98547 Viernau u.a. +98593 Floh-Seligenthal +98596 Brotterode-Trusetal +98660 Themar +98701 Großbreitenbach +98666 Biberau, Masserberg +98673 Eisfeld, Auengrund +98667 Schleusegrund +98669 Veilsdorf +98559 Oberhof +98529 Suhl +98528 Suhl +98527 Suhl +98587 Steinbach-Hallenberg +98544 Zella-Mehlis +98554 Benshausen +99887 Georgenthal/ Thür. Wald +99897 Tambach-Dietharz/ Thür. +98704 Langewiesen +98693 Ilmenau +98714 Stützerbach +98711 Schmiedefeld, Frauenwald, Suhl +98716 Geschwenda +99338 Arnstadt +07389 Ranis +07922 Tanna +07926 Gefell +07907 Schleiz +96515 Sonneberg, Judenbach +07330 Probstzella +07407 Rudolstadt +07318 Saalfeld/Saale +96528 Frankenblick, Schalkau, Bachfeld +96524 Neuhaus-Schierschnitz +96523 Steinach +98724 Neuhaus am Rennweg, Lauscha +98744 Oberweißbach u.a. +98746 Katzhütte +98743 Gräfenthal +07426 Königsee-Rottenbach u.a. +98708 Gehren +99326 Stadtilm, Ilmtal +07429 Döschnitz, Sitzendorf, Rohrbach +07427 Schwarzburg +07422 Bad Blankenburg +07368 Remptendorf +07343 Wurzbach +07338 Kaulsdorf +07349 Lehesten +07366 Rosenthal am Rennsteig +07356 Lobenstein +07929 Saalburg-Ebersdorf +07387 Krölpa +07333 Unterwellenborn +07768 Kahla +07381 Pößneck +07924 Ziegenrück +07806 Neustadt/ Orla +07646 Stadtroda u.a. +36469 Tiefenort +99817 Eisenach +99092 Erfurt +99334 Elleben, Wachsenburg +99869 Drei Gleichen +99947 Bad Langensalza +99976 Rodeberg, Dünwald u.a. +99974 Mühlhausen, Unstruttal +99820 Hörselberg-Hainich +37339 Worbis +99706 Sondershausen +99713 Ebeleben +99755 Ellrich +99734 Nordhausen +99759 Sollstedt +99819 Marksuhl, Krauthausen u.a. +99707 Kyffhäuserland +36448 Bad Liebenstein +99891 Tabarz/ Thür. Wald +99880 Waltershausen +99842 Ruhla +99848 Wutha-Farnroda +99846 Seebach +99988 Südeichsfeld +99831 Creuzburg, Ifta +99826 Mihla +99830 Treffurt +99986 Vogtei, Kammerforst u.a. +99998 Körner, Weinbergen +99867 Gotha +99894 Leinatal +99192 Nesse-Apfelstädt, Nottleben +99090 Erfurt +99958 Großvargula, Tonna +99991 Großengottern, Heroldishausen +99100 Großfahner, Dachwig u.a. +99955 Bad Tennstedt +99189 Gebesee +37359 Küllstedt +37327 Leinefelde-Worbis, Wingerode, Hausen +37351 Dingelstädt +37355 Niederorschel u.a. +37345 Am Ohmberg, Sonnenstein +99996 Menteroda, Obermehler +99994 Schlotheim +99735 Werther Hohenstein Wolkramshausen +99718 Greußen Clingen Großenehrich +99752 Bleicherode +99765 Heringen/ Helme +99768 Harztor +99098 Erfurt +99094 Erfurt +99097 Erfurt +99085 Erfurt +99099 Erfurt +99102 Rockhausen, Klettbach +99198 Udestedt, Mönchenholzhausen u.a. +99610 Sömmerda +07751 Jena, Bucha, Großpürschütz u.a. +99084 Erfurt +99096 Erfurt +99438 Bad Berka u.a. +99425 Weimar +99444 Blankenhain +99448 Kranichfeld u.a. +99423 Weimar +99428 Weimar +99195 Großrudestedt, Schloßvippach u.a. +99634 Straußfurt +99631 Weißensee +99091 Erfurt +99089 Erfurt +99095 Erfurt +99087 Erfurt +99086 Erfurt +99636 Rastenberg +99628 Buttstädt +99625 Kölleda +99439 Berlstedt +99427 Weimar +99510 Apolda +99441 Lehnstedt u.a. +07745 Jena +07778 Neuengönna u.a. +07619 Schkölen +07616 Bürgel u.a. +07743 Jena +07749 Jena +07747 Jena +99518 Bad Sulza +07774 Dornburg-Camburg u.a. +99638 Kindelbrück +06578 Bilzingsleben Kannawurf Oldisleben +06567 Bad Frankenhausen/Kyffhäuser +06571 Roßleben-Wiehe, Gehofen +06556 Artern/Unstrut u.a. +06577 Heldrungen +06537 Kelbra (Kyffhäuser) +07937 Zeulenroda-Triebes, Langenwolschendorf +07570 Weida, Harth-Pöllnitz, Wünschendorf +07580 Ronneburg, Braunichswalde, Großenstein u.a. +95183 Feilitzsch +07927 Hirschberg +07919 Kirschkau, Pausa-Mühltroff +07819 Triptis +07950 Zeulenroda-Triebes, Weißendorf +07955 Auma-Weidatal +07980 Berga/Elster +07987 Mohlsdorf-Teichwolframsdorf +07973 Greiz +07957 Langenwetzendorf +07958 Hohenleuben +07613 Crossen, Heideland u.a. +07639 Bad Klosterlausnitz +07607 Eisenberg, Gösen, Hainspitz +07629 Hermsdorf +07586 Bad Köstritz +07589 Münchenbernsdorf, Schwarzbach, Bocka +04603 Nobitz, Göhren, Windischleuba +04618 Langenleuba-Niederhain +04639 Gößnitz +07557 Gera, Zedlitz u.a. +07554 Brahmenau +07546 Gera +07548 Gera +07549 Gera +07551 Gera +07552 Gera +07545 Gera +04626 Schmölln, Altkirchen, Nöbdenitz u.a. +04600 Altenburg +04617 Rositz, Starkenberg, Treben +04610 Meuselwitz +04613 Lucka diff --git a/address-validation-service/src/test/pacts/delivery-service-address-validation-service.json b/address-validation-service/src/test/pacts/delivery-service-address-validation-service.json new file mode 100644 index 00000000..11006e15 --- /dev/null +++ b/address-validation-service/src/test/pacts/delivery-service-address-validation-service.json @@ -0,0 +1,126 @@ +{ + "provider": { + "name": "address-validation-service" + }, + "consumer": { + "name": "delivery-service" + }, + "interactions": [ + { + "description": "POST request for Max's address", + "request": { + "method": "POST", + "path": "/valid-addresses", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "city": "26122 Oldenburg", + "street": { + "number": "1", + "name": "Poststrasse" + }, + "recipient": "Max Mustermann" + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/json.*" + } + ], + "combine": "AND" + } + } + } + }, + "response": { + "status": 200 + }, + "providerStates": [ + { + "name": "Three customers" + } + ] + }, + { + "description": "POST request for Sherlock's address", + "request": { + "method": "POST", + "path": "/valid-addresses", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "city": "London NW1 6XE", + "street": { + "number": "221B", + "name": "Baker Street" + }, + "recipient": "Sherlock Holmes" + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/json.*" + } + ], + "combine": "AND" + } + } + } + }, + "response": { + "status": 400, + "headers": { + "Content-Type": "application/problem+json" + }, + "body": { + "detail": "Addresses from UK are not supported for delivery" + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/problem\\+json.*" + } + ], + "combine": "AND" + } + }, + "body": { + "$.detail": { + "matchers": [ + { + "match": "regex", + "regex": ".*" + } + ], + "combine": "AND" + } + } + } + }, + "providerStates": [ + { + "name": "Three customers" + } + ] + } + ], + "metadata": { + "pactSpecification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "4.0.10" + } + } +} \ No newline at end of file diff --git a/address-validation-service/src/test/resources/de/openknowledge/sample/address/ambiguous-address.json b/address-validation-service/src/test/resources/de/openknowledge/sample/address/ambiguous-address.json new file mode 100644 index 00000000..7f16eca7 --- /dev/null +++ b/address-validation-service/src/test/resources/de/openknowledge/sample/address/ambiguous-address.json @@ -0,0 +1,7 @@ +{ + "street": { + "name": "Poststraße", + "number": "1" + }, + "city": "26909 Oldenburg" +} \ No newline at end of file diff --git a/address-validation-service/src/test/resources/de/openknowledge/sample/address/missing-address.json b/address-validation-service/src/test/resources/de/openknowledge/sample/address/missing-address.json new file mode 100644 index 00000000..4e26f972 --- /dev/null +++ b/address-validation-service/src/test/resources/de/openknowledge/sample/address/missing-address.json @@ -0,0 +1,7 @@ +{ + "street": { + "name": "Poststraße", + "number": "1" + }, + "city": "12345 Oldenburg" +} \ No newline at end of file diff --git a/address-validation-service/src/test/resources/de/openknowledge/sample/address/misspelled-address.json b/address-validation-service/src/test/resources/de/openknowledge/sample/address/misspelled-address.json new file mode 100644 index 00000000..a49a4765 --- /dev/null +++ b/address-validation-service/src/test/resources/de/openknowledge/sample/address/misspelled-address.json @@ -0,0 +1,7 @@ +{ + "street": { + "name": "Poststraße", + "number": "1" + }, + "city": "26121 Oldenburg" +} \ No newline at end of file diff --git a/address-validation-service/src/test/resources/de/openknowledge/sample/address/valid-address.json b/address-validation-service/src/test/resources/de/openknowledge/sample/address/valid-address.json new file mode 100644 index 00000000..1d5f4681 --- /dev/null +++ b/address-validation-service/src/test/resources/de/openknowledge/sample/address/valid-address.json @@ -0,0 +1,7 @@ +{ + "street": { + "name": "Poststraße", + "number": "1" + }, + "city": "26122 Oldenburg" +} \ No newline at end of file diff --git a/billing-service/.gitignore b/billing-service/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/billing-service/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/billing-service/Dockerfile b/billing-service/Dockerfile new file mode 100644 index 00000000..7d8ff0ad --- /dev/null +++ b/billing-service/Dockerfile @@ -0,0 +1,17 @@ +FROM maven:3.9.9-eclipse-temurin-21 AS mvn + +WORKDIR /usr/src/online-shop +COPY pom.xml ./ +RUN mvn package dependency:go-offline # cache dependencies +COPY src ./src +RUN mvn clean package meecrowave:bundle -Dservice.name=billing-service -Dmaven.test.skip=true -Dcheckstyle.skip=true + +FROM eclipse-temurin:21-jre + +RUN apt-get update -qq && apt-get install --no-install-recommends -qqy unzip && rm -rf /var/lib/apt/lists/* +COPY --from=mvn /usr/src/online-shop/target/billing-service-meecrowave-distribution.zip /opt/billing-service.zip +WORKDIR /opt +RUN unzip billing-service.zip && rm billing-service.zip +EXPOSE 8080 + +ENTRYPOINT ["/opt/billing-service-distribution/bin/meecrowave.sh", "run"] diff --git a/billing-service/pom.xml b/billing-service/pom.xml new file mode 100644 index 00000000..f21e858d --- /dev/null +++ b/billing-service/pom.xml @@ -0,0 +1,212 @@ + + + + + 4.0.0 + + de.openknowledge.sample.shop + ${service.name} + 1.0.0-SNAPSHOT + + + online-shop + 17 + 17 + false + UTF-8 + 1.2.15 + 1.9.6 + 5.8.2 + + + + + + org.apache.meecrowave + meecrowave-specs-api + ${meecrowave.version} + provided + + + org.apache.meecrowave + meecrowave-core + ${meecrowave.version} + provided + + + org.apache.geronimo.specs + geronimo-validation_1.0_spec + 1.1 + + + org.eclipse.microprofile.config + microprofile-config-api + 1.4 + + + org.apache.geronimo.config + geronimo-config-impl + 1.2.2 + + + org.eclipse.microprofile.jwt + microprofile-jwt-auth-api + 1.2.1 + + + org.apache.geronimo + geronimo-jwt-auth + 1.0.4 + + + org.apache.deltaspike.modules + deltaspike-security-module-api + ${deltaspike.version} + compile + + + org.apache.deltaspike.modules + deltaspike-security-module-impl + ${deltaspike.version} + runtime + + + org.eclipse.microprofile.opentracing + microprofile-opentracing-api + 1.2 + + + io.opentracing + opentracing-api + 0.31.0 + + + org.apache.geronimo + geronimo-opentracing + 1.0.3 + + + org.eclipse.microprofile.health + microprofile-health-api + 3.0-RC3 + + + org.apache.geronimo + geronimo-health + 2.0.1 + + + org.microprofile-ext.health-ext + healthprobe-jvm + 1.2.0 + + + org.microprofile-ext.health-ext + healthprobe-system + 1.2.0 + + + org.eclipse.microprofile.metrics + microprofile-metrics-api + 3.0-RC2 + + + org.apache.geronimo + geronimo-metrics + 1.0.6 + + + org.apache.commons + commons-lang3 + 3.9 + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.assertj + assertj-core + 3.22.0 + test + + + org.mockito + mockito-core + 2.28.2 + test + + + org.apache.meecrowave + meecrowave-junit + ${meecrowave.version} + test + + + rocks.limburg.cdimock + cdimock + 1.0.4 + test + + + com.tngtech.archunit + archunit + 1.0.1 + test + + + + ${project.artifactId} + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + org.apache.meecrowave + meecrowave-maven-plugin + ${meecrowave.version} + + + maven-checkstyle-plugin + 3.2.0 + + true + ${project.basedir}/src/main/checkstyle/java.header + true + true + + + + compile + compile + + checkstyle + + + ${project.basedir}/src/main/checkstyle/configuration.xml + true + + + + + + + diff --git a/billing-service/src/main/checkstyle/configuration.xml b/billing-service/src/main/checkstyle/configuration.xml new file mode 100644 index 00000000..3392d8cf --- /dev/null +++ b/billing-service/src/main/checkstyle/configuration.xml @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/billing-service/src/main/checkstyle/java.header b/billing-service/src/main/checkstyle/java.header new file mode 100644 index 00000000..baba0272 --- /dev/null +++ b/billing-service/src/main/checkstyle/java.header @@ -0,0 +1,15 @@ +/* + * Copyright \d{4}( - \d{4}|(, \d{4})*) [A-Za-z ,\-]. + * + * Licensed under the Apache License, Version 2\.0 \(the "License"\); + * you may not use this file except in compliance with the License\. + * You may obtain a copy of the License at + * + * http://www\.apache\.org/licenses/LICENSE-2\.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\. + * See the License for the specific language governing permissions and + * limitations under the License\. + */ diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java b/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java new file mode 100644 index 00000000..89397e09 --- /dev/null +++ b/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java @@ -0,0 +1,26 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.application; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +/** + * Application initialization + */ +@ApplicationPath("/") +public class AddressApplication extends Application { +} diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java b/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java new file mode 100644 index 00000000..3caa46af --- /dev/null +++ b/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.application; + +import java.util.logging.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import de.openknowledge.sample.address.domain.Address; +import de.openknowledge.sample.address.domain.AddressRepository; +import de.openknowledge.sample.address.domain.CustomerNumber; + +/** + * RESTFul endpoint for delivery addresses + */ +@ApplicationScoped +@Path("/billing-addresses") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class AddressResource { + + private static final Logger LOGGER = Logger.getLogger(AddressResource.class.getSimpleName()); + + @Inject + private AddressRepository addressesRepository; + + @GET + @Path("/{customerNumber}") + @Produces(MediaType.APPLICATION_JSON) + public Address getAddress(@PathParam("customerNumber") CustomerNumber number) { + LOGGER.info("RESTful call 'GET address'"); + return addressesRepository.find(number).orElseThrow(NotFoundException::new); + } + + @POST + @Path("/{customerNumber}") + @Consumes(MediaType.APPLICATION_JSON) + public Response setAddress( + @PathParam("customerNumber") CustomerNumber customerNumber, + Address address, + @Context UriInfo uri) { + + LOGGER.info("RESTful call 'POST address'"); + addressesRepository.update(customerNumber, address); + return Response.ok().build(); + } +} diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/Address.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/Address.java new file mode 100644 index 00000000..f650f7f5 --- /dev/null +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/Address.java @@ -0,0 +1,106 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static java.util.Optional.ofNullable; +import static org.apache.commons.lang3.Validate.notNull; + +import java.util.Objects; + +import javax.json.bind.annotation.JsonbCreator; +import javax.json.bind.annotation.JsonbProperty; + +public class Address { + private Recipient recipient; + private Street street; + private City city; + + @JsonbCreator + public Address(@JsonbProperty("recipient") Recipient recipient) { + this.recipient = notNull(recipient, "recipient may not be null"); + } + + public Address(Recipient recipient, Street street, City city) { + this(recipient); + setStreet(street); + setCity(city); + } + + public Recipient getRecipient() { + return recipient; + } + + public Street getStreet() { + return street; + } + + public void setStreet(Street street) { + this.street = street; + } + + public City getCity() { + return city; + } + + public void setCity(City city) { + this.city = city; + } + + @Override + public int hashCode() { + return recipient.hashCode() + ofNullable(street).map(Object::hashCode).orElse(0) + ofNullable(city).map(Object::hashCode).orElse(0); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || !getClass().equals(object.getClass())) { + return false; + } + Address address = (Address)object; + return recipient.equals(recipient) && Objects.equals(street, address.street) && Objects.equals(city, address.city); + } + + public static Builder of(String recipient) { + return new Builder(new Recipient(recipient)); + } + + public static class Builder { + + private Address address; + + private Builder(Recipient recipient) { + address = new Address(recipient); + } + + public Builder atStreet(String name) { + AddressLine addressLine = new AddressLine(name); + address.setStreet(new Street(addressLine.getStreetName(), addressLine.getHouseNumber())); + return this; + } + + public Builder inCity(String city) { + address.setCity(new City(city)); + return this; + } + + public Address build() { + return address; + } + } +} diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java new file mode 100644 index 00000000..596ce2b3 --- /dev/null +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java @@ -0,0 +1,114 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.AddressLine.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class AddressLine { + + public static final AddressLine EMPTY = new AddressLine(""); + + private String line; + + protected AddressLine() { + // for frameworks + } + + public AddressLine(String line) { + this.line = notNull(line, "line may not be null").trim(); + } + + public StreetName getStreetName() { + String firstSegment = line.substring(0, line.indexOf(' ')); + String lastSegment = line.substring(line.lastIndexOf(' ') + 1); + if (containsDigit(lastSegment)) { + return new StreetName(line.substring(0, line.length() - lastSegment.length())); + } else if (containsDigit(firstSegment)) { + return new StreetName(line.substring(firstSegment.length())); + } else { + throw new IllegalStateException("Could not determine street name"); + } + } + + public HouseNumber getHouseNumber() { + String firstSegment = line.substring(0, line.indexOf(' ')); + String lastSegment = line.substring(line.lastIndexOf(' ') + 1); + if (containsDigit(lastSegment)) { + return new HouseNumber(lastSegment); + } else if (containsDigit(firstSegment)) { + return new HouseNumber(firstSegment); + } else { + throw new IllegalStateException("Could not determine house number"); + } + } + + @Override + public String toString() { + return line; + } + + @Override + public int hashCode() { + return line.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof AddressLine)) { + return false; + } + + AddressLine recipient = (AddressLine)object; + + return toString().equals(recipient.toString()); + } + + private boolean containsDigit(String name) { + return name.contains("0") + || name.contains("1") + || name.contains("2") + || name.contains("3") + || name.contains("4") + || name.contains("5") + || name.contains("6") + || name.contains("7") + || name.contains("8") + || name.contains("9"); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public AddressLine adaptFromJson(String name) throws Exception { + return new AddressLine(name); + } + + @Override + public String adaptToJson(AddressLine name) throws Exception { + return name.toString(); + } + } +} diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java new file mode 100644 index 00000000..ea48ac32 --- /dev/null +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java @@ -0,0 +1,55 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static java.lang.String.format; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class AddressRepository { + + private static final Logger LOGGER = Logger.getLogger(AddressRepository.class.getSimpleName()); + + private Map addresses; + + @PostConstruct + public void initialize() { + + addresses = new ConcurrentHashMap<>(); + + addresses.put(new CustomerNumber("0815"), new Address(new Recipient("Max Mustermann"), + new Street(new StreetName("Poststr."), new HouseNumber("1")), new City("26122 Oldenburg"))); + + addresses.put(new CustomerNumber("0816"), new Address(new Recipient("Erika Mustermann"), + new Street(new StreetName("II. Hagen"), new HouseNumber("7")), new City("45127 Essen"))); + LOGGER.info(format("address repository initialized with %d addresses: ", addresses.size())); + } + + public Optional
find(CustomerNumber number) { + return Optional.ofNullable(addresses.get(number)); + } + + public void update(CustomerNumber number, Address address) { + Optional.ofNullable(address).ifPresent(a -> addresses.put(number, a)); + } +} diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/City.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/City.java new file mode 100644 index 00000000..cc521fe9 --- /dev/null +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/City.java @@ -0,0 +1,113 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.City.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class City { + + private String name; + + public City(String name) { + this.name = notNull(name, "name may not be empty").trim(); + } + + protected City() { + // for framework + } + + public ZipCode getZipCode() { + String firstSegment = name.substring(0, name.indexOf(' ')); + String lastSegment = name.substring(name.lastIndexOf(' ') + 1); + if (containsDigit(firstSegment)) { + return new ZipCode(firstSegment); + } else if (containsDigit(lastSegment)) { + return new ZipCode(name.substring(firstSegment.length())); + } else { + throw new IllegalStateException("Could not determine zip code"); + } + } + + public CityName getCityName() { + String firstSegment = name.substring(0, name.indexOf(' ')); + String lastSegment = name.substring(name.lastIndexOf(' ') + 1); + if (containsDigit(firstSegment)) { + return new CityName(name.substring(firstSegment.length())); + } else if (containsDigit(lastSegment)) { + return new CityName(name.substring(0, firstSegment.length())); + } else { + throw new IllegalStateException("Could not determine city name"); + } + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + City city = (City)object; + + return toString().equals(city.toString()); + } + + private boolean containsDigit(String cityName) { + return cityName.contains("0") + || cityName.contains("1") + || cityName.contains("2") + || cityName.contains("3") + || cityName.contains("4") + || cityName.contains("5") + || cityName.contains("6") + || cityName.contains("7") + || cityName.contains("8") + || cityName.contains("9"); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public City adaptFromJson(String name) throws Exception { + return new City(name); + } + + @Override + public String adaptToJson(City name) throws Exception { + return name.toString(); + } + } + +} diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java new file mode 100644 index 00000000..1fea0874 --- /dev/null +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.CityName.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class CityName { + + private String name; + + public CityName(String name) { + this.name = notNull(name, "name may not be empty").trim(); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + CityName city = (CityName)object; + + return toString().equals(city.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public CityName adaptFromJson(String name) throws Exception { + return new CityName(name); + } + + @Override + public String adaptToJson(CityName name) throws Exception { + return name.toString(); + } + } + +} diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java new file mode 100644 index 00000000..2cf9dec8 --- /dev/null +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notBlank; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.CustomerNumber.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class CustomerNumber { + + private String number; + + protected CustomerNumber() { + // for frameworks + } + + public CustomerNumber(String number) { + this.number = notBlank(number, "number may not be empty").trim(); + } + + @Override + public String toString() { + return number; + } + + @Override + public int hashCode() { + return number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof CustomerNumber)) { + return false; + } + + CustomerNumber customerNumber = (CustomerNumber)object; + + return toString().equals(customerNumber.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public CustomerNumber adaptFromJson(String number) throws Exception { + return new CustomerNumber(number); + } + + @Override + public String adaptToJson(CustomerNumber number) throws Exception { + return number.toString(); + } + + } + +} diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java new file mode 100644 index 00000000..e54c14c7 --- /dev/null +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java @@ -0,0 +1,81 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notBlank; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.HouseNumber.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class HouseNumber { + + private String number; + + protected HouseNumber() { + // for frameworks + } + + public HouseNumber(String number) { + this.number = notBlank(number, "number may not be empty").trim(); + } + + public static HouseNumber valueOf(String number) { + return new HouseNumber(number); + } + + @Override + public String toString() { + return number; + } + + @Override + public int hashCode() { + return number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof HouseNumber)) { + return false; + } + + HouseNumber houseNumber = (HouseNumber)object; + + return toString().equals(houseNumber.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public HouseNumber adaptFromJson(String number) throws Exception { + return new HouseNumber(number); + } + + @Override + public String adaptToJson(HouseNumber number) throws Exception { + return number.toString(); + } + + } + +} diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/Location.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/Location.java new file mode 100644 index 00000000..2152f801 --- /dev/null +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/Location.java @@ -0,0 +1,66 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.annotation.JsonbCreator; +import javax.json.bind.annotation.JsonbProperty; + +public class Location { + + private ZipCode zipCode; + private CityName cityName; + + @JsonbCreator + public Location(@JsonbProperty("zipCode") ZipCode zipCode, @JsonbProperty("cityName") CityName city) { + this.zipCode = notNull(zipCode, "zip code may not be null"); + this.cityName = notNull(city, "city name may not be null"); + } + + public ZipCode getZipCode() { + return zipCode; + } + + public CityName getCityName() { + return cityName; + } + + @Override + public int hashCode() { + return zipCode.hashCode() ^ cityName.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof Location)) { + return false; + } + + Location location = (Location)object; + + return zipCode.equals(location.zipCode) && cityName.equals(location.cityName); + } + + @Override + public String toString() { + return zipCode.isGerman() ? zipCode + " " + cityName : cityName + " " + zipCode; + } +} diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java new file mode 100644 index 00000000..cbf39d97 --- /dev/null +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java @@ -0,0 +1,81 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notBlank; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.Recipient.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class Recipient { + + private String name; + + protected Recipient() { + // for frameworks + } + + public Recipient(String name) { + this.name = notBlank(name, "name may not be empty").trim(); + } + + public static Recipient valueOf(String name) { + return new Recipient(name); + } + + @Override + public String toString() { + return name; + } + + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof Recipient)) { + return false; + } + + Recipient recipient = (Recipient)object; + + return toString().equals(recipient.toString()); + } + + + public static class Adapter implements JsonbAdapter { + + @Override + public Recipient adaptFromJson(String name) throws Exception { + return new Recipient(name); + } + + @Override + public String adaptToJson(Recipient name) throws Exception { + return name.toString(); + } + } +} diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/Street.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/Street.java new file mode 100644 index 00000000..bc0af1cd --- /dev/null +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/Street.java @@ -0,0 +1,74 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.annotation.JsonbCreator; +import javax.json.bind.annotation.JsonbProperty; + +public class Street { + + private StreetName name; + private HouseNumber number; + + @JsonbCreator + public Street(@JsonbProperty("name") StreetName name, @JsonbProperty("number") HouseNumber houseNumber) { + this.name = notNull(name, "name may not be null"); + this.number = notNull(houseNumber, "house number may not be null"); + } + + public StreetName getName() { + return name; + } + + public HouseNumber getNumber() { + return number; + } + + @Override + public int hashCode() { + return name.hashCode() ^ number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof Street)) { + return false; + } + + Street street = (Street)object; + + return name.equals(street.getName()) && number.equals(street.getNumber()); + } + + @Override + public String toString() { + if (isEnglish()) { + return number + " " + name; + } else { + return name + " " + number; + } + } + + private boolean isEnglish() { + return name.toString().toLowerCase().contains("street"); + } +} diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java new file mode 100644 index 00000000..16e7ae01 --- /dev/null +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notBlank; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.StreetName.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class StreetName { + + private String name; + + protected StreetName() { + // for frameworks + } + + public StreetName(String name) { + this.name = notBlank(name, "name may not be empty").trim(); + } + + public static StreetName valueOf(String name) { + return new StreetName(name); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof StreetName streetName)) { + return false; + } + + return toString().equals(streetName.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public StreetName adaptFromJson(String name) throws Exception { + return new StreetName(name); + } + + @Override + public String adaptToJson(StreetName name) throws Exception { + return name.toString(); + } + } +} diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java new file mode 100644 index 00000000..1e62563b --- /dev/null +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.ZipCode.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class ZipCode { + + private String code; + + public ZipCode(String code) { + this.code = notNull(code, "code may not be empty").trim(); + } + + public boolean isGerman() { + return code.length() == 5; + } + + @Override + public String toString() { + return code; + } + + @Override + public int hashCode() { + return code.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + ZipCode zipCode = (ZipCode)object; + + return toString().equals(zipCode.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public ZipCode adaptFromJson(String zip) throws Exception { + return new ZipCode(zip); + } + + @Override + public String adaptToJson(ZipCode zip) throws Exception { + return zip.toString(); + } + } + +} diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java b/billing-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java new file mode 100644 index 00000000..8a84c0f2 --- /dev/null +++ b/billing-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.infrastructure; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.ext.Provider; + +/** + * Filter to allow cross origin calls. + */ +@Provider +public class CorsFilter implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { + responseContext.getHeaders().add("Access-Control-Allow-Origin", "*"); + responseContext.getHeaders().add("Access-Control-Allow-Headers", "origin, content-type, accept, authorization"); + responseContext.getHeaders().add("Access-Control-Allow-Credentials", "true"); + responseContext.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD"); + responseContext.getHeaders().add("Access-Control-Max-Age", "1209600"); + } +} diff --git a/billing-service/src/test/java/de/openknowledge/sample/address/JsonObjectComparision.java b/billing-service/src/test/java/de/openknowledge/sample/address/JsonObjectComparision.java new file mode 100644 index 00000000..e7e08f86 --- /dev/null +++ b/billing-service/src/test/java/de/openknowledge/sample/address/JsonObjectComparision.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019 - 2025 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address; + +import java.io.InputStream; +import java.util.Map; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonValue; + +import org.assertj.core.api.Condition; + +public class JsonObjectComparision extends Condition> { + + public JsonObjectComparision(JsonObject object) { + super(v -> v.entrySet().containsAll(object.entrySet()), "object containing %s", object); + } + + public static Condition> sameAs(InputStream in) { + return new JsonObjectComparision(Json.createReader(in).readObject()); + } + + public static Condition thatIsSameAs(InputStream in) { + Condition condition = new JsonObjectComparision(Json.createReader(in).readObject()); + return (Condition)condition; + } +} diff --git a/billing-service/src/test/java/de/openknowledge/sample/address/domain/TestAddressRepository.java b/billing-service/src/test/java/de/openknowledge/sample/address/domain/TestAddressRepository.java new file mode 100644 index 00000000..180e75e9 --- /dev/null +++ b/billing-service/src/test/java/de/openknowledge/sample/address/domain/TestAddressRepository.java @@ -0,0 +1,25 @@ +/* + * Copyright 2019 - 2025 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import javax.enterprise.context.RequestScoped; +import javax.enterprise.inject.Specializes; + +@Specializes +@RequestScoped +public class TestAddressRepository extends AddressRepository { + +} diff --git a/billing-service/src/test/resources/de/openknowledge/sample/address/007.json b/billing-service/src/test/resources/de/openknowledge/sample/address/007.json new file mode 100644 index 00000000..481cc898 --- /dev/null +++ b/billing-service/src/test/resources/de/openknowledge/sample/address/007.json @@ -0,0 +1,8 @@ +{ + "recipient": "Sherlock Holmes", + "street": { + "name": "Baker Street", + "number": "221B" + }, + "city": "London NW1 6XE" +} diff --git a/billing-service/src/test/resources/de/openknowledge/sample/address/0815-new.json b/billing-service/src/test/resources/de/openknowledge/sample/address/0815-new.json new file mode 100644 index 00000000..9541839a --- /dev/null +++ b/billing-service/src/test/resources/de/openknowledge/sample/address/0815-new.json @@ -0,0 +1,8 @@ +{ + "recipient": "Erika Mustermann", + "street": { + "name": "II. Hagen", + "number": "7" + }, + "city": "45127 Essen" +} \ No newline at end of file diff --git a/billing-service/src/test/resources/de/openknowledge/sample/address/0815.json b/billing-service/src/test/resources/de/openknowledge/sample/address/0815.json new file mode 100644 index 00000000..84bb8d04 --- /dev/null +++ b/billing-service/src/test/resources/de/openknowledge/sample/address/0815.json @@ -0,0 +1,8 @@ +{ + "recipient": "Max Mustermann", + "street": { + "name": "Poststraße", + "number": "1" + }, + "city": "26122 Oldenburg" +} diff --git a/billing-service/src/test/resources/de/openknowledge/sample/address/0816.json b/billing-service/src/test/resources/de/openknowledge/sample/address/0816.json new file mode 100644 index 00000000..9541839a --- /dev/null +++ b/billing-service/src/test/resources/de/openknowledge/sample/address/0816.json @@ -0,0 +1,8 @@ +{ + "recipient": "Erika Mustermann", + "street": { + "name": "II. Hagen", + "number": "7" + }, + "city": "45127 Essen" +} \ No newline at end of file diff --git a/customer-service/.gitignore b/customer-service/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/customer-service/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/customer-service/Dockerfile b/customer-service/Dockerfile new file mode 100644 index 00000000..d56b6b78 --- /dev/null +++ b/customer-service/Dockerfile @@ -0,0 +1,17 @@ +FROM maven:3.9.9-eclipse-temurin-21 AS mvn + +WORKDIR /usr/src/online-shop +COPY pom.xml ./ +RUN mvn package dependency:go-offline # cache dependencies +COPY src ./src +RUN mvn clean package meecrowave:bundle -Dservice.name=customer-service -Dmaven.test.skip=true -Dcheckstyle.skip=true + +FROM eclipse-temurin:21-jre + +RUN apt-get update -qq && apt-get install --no-install-recommends -qqy unzip && rm -rf /var/lib/apt/lists/* +COPY --from=mvn /usr/src/online-shop/target/customer-service-meecrowave-distribution.zip /opt/customer-service.zip +WORKDIR /opt +RUN unzip customer-service.zip && rm customer-service.zip +EXPOSE 8080 + +ENTRYPOINT ["/opt/customer-service-distribution/bin/meecrowave.sh", "run"] diff --git a/customer-service/pom.xml b/customer-service/pom.xml new file mode 100644 index 00000000..f21e858d --- /dev/null +++ b/customer-service/pom.xml @@ -0,0 +1,212 @@ + + + + + 4.0.0 + + de.openknowledge.sample.shop + ${service.name} + 1.0.0-SNAPSHOT + + + online-shop + 17 + 17 + false + UTF-8 + 1.2.15 + 1.9.6 + 5.8.2 + + + + + + org.apache.meecrowave + meecrowave-specs-api + ${meecrowave.version} + provided + + + org.apache.meecrowave + meecrowave-core + ${meecrowave.version} + provided + + + org.apache.geronimo.specs + geronimo-validation_1.0_spec + 1.1 + + + org.eclipse.microprofile.config + microprofile-config-api + 1.4 + + + org.apache.geronimo.config + geronimo-config-impl + 1.2.2 + + + org.eclipse.microprofile.jwt + microprofile-jwt-auth-api + 1.2.1 + + + org.apache.geronimo + geronimo-jwt-auth + 1.0.4 + + + org.apache.deltaspike.modules + deltaspike-security-module-api + ${deltaspike.version} + compile + + + org.apache.deltaspike.modules + deltaspike-security-module-impl + ${deltaspike.version} + runtime + + + org.eclipse.microprofile.opentracing + microprofile-opentracing-api + 1.2 + + + io.opentracing + opentracing-api + 0.31.0 + + + org.apache.geronimo + geronimo-opentracing + 1.0.3 + + + org.eclipse.microprofile.health + microprofile-health-api + 3.0-RC3 + + + org.apache.geronimo + geronimo-health + 2.0.1 + + + org.microprofile-ext.health-ext + healthprobe-jvm + 1.2.0 + + + org.microprofile-ext.health-ext + healthprobe-system + 1.2.0 + + + org.eclipse.microprofile.metrics + microprofile-metrics-api + 3.0-RC2 + + + org.apache.geronimo + geronimo-metrics + 1.0.6 + + + org.apache.commons + commons-lang3 + 3.9 + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.assertj + assertj-core + 3.22.0 + test + + + org.mockito + mockito-core + 2.28.2 + test + + + org.apache.meecrowave + meecrowave-junit + ${meecrowave.version} + test + + + rocks.limburg.cdimock + cdimock + 1.0.4 + test + + + com.tngtech.archunit + archunit + 1.0.1 + test + + + + ${project.artifactId} + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + org.apache.meecrowave + meecrowave-maven-plugin + ${meecrowave.version} + + + maven-checkstyle-plugin + 3.2.0 + + true + ${project.basedir}/src/main/checkstyle/java.header + true + true + + + + compile + compile + + checkstyle + + + ${project.basedir}/src/main/checkstyle/configuration.xml + true + + + + + + + diff --git a/customer-service/src/main/checkstyle/configuration.xml b/customer-service/src/main/checkstyle/configuration.xml new file mode 100644 index 00000000..3392d8cf --- /dev/null +++ b/customer-service/src/main/checkstyle/configuration.xml @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/customer-service/src/main/checkstyle/java.header b/customer-service/src/main/checkstyle/java.header new file mode 100644 index 00000000..baba0272 --- /dev/null +++ b/customer-service/src/main/checkstyle/java.header @@ -0,0 +1,15 @@ +/* + * Copyright \d{4}( - \d{4}|(, \d{4})*) [A-Za-z ,\-]. + * + * Licensed under the Apache License, Version 2\.0 \(the "License"\); + * you may not use this file except in compliance with the License\. + * You may obtain a copy of the License at + * + * http://www\.apache\.org/licenses/LICENSE-2\.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\. + * See the License for the specific language governing permissions and + * limitations under the License\. + */ diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/Address.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/Address.java new file mode 100644 index 00000000..f650f7f5 --- /dev/null +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/Address.java @@ -0,0 +1,106 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static java.util.Optional.ofNullable; +import static org.apache.commons.lang3.Validate.notNull; + +import java.util.Objects; + +import javax.json.bind.annotation.JsonbCreator; +import javax.json.bind.annotation.JsonbProperty; + +public class Address { + private Recipient recipient; + private Street street; + private City city; + + @JsonbCreator + public Address(@JsonbProperty("recipient") Recipient recipient) { + this.recipient = notNull(recipient, "recipient may not be null"); + } + + public Address(Recipient recipient, Street street, City city) { + this(recipient); + setStreet(street); + setCity(city); + } + + public Recipient getRecipient() { + return recipient; + } + + public Street getStreet() { + return street; + } + + public void setStreet(Street street) { + this.street = street; + } + + public City getCity() { + return city; + } + + public void setCity(City city) { + this.city = city; + } + + @Override + public int hashCode() { + return recipient.hashCode() + ofNullable(street).map(Object::hashCode).orElse(0) + ofNullable(city).map(Object::hashCode).orElse(0); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || !getClass().equals(object.getClass())) { + return false; + } + Address address = (Address)object; + return recipient.equals(recipient) && Objects.equals(street, address.street) && Objects.equals(city, address.city); + } + + public static Builder of(String recipient) { + return new Builder(new Recipient(recipient)); + } + + public static class Builder { + + private Address address; + + private Builder(Recipient recipient) { + address = new Address(recipient); + } + + public Builder atStreet(String name) { + AddressLine addressLine = new AddressLine(name); + address.setStreet(new Street(addressLine.getStreetName(), addressLine.getHouseNumber())); + return this; + } + + public Builder inCity(String city) { + address.setCity(new City(city)); + return this; + } + + public Address build() { + return address; + } + } +} diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java new file mode 100644 index 00000000..864e5884 --- /dev/null +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java @@ -0,0 +1,115 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.AddressLine.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class AddressLine { + + public static final AddressLine EMPTY = new AddressLine(""); + + private String line; + + protected AddressLine() { + // for frameworks + } + + public AddressLine(String line) { + this.line = notNull(line, "line may not be null").trim(); + } + + public StreetName getStreetName() { + String firstSegment = line.substring(0, line.indexOf(' ')); + String lastSegment = line.substring(line.lastIndexOf(' ') + 1); + if (containsDigit(lastSegment)) { + return new StreetName(line.substring(0, line.length() - lastSegment.length())); + } else if (containsDigit(firstSegment)) { + return new StreetName(line.substring(firstSegment.length())); + } else { + throw new IllegalStateException("Could not determine street name"); + } + } + + public HouseNumber getHouseNumber() { + String firstSegment = line.substring(0, line.indexOf(' ')); + String lastSegment = line.substring(line.lastIndexOf(' ') + 1); + if (containsDigit(lastSegment)) { + return new HouseNumber(lastSegment); + } else if (containsDigit(firstSegment)) { + return new HouseNumber(firstSegment); + } else { + throw new IllegalStateException("Could not determine house number"); + } + } + + @Override + public String toString() { + return line; + } + + + @Override + public int hashCode() { + return line.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof AddressLine)) { + return false; + } + + AddressLine recipient = (AddressLine)object; + + return toString().equals(recipient.toString()); + } + + private boolean containsDigit(String name) { + return name.contains("0") + || name.contains("1") + || name.contains("2") + || name.contains("3") + || name.contains("4") + || name.contains("5") + || name.contains("6") + || name.contains("7") + || name.contains("8") + || name.contains("9"); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public AddressLine adaptFromJson(String name) throws Exception { + return new AddressLine(name); + } + + @Override + public String adaptToJson(AddressLine name) throws Exception { + return name.toString(); + } + } +} diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/BillingAddressRepository.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/BillingAddressRepository.java new file mode 100644 index 00000000..d5075f5c --- /dev/null +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/BillingAddressRepository.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static javax.ws.rs.client.Entity.entity; +import static javax.ws.rs.core.Response.Status.Family.SUCCESSFUL; + +import java.util.Optional; +import java.util.logging.Logger; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.apache.johnzon.jaxrs.jsonb.jaxrs.JsonbJaxrsProvider; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import de.openknowledge.sample.customer.domain.CustomerNumber; + +@ApplicationScoped +public class BillingAddressRepository { + + private static final Logger LOG = Logger.getLogger(BillingAddressRepository.class.getSimpleName()); + private static final String BILLING_ADDRESSES_PATH = "billing-addresses"; + + @Inject + @ConfigProperty(name = "billing-service.url") + String billingServiceUrl; + + Client client; + + @PostConstruct + public void newClient() { + client = ClientBuilder.newClient(); + } + + public Optional
find(CustomerNumber customerNumber) { + LOG.info("load billing address from " + billingServiceUrl); + return Optional.of(client + .register(JsonbJaxrsProvider.class) + .target(billingServiceUrl) + .path(BILLING_ADDRESSES_PATH) + .path(customerNumber.toString()) + .request(MediaType.APPLICATION_JSON) + .get()) + .filter(r -> r.getStatusInfo().getFamily() == SUCCESSFUL) + .filter(Response::hasEntity) + .map(r -> r.readEntity(Address.class)); + } + + public void update(CustomerNumber customerNumber, Address billingAddress) { + LOG.info("update billing address at " + billingServiceUrl); + client.target(billingServiceUrl) + .path(BILLING_ADDRESSES_PATH) + .path(customerNumber.toString()) + .request(MediaType.APPLICATION_JSON) + .post(entity(billingAddress, MediaType.APPLICATION_JSON_TYPE)); + } +} diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/City.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/City.java new file mode 100644 index 00000000..cc521fe9 --- /dev/null +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/City.java @@ -0,0 +1,113 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.City.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class City { + + private String name; + + public City(String name) { + this.name = notNull(name, "name may not be empty").trim(); + } + + protected City() { + // for framework + } + + public ZipCode getZipCode() { + String firstSegment = name.substring(0, name.indexOf(' ')); + String lastSegment = name.substring(name.lastIndexOf(' ') + 1); + if (containsDigit(firstSegment)) { + return new ZipCode(firstSegment); + } else if (containsDigit(lastSegment)) { + return new ZipCode(name.substring(firstSegment.length())); + } else { + throw new IllegalStateException("Could not determine zip code"); + } + } + + public CityName getCityName() { + String firstSegment = name.substring(0, name.indexOf(' ')); + String lastSegment = name.substring(name.lastIndexOf(' ') + 1); + if (containsDigit(firstSegment)) { + return new CityName(name.substring(firstSegment.length())); + } else if (containsDigit(lastSegment)) { + return new CityName(name.substring(0, firstSegment.length())); + } else { + throw new IllegalStateException("Could not determine city name"); + } + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + City city = (City)object; + + return toString().equals(city.toString()); + } + + private boolean containsDigit(String cityName) { + return cityName.contains("0") + || cityName.contains("1") + || cityName.contains("2") + || cityName.contains("3") + || cityName.contains("4") + || cityName.contains("5") + || cityName.contains("6") + || cityName.contains("7") + || cityName.contains("8") + || cityName.contains("9"); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public City adaptFromJson(String name) throws Exception { + return new City(name); + } + + @Override + public String adaptToJson(City name) throws Exception { + return name.toString(); + } + } + +} diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java new file mode 100644 index 00000000..1fea0874 --- /dev/null +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.CityName.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class CityName { + + private String name; + + public CityName(String name) { + this.name = notNull(name, "name may not be empty").trim(); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + CityName city = (CityName)object; + + return toString().equals(city.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public CityName adaptFromJson(String name) throws Exception { + return new CityName(name); + } + + @Override + public String adaptToJson(CityName name) throws Exception { + return name.toString(); + } + } + +} diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/DeliveryAddressRepository.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/DeliveryAddressRepository.java new file mode 100644 index 00000000..ed530fa5 --- /dev/null +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/DeliveryAddressRepository.java @@ -0,0 +1,91 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static javax.ws.rs.client.Entity.entity; +import static javax.ws.rs.core.Response.Status.Family.SUCCESSFUL; + +import java.io.StringReader; +import java.util.Optional; +import java.util.logging.Logger; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.json.Json; +import javax.json.JsonObject; +import javax.validation.ValidationException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import de.openknowledge.sample.customer.domain.CustomerNumber; + +@ApplicationScoped +public class DeliveryAddressRepository { + + private static final Logger LOG = Logger.getLogger(DeliveryAddressRepository.class.getSimpleName()); + private static final String DELIVERY_ADDRESSES_PATH = "delivery-addresses"; + + @Inject + @ConfigProperty(name = "delivery-service.url") + String deliveryServiceUrl; + + Client client; + + @PostConstruct + public void newClient() { + client = ClientBuilder.newClient(); + } + + public Optional
find(CustomerNumber customerNumber) { + LOG.info("load delivery address from " + deliveryServiceUrl); + return Optional.of(client + .target(deliveryServiceUrl) + .path(DELIVERY_ADDRESSES_PATH) + .path(customerNumber.toString()) + .request(MediaType.APPLICATION_JSON) + .get()) + .filter(r -> r.getStatusInfo().getFamily() == SUCCESSFUL) + .filter(Response::hasEntity) + .map(r -> r.readEntity(Address.class)); + } + + public void update(CustomerNumber customerNumber, Address deliveryAddress) { + LOG.info("update delivery address at " + deliveryServiceUrl); + Response response = client + .target(deliveryServiceUrl) + .path(DELIVERY_ADDRESSES_PATH) + .path(customerNumber.toString()) + .request(MediaType.APPLICATION_JSON) + .post(entity(deliveryAddress, MediaType.APPLICATION_JSON_TYPE)); + handleValidationError(response); + } + + private void handleValidationError(Response response) { + if (response.getStatus() == Response.Status.OK.getStatusCode()) { + return; + } + if (!response.hasEntity()) { + throw new ValidationException("invalid address"); + } + JsonObject problem = Json.createReader(new StringReader(response.readEntity(String.class))).readObject(); + throw new ValidationException(problem.getString("detail")); + } +} diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java new file mode 100644 index 00000000..e54c14c7 --- /dev/null +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java @@ -0,0 +1,81 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notBlank; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.HouseNumber.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class HouseNumber { + + private String number; + + protected HouseNumber() { + // for frameworks + } + + public HouseNumber(String number) { + this.number = notBlank(number, "number may not be empty").trim(); + } + + public static HouseNumber valueOf(String number) { + return new HouseNumber(number); + } + + @Override + public String toString() { + return number; + } + + @Override + public int hashCode() { + return number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof HouseNumber)) { + return false; + } + + HouseNumber houseNumber = (HouseNumber)object; + + return toString().equals(houseNumber.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public HouseNumber adaptFromJson(String number) throws Exception { + return new HouseNumber(number); + } + + @Override + public String adaptToJson(HouseNumber number) throws Exception { + return number.toString(); + } + + } + +} diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java new file mode 100644 index 00000000..cbf39d97 --- /dev/null +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java @@ -0,0 +1,81 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notBlank; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.Recipient.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class Recipient { + + private String name; + + protected Recipient() { + // for frameworks + } + + public Recipient(String name) { + this.name = notBlank(name, "name may not be empty").trim(); + } + + public static Recipient valueOf(String name) { + return new Recipient(name); + } + + @Override + public String toString() { + return name; + } + + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof Recipient)) { + return false; + } + + Recipient recipient = (Recipient)object; + + return toString().equals(recipient.toString()); + } + + + public static class Adapter implements JsonbAdapter { + + @Override + public Recipient adaptFromJson(String name) throws Exception { + return new Recipient(name); + } + + @Override + public String adaptToJson(Recipient name) throws Exception { + return name.toString(); + } + } +} diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/Street.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/Street.java new file mode 100644 index 00000000..bc0af1cd --- /dev/null +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/Street.java @@ -0,0 +1,74 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.annotation.JsonbCreator; +import javax.json.bind.annotation.JsonbProperty; + +public class Street { + + private StreetName name; + private HouseNumber number; + + @JsonbCreator + public Street(@JsonbProperty("name") StreetName name, @JsonbProperty("number") HouseNumber houseNumber) { + this.name = notNull(name, "name may not be null"); + this.number = notNull(houseNumber, "house number may not be null"); + } + + public StreetName getName() { + return name; + } + + public HouseNumber getNumber() { + return number; + } + + @Override + public int hashCode() { + return name.hashCode() ^ number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof Street)) { + return false; + } + + Street street = (Street)object; + + return name.equals(street.getName()) && number.equals(street.getNumber()); + } + + @Override + public String toString() { + if (isEnglish()) { + return number + " " + name; + } else { + return name + " " + number; + } + } + + private boolean isEnglish() { + return name.toString().toLowerCase().contains("street"); + } +} diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java new file mode 100644 index 00000000..16e7ae01 --- /dev/null +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notBlank; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.StreetName.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class StreetName { + + private String name; + + protected StreetName() { + // for frameworks + } + + public StreetName(String name) { + this.name = notBlank(name, "name may not be empty").trim(); + } + + public static StreetName valueOf(String name) { + return new StreetName(name); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof StreetName streetName)) { + return false; + } + + return toString().equals(streetName.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public StreetName adaptFromJson(String name) throws Exception { + return new StreetName(name); + } + + @Override + public String adaptToJson(StreetName name) throws Exception { + return name.toString(); + } + } +} diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java new file mode 100644 index 00000000..1e62563b --- /dev/null +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.ZipCode.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class ZipCode { + + private String code; + + public ZipCode(String code) { + this.code = notNull(code, "code may not be empty").trim(); + } + + public boolean isGerman() { + return code.length() == 5; + } + + @Override + public String toString() { + return code; + } + + @Override + public int hashCode() { + return code.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + ZipCode zipCode = (ZipCode)object; + + return toString().equals(zipCode.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public ZipCode adaptFromJson(String zip) throws Exception { + return new ZipCode(zip); + } + + @Override + public String adaptToJson(ZipCode zip) throws Exception { + return zip.toString(); + } + } + +} diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java b/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java new file mode 100644 index 00000000..8a84c0f2 --- /dev/null +++ b/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.infrastructure; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.ext.Provider; + +/** + * Filter to allow cross origin calls. + */ +@Provider +public class CorsFilter implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { + responseContext.getHeaders().add("Access-Control-Allow-Origin", "*"); + responseContext.getHeaders().add("Access-Control-Allow-Headers", "origin, content-type, accept, authorization"); + responseContext.getHeaders().add("Access-Control-Allow-Credentials", "true"); + responseContext.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD"); + responseContext.getHeaders().add("Access-Control-Max-Age", "1209600"); + } +} diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java b/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java new file mode 100644 index 00000000..4ece55c7 --- /dev/null +++ b/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java @@ -0,0 +1,47 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.infrastructure; + +import javax.validation.ValidationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +public class ValidationExceptionHandler implements ExceptionMapper { + + private static final String PROBLEM_JSON_TYPE = "application/problem+json"; + private static final String PROBLEM_JSON + = "{\"type\": \"%s\", \"title\": \"%s\", \"status\": %d, \"detail\": \"%s\", \"instance\": \"%s\"}"; + + @Context + private UriInfo uri; + + @Override + public Response toResponse(ValidationException exception) { + return Response.status(Response.Status.BAD_REQUEST) + .type(PROBLEM_JSON_TYPE) + .entity(String.format( + PROBLEM_JSON, + uri.getBaseUri().resolve("/errors/invalid-" + uri.getPathSegments().get(uri.getPathSegments().size() - 1)), + "bad request", Response.Status.BAD_REQUEST.getStatusCode(), + exception.getMessage(), + uri.getAbsolutePath().toString())) + .build(); + } +} diff --git a/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerApplication.java b/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerApplication.java new file mode 100644 index 00000000..0786ef68 --- /dev/null +++ b/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerApplication.java @@ -0,0 +1,26 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.customer.application; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +/** + * Application initialization + */ +@ApplicationPath("/") +public class CustomerApplication extends Application { +} diff --git a/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerResource.java b/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerResource.java new file mode 100644 index 00000000..d28fdbac --- /dev/null +++ b/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerResource.java @@ -0,0 +1,115 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.customer.application; + +import java.net.URISyntaxException; +import java.util.List; +import java.util.function.Supplier; +import java.util.logging.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import de.openknowledge.sample.address.domain.Address; +import de.openknowledge.sample.address.domain.BillingAddressRepository; +import de.openknowledge.sample.address.domain.DeliveryAddressRepository; +import de.openknowledge.sample.customer.domain.Customer; +import de.openknowledge.sample.customer.domain.CustomerNumber; +import de.openknowledge.sample.customer.domain.CustomerRepository; + +/** + * RESTFul endpoint for customers + */ +@ApplicationScoped +@Path("/customers") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class CustomerResource { + + private static final Logger LOG = Logger.getLogger(CustomerResource.class.getSimpleName()); + + @Inject + private CustomerRepository customerRepository; + @Inject + private BillingAddressRepository billingAddressRepository; + @Inject + private DeliveryAddressRepository deliveryAddressRepository; + + @GET + @Path("/") + @Produces(MediaType.APPLICATION_JSON) + public List getCustomers() { + LOG.info("RESTful call 'GET all customers'"); + return customerRepository.findAll(); + } + + @POST + @Path("/") + @Consumes(MediaType.APPLICATION_JSON) + public Response createCustomer(Customer customer, @Context UriInfo uri) throws URISyntaxException { + LOG.info("RESTful call 'POST new customer'"); + customerRepository.persist(customer); + return Response.created(uri.getAbsolutePathBuilder().path(customer.getNumber().toString()).build()).build(); + } + + @GET + @Path("/{customerNumber}") + @Produces(MediaType.APPLICATION_JSON) + public Customer getCustomer(@PathParam("customerNumber") CustomerNumber customerNumber) { + LOG.info("RESTful call 'GET customer'"); + Customer customer = customerRepository.find(customerNumber).orElseThrow(customerNotFound(customerNumber)); + billingAddressRepository.find(customerNumber).ifPresent(customer::setBillingAddress); + deliveryAddressRepository.find(customerNumber).ifPresent(customer::setDeliveryAddress); + return customer; + } + + @PUT + @Path("/{customerNumber}/billing-address") + @Produces(MediaType.APPLICATION_JSON) + public void setBillingAddress(@PathParam("customerNumber") CustomerNumber customerNumber, Address billingAddress) { + LOG.info("RESTful call 'PUT billing address'"); + customerRepository.find(customerNumber).orElseThrow(customerNotFound(customerNumber)); + billingAddressRepository.update(customerNumber, billingAddress); + } + + @PUT + @Path("/{customerNumber}/delivery-address") + @Produces(MediaType.APPLICATION_JSON) + public void setDeliveryAddress( + @PathParam("customerNumber") CustomerNumber customerNumber, + Address deliveryAddress) { + + LOG.info("RESTful call 'PUT delivery address'"); + customerRepository.find(customerNumber).orElseThrow(customerNotFound(customerNumber)); + deliveryAddressRepository.update(customerNumber, deliveryAddress); + } + + private Supplier customerNotFound(CustomerNumber number) { + return () -> new NotFoundException("customer " + number + " not found"); + } +} diff --git a/customer-service/src/main/java/de/openknowledge/sample/customer/domain/Customer.java b/customer-service/src/main/java/de/openknowledge/sample/customer/domain/Customer.java new file mode 100644 index 00000000..06b82bad --- /dev/null +++ b/customer-service/src/main/java/de/openknowledge/sample/customer/domain/Customer.java @@ -0,0 +1,98 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.customer.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.annotation.JsonbCreator; +import javax.json.bind.annotation.JsonbProperty; + +import de.openknowledge.sample.address.domain.Address; + +public class Customer { + + CustomerNumber number; + private CustomerName name; + private Address billingAddress; + private Address deliveryAddress; + + @JsonbCreator + public Customer(@JsonbProperty("name") CustomerName name) { + this.name = notNull(name, "name may not be null"); + } + + public Customer(CustomerNumber number, CustomerName name) { + this.name = notNull(name, "name may not be null"); + this.number = notNull(number, "number may not be null"); + } + + public CustomerName getName() { + return name; + } + + public CustomerNumber getNumber() { + return number; + } + + @JsonbProperty(nillable = false) + public Address getBillingAddress() { + return billingAddress; + } + + public void setBillingAddress(Address billingAddress) { + this.billingAddress = billingAddress; + } + + @JsonbProperty(nillable = false) + public Address getDeliveryAddress() { + return deliveryAddress; + } + + public void setDeliveryAddress(Address deliveryAddress) { + this.deliveryAddress = deliveryAddress; + } + + Customer clearAddresses() { + deliveryAddress = null; + billingAddress = null; + return this; + } + + @Override + public int hashCode() { + return name.hashCode() ^ number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof Customer)) { + return false; + } + + Customer customer = (Customer)object; + + return name.equals(customer.getName()) && number.equals(customer.getNumber()); + } + + @Override + public String toString() { + return name + "(" + number + ")"; + } +} diff --git a/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerName.java b/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerName.java new file mode 100644 index 00000000..44093e2d --- /dev/null +++ b/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerName.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.customer.domain; + +import static org.apache.commons.lang3.Validate.notBlank; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.customer.domain.CustomerName.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class CustomerName { + + private String name; + + public CustomerName(String name) { + this.name = notBlank(name, "name may not be empty").trim(); + } + + protected CustomerName() { + // for frameworks + } + + public static CustomerName valueOf(String value) { + return new CustomerName(value); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof CustomerName)) { + return false; + } + CustomerName customerName = (CustomerName)object; + + return toString().equals(customerName.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public CustomerName adaptFromJson(String name) throws Exception { + return new CustomerName(name); + } + + @Override + public String adaptToJson(CustomerName name) throws Exception { + return name.toString(); + } + } +} diff --git a/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerNumber.java b/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerNumber.java new file mode 100644 index 00000000..d6fbeb6a --- /dev/null +++ b/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerNumber.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.customer.domain; + +import static org.apache.commons.lang3.Validate.notBlank; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.customer.domain.CustomerNumber.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class CustomerNumber { + private String number; + + protected CustomerNumber() { + // for frameworks + } + + public CustomerNumber(String number) { + this.number = notBlank(number, "number may not be empty").trim(); + } + + @Override + public String toString() { + return number; + } + + @Override + public int hashCode() { + return number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof CustomerNumber)) { + return false; + } + + CustomerNumber customerNumber = (CustomerNumber)object; + + return toString().equals(customerNumber.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public CustomerNumber adaptFromJson(String number) throws Exception { + return new CustomerNumber(number); + } + + @Override + public String adaptToJson(CustomerNumber number) throws Exception { + return number.toString(); + } + + } + +} diff --git a/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerRepository.java b/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerRepository.java new file mode 100644 index 00000000..8c079e51 --- /dev/null +++ b/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerRepository.java @@ -0,0 +1,67 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.customer.domain; + +import static java.lang.String.format; +import static java.util.stream.Collectors.toList; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; + +/** + * Addresses repository + */ +@ApplicationScoped +public class CustomerRepository { + + private static final Logger LOGGER = Logger.getLogger(CustomerRepository.class.getSimpleName()); + private static final AtomicInteger CUSTOMER_NUMBERS = new AtomicInteger(0); + + private Map customers; + + @PostConstruct + public void initialize() { + + customers = new ConcurrentHashMap<>(); + customers.put(new CustomerNumber("0815"), + new Customer(new CustomerNumber("0815"), new CustomerName("Max Mustermann"))); + customers.put(new CustomerNumber("0816"), + new Customer(new CustomerNumber("0816"), new CustomerName("Erika Mustermann"))); + customers.put(new CustomerNumber("007"), + new Customer(new CustomerNumber("007"), new CustomerName("James Bond"))); + LOGGER.info(format("customer repository initialized with %d customers: ", customers.size())); + } + + public List findAll() { + return customers.values().stream().map(Customer::clearAddresses).collect(toList()); + } + + public void persist(Customer customer) { + customer.number = new CustomerNumber(Integer.toString(CUSTOMER_NUMBERS.incrementAndGet())); + customers.put(customer.number, customer); + } + + public Optional find(CustomerNumber customerNumber) { + return Optional.ofNullable(customers.get(customerNumber)); + } +} diff --git a/customer-service/src/main/resources/META-INF/microprofile-config.properties b/customer-service/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 00000000..ba06d320 --- /dev/null +++ b/customer-service/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,2 @@ +billing-service.url=http://localhost:4001 +delivery-service.url=http://localhost:4002 diff --git a/customer-service/src/test/java/de/openknowledge/sample/customer/CustomerServiceTest.java b/customer-service/src/test/java/de/openknowledge/sample/customer/CustomerServiceTest.java new file mode 100644 index 00000000..5e1e785c --- /dev/null +++ b/customer-service/src/test/java/de/openknowledge/sample/customer/CustomerServiceTest.java @@ -0,0 +1,209 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.customer; + +import static de.openknowledge.sample.customer.JsonObjectComparision.sameAs; +import static de.openknowledge.sample.customer.JsonObjectComparision.thatIsSameAs; +import static javax.ws.rs.client.Entity.entity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import java.io.StringReader; +import java.net.URI; +import java.util.Optional; + +import javax.inject.Inject; +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.apache.meecrowave.Meecrowave; +import org.apache.meecrowave.junit5.MeecrowaveConfig; +import org.apache.meecrowave.testing.ConfigurationInject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import de.openknowledge.sample.address.domain.Address; +import de.openknowledge.sample.address.domain.BillingAddressRepository; +import de.openknowledge.sample.address.domain.DeliveryAddressRepository; +import de.openknowledge.sample.customer.domain.CustomerNumber; +import rocks.limburg.cdimock.MockitoBeans; + +@MockitoBeans(types = {BillingAddressRepository.class, DeliveryAddressRepository.class}) +@MeecrowaveConfig +public class CustomerServiceTest { + + @ConfigurationInject + private Meecrowave.Builder config; + + @Inject + private BillingAddressRepository billingAddressRepository; + + @Inject + private DeliveryAddressRepository deliveryAddressRepository; + + private URI uri; + + @BeforeEach + public void setUp() { + when(deliveryAddressRepository.find(new CustomerNumber("0815"))) + .thenReturn(Optional.of(Address.of("Max Mustermann").atStreet("Poststrasse 1").inCity("26122 Oldenburg").build())); + when(deliveryAddressRepository.find(new CustomerNumber("0816"))) + .thenReturn(Optional.of(Address.of("Erika Mustermann").atStreet("II. Hagen 7").inCity("45127 Essen").build())); + + when(billingAddressRepository.find(new CustomerNumber("0815"))) + .thenReturn(Optional.of(Address.of("Max Mustermann").atStreet("Poststrasse 1").inCity("26122 Oldenburg").build())); + when(billingAddressRepository.find(new CustomerNumber("007"))) + .thenReturn(Optional.of(Address.of("Sherlock Holmes").atStreet("221B Baker Street").inCity("London NW1 6XE").build())); + + uri = URI.create("http://localhost:" + config.getHttpPort()); + } + + @Test + public void getCustomers() { + JsonArray result = Json.createReader(new StringReader(ClientBuilder + .newClient() + .target(uri) + .path("customers") + .request(MediaType.APPLICATION_JSON) + .get() + .readEntity(String.class))) + .readArray(); + assertThat(result).haveAtLeastOne(thatIsSameAs(getClass().getResourceAsStream("max.json"))); + assertThat(result).haveAtLeastOne(thatIsSameAs(getClass().getResourceAsStream("erika.json"))); + assertThat(result).haveAtLeastOne(thatIsSameAs(getClass().getResourceAsStream("james.json"))); + } + + @Test + public void getCustomerWithAddresses() { + JsonObject result = Json.createReader(new StringReader(ClientBuilder + .newClient() + .target(uri) + .path("customers") + .path("0815") + .request(MediaType.APPLICATION_JSON) + .get() + .readEntity(String.class))) + .readObject(); + assertThat(result).is(sameAs(getClass().getResourceAsStream("max-with-addresses.json"))); + } + + @Test + public void createCustomer() { + Response response = ClientBuilder + .newClient() + .target(uri) + .path("customers") + .request(MediaType.APPLICATION_JSON) + .post(entity(getClass().getResourceAsStream("sherlock.json"), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(Response.Status.CREATED.getStatusCode()); + + JsonObject result = Json.createReader(new StringReader(ClientBuilder + .newClient() + .target(response.getLocation()) + .request(MediaType.APPLICATION_JSON) + .get() + .readEntity(String.class))) + .readObject(); + assertThat(result).is(sameAs(getClass().getResourceAsStream("sherlock.json"))); + + JsonArray customers = Json.createReader(new StringReader(ClientBuilder + .newClient() + .target(uri) + .path("customers") + .request(MediaType.APPLICATION_JSON) + .get() + .readEntity(String.class))) + .readArray(); + assertThat(customers).haveAtLeastOne(thatIsSameAs(getClass().getResourceAsStream("sherlock.json"))); + } + + @Test + public void getCustomerWithBillingAddress() { + JsonObject result = Json.createReader(new StringReader(ClientBuilder + .newClient() + .target(uri) + .path("customers") + .path("007") + .request(MediaType.APPLICATION_JSON) + .get() + .readEntity(String.class))) + .readObject(); + assertThat(result).is(sameAs(getClass().getResourceAsStream("james-with-addresses.json"))); + } + + @Test + public void getCustomerWithDeliveryAddress() { + JsonObject result = Json.createReader(new StringReader(ClientBuilder + .newClient() + .target(uri) + .path("customers") + .path("0816") + .request(MediaType.APPLICATION_JSON) + .get() + .readEntity(String.class))) + .readObject(); + assertThat(result).is(sameAs(getClass().getResourceAsStream("erika-with-addresses.json"))); + } + + @Test + public void setBillingAddressOfExistingCustomer() { + Response response = ClientBuilder + .newClient() + .target(uri) + .path("customers/0816/billing-address") + .request(MediaType.APPLICATION_JSON) + .put(entity(getClass().getResourceAsStream("sherlock-address.json"), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(Response.Status.NO_CONTENT.getStatusCode()); + } + + @Test + public void setBillingAddressOfNonExistingCustomerFails() { + Response response = ClientBuilder + .newClient() + .target(uri) + .path("customers/0817/billing-address") + .request(MediaType.APPLICATION_JSON) + .put(entity(getClass().getResourceAsStream("sherlock-address.json"), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(Response.Status.NOT_FOUND.getStatusCode()); + } + + + @Test + public void setDeliveryAddressOfExistingCustomer() { + Response response = ClientBuilder + .newClient() + .target(uri) + .path("customers/0816/delivery-address") + .request(MediaType.APPLICATION_JSON) + .put(entity(getClass().getResourceAsStream("sherlock-address.json"), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(Response.Status.NO_CONTENT.getStatusCode()); + } + + @Test + public void setDeliveryAddressOfNonExistingCustomerFails() { + Response response = ClientBuilder + .newClient() + .target(uri) + .path("customers/0817/delivery-address") + .request(MediaType.APPLICATION_JSON) + .put(entity(getClass().getResourceAsStream("sherlock-address.json"), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(Response.Status.NOT_FOUND.getStatusCode()); + } +} diff --git a/customer-service/src/test/java/de/openknowledge/sample/customer/JsonObjectComparision.java b/customer-service/src/test/java/de/openknowledge/sample/customer/JsonObjectComparision.java new file mode 100644 index 00000000..b2f4cf26 --- /dev/null +++ b/customer-service/src/test/java/de/openknowledge/sample/customer/JsonObjectComparision.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019 - 2025 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.customer; + +import java.io.InputStream; +import java.util.Map; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonValue; + +import org.assertj.core.api.Condition; + +public class JsonObjectComparision extends Condition> { + + public JsonObjectComparision(JsonObject object) { + super(v -> v.entrySet().containsAll(object.entrySet()), "object containing %s", object); + } + + public static Condition> sameAs(InputStream in) { + return new JsonObjectComparision(Json.createReader(in).readObject()); + } + + public static Condition thatIsSameAs(InputStream in) { + Condition condition = new JsonObjectComparision(Json.createReader(in).readObject()); + return (Condition)condition; + } +} diff --git a/customer-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java b/customer-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java new file mode 100644 index 00000000..11bbc834 --- /dev/null +++ b/customer-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java @@ -0,0 +1,35 @@ +/* + * Copyright 2019 - 2025 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.infrastructure; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.enterprise.inject.Alternative; +import javax.enterprise.inject.Stereotype; + +@Stereotype +@Alternative +@Retention(RUNTIME) +@Target({TYPE, METHOD, FIELD}) +public @interface CdiMock { + +} diff --git a/customer-service/src/test/resources/META-INF/beans.xml b/customer-service/src/test/resources/META-INF/beans.xml new file mode 100644 index 00000000..4c50327c --- /dev/null +++ b/customer-service/src/test/resources/META-INF/beans.xml @@ -0,0 +1,11 @@ + + + + rocks.limburg.cdimock.CdiMock + + diff --git a/customer-service/src/test/resources/de/openknowledge/sample/customer/erika-with-addresses.json b/customer-service/src/test/resources/de/openknowledge/sample/customer/erika-with-addresses.json new file mode 100644 index 00000000..c44ac674 --- /dev/null +++ b/customer-service/src/test/resources/de/openknowledge/sample/customer/erika-with-addresses.json @@ -0,0 +1,12 @@ +{ + "name": "Erika Mustermann", + "number": "0816", + "deliveryAddress": { + "recipient": "Erika Mustermann", + "street": { + "name": "II. Hagen", + "number": "7" + }, + "city": "45127 Essen" + } +} diff --git a/customer-service/src/test/resources/de/openknowledge/sample/customer/erika.json b/customer-service/src/test/resources/de/openknowledge/sample/customer/erika.json new file mode 100644 index 00000000..6b677ea6 --- /dev/null +++ b/customer-service/src/test/resources/de/openknowledge/sample/customer/erika.json @@ -0,0 +1,4 @@ +{ + "name": "Erika Mustermann", + "number": "0816" +} diff --git a/customer-service/src/test/resources/de/openknowledge/sample/customer/james-with-addresses.json b/customer-service/src/test/resources/de/openknowledge/sample/customer/james-with-addresses.json new file mode 100644 index 00000000..b43e0fcd --- /dev/null +++ b/customer-service/src/test/resources/de/openknowledge/sample/customer/james-with-addresses.json @@ -0,0 +1,12 @@ +{ + "name": "James Bond", + "number": "007", + "billingAddress": { + "recipient": "Sherlock Holmes", + "street": { + "name": "Baker Street", + "number": "221B" + }, + "city": "London NW1 6XE" + } +} diff --git a/customer-service/src/test/resources/de/openknowledge/sample/customer/james.json b/customer-service/src/test/resources/de/openknowledge/sample/customer/james.json new file mode 100644 index 00000000..368ad803 --- /dev/null +++ b/customer-service/src/test/resources/de/openknowledge/sample/customer/james.json @@ -0,0 +1,4 @@ +{ + "name": "James Bond", + "number": "007" +} diff --git a/customer-service/src/test/resources/de/openknowledge/sample/customer/max-with-addresses.json b/customer-service/src/test/resources/de/openknowledge/sample/customer/max-with-addresses.json new file mode 100644 index 00000000..743b7e0b --- /dev/null +++ b/customer-service/src/test/resources/de/openknowledge/sample/customer/max-with-addresses.json @@ -0,0 +1,20 @@ +{ + "name": "Max Mustermann", + "number": "0815", + "billingAddress": { + "recipient": "Max Mustermann", + "street": { + "name": "Poststrasse", + "number": "1" + }, + "city": "26122 Oldenburg" + }, + "deliveryAddress": { + "recipient": "Max Mustermann", + "street": { + "name": "Poststrasse", + "number": "1" + }, + "city": "26122 Oldenburg" + } +} diff --git a/customer-service/src/test/resources/de/openknowledge/sample/customer/max.json b/customer-service/src/test/resources/de/openknowledge/sample/customer/max.json new file mode 100644 index 00000000..872bbff4 --- /dev/null +++ b/customer-service/src/test/resources/de/openknowledge/sample/customer/max.json @@ -0,0 +1,4 @@ +{ + "name": "Max Mustermann", + "number": "0815" +} diff --git a/customer-service/src/test/resources/de/openknowledge/sample/customer/sherlock-address.json b/customer-service/src/test/resources/de/openknowledge/sample/customer/sherlock-address.json new file mode 100644 index 00000000..481cc898 --- /dev/null +++ b/customer-service/src/test/resources/de/openknowledge/sample/customer/sherlock-address.json @@ -0,0 +1,8 @@ +{ + "recipient": "Sherlock Holmes", + "street": { + "name": "Baker Street", + "number": "221B" + }, + "city": "London NW1 6XE" +} diff --git a/customer-service/src/test/resources/de/openknowledge/sample/customer/sherlock.json b/customer-service/src/test/resources/de/openknowledge/sample/customer/sherlock.json new file mode 100644 index 00000000..84e0f5f0 --- /dev/null +++ b/customer-service/src/test/resources/de/openknowledge/sample/customer/sherlock.json @@ -0,0 +1,3 @@ +{ + "name": "Sherlock Holmes" +} diff --git a/delivery-service/.gitignore b/delivery-service/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/delivery-service/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/delivery-service/Dockerfile b/delivery-service/Dockerfile new file mode 100644 index 00000000..4318462a --- /dev/null +++ b/delivery-service/Dockerfile @@ -0,0 +1,17 @@ +FROM maven:3.9.9-eclipse-temurin-21 AS mvn + +WORKDIR /usr/src/online-shop +COPY pom.xml ./ +RUN mvn package dependency:go-offline # cache dependencies +COPY src ./src +RUN mvn clean package meecrowave:bundle -Dservice.name=delivery-service -Dmaven.test.skip=true -Dcheckstyle.skip=true + +FROM eclipse-temurin:21-jre + +RUN apt-get update -qq && apt-get install --no-install-recommends -qqy unzip && rm -rf /var/lib/apt/lists/* +COPY --from=mvn /usr/src/online-shop/target/delivery-service-meecrowave-distribution.zip /opt/delivery-service.zip +WORKDIR /opt +RUN unzip delivery-service.zip && rm delivery-service.zip +EXPOSE 8080 + +ENTRYPOINT ["/opt/delivery-service-distribution/bin/meecrowave.sh", "run"] diff --git a/delivery-service/pom.xml b/delivery-service/pom.xml new file mode 100644 index 00000000..f21e858d --- /dev/null +++ b/delivery-service/pom.xml @@ -0,0 +1,212 @@ + + + + + 4.0.0 + + de.openknowledge.sample.shop + ${service.name} + 1.0.0-SNAPSHOT + + + online-shop + 17 + 17 + false + UTF-8 + 1.2.15 + 1.9.6 + 5.8.2 + + + + + + org.apache.meecrowave + meecrowave-specs-api + ${meecrowave.version} + provided + + + org.apache.meecrowave + meecrowave-core + ${meecrowave.version} + provided + + + org.apache.geronimo.specs + geronimo-validation_1.0_spec + 1.1 + + + org.eclipse.microprofile.config + microprofile-config-api + 1.4 + + + org.apache.geronimo.config + geronimo-config-impl + 1.2.2 + + + org.eclipse.microprofile.jwt + microprofile-jwt-auth-api + 1.2.1 + + + org.apache.geronimo + geronimo-jwt-auth + 1.0.4 + + + org.apache.deltaspike.modules + deltaspike-security-module-api + ${deltaspike.version} + compile + + + org.apache.deltaspike.modules + deltaspike-security-module-impl + ${deltaspike.version} + runtime + + + org.eclipse.microprofile.opentracing + microprofile-opentracing-api + 1.2 + + + io.opentracing + opentracing-api + 0.31.0 + + + org.apache.geronimo + geronimo-opentracing + 1.0.3 + + + org.eclipse.microprofile.health + microprofile-health-api + 3.0-RC3 + + + org.apache.geronimo + geronimo-health + 2.0.1 + + + org.microprofile-ext.health-ext + healthprobe-jvm + 1.2.0 + + + org.microprofile-ext.health-ext + healthprobe-system + 1.2.0 + + + org.eclipse.microprofile.metrics + microprofile-metrics-api + 3.0-RC2 + + + org.apache.geronimo + geronimo-metrics + 1.0.6 + + + org.apache.commons + commons-lang3 + 3.9 + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.assertj + assertj-core + 3.22.0 + test + + + org.mockito + mockito-core + 2.28.2 + test + + + org.apache.meecrowave + meecrowave-junit + ${meecrowave.version} + test + + + rocks.limburg.cdimock + cdimock + 1.0.4 + test + + + com.tngtech.archunit + archunit + 1.0.1 + test + + + + ${project.artifactId} + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + org.apache.meecrowave + meecrowave-maven-plugin + ${meecrowave.version} + + + maven-checkstyle-plugin + 3.2.0 + + true + ${project.basedir}/src/main/checkstyle/java.header + true + true + + + + compile + compile + + checkstyle + + + ${project.basedir}/src/main/checkstyle/configuration.xml + true + + + + + + + diff --git a/delivery-service/src/main/checkstyle/configuration.xml b/delivery-service/src/main/checkstyle/configuration.xml new file mode 100644 index 00000000..3392d8cf --- /dev/null +++ b/delivery-service/src/main/checkstyle/configuration.xml @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/delivery-service/src/main/checkstyle/java.header b/delivery-service/src/main/checkstyle/java.header new file mode 100644 index 00000000..baba0272 --- /dev/null +++ b/delivery-service/src/main/checkstyle/java.header @@ -0,0 +1,15 @@ +/* + * Copyright \d{4}( - \d{4}|(, \d{4})*) [A-Za-z ,\-]. + * + * Licensed under the Apache License, Version 2\.0 \(the "License"\); + * you may not use this file except in compliance with the License\. + * You may obtain a copy of the License at + * + * http://www\.apache\.org/licenses/LICENSE-2\.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\. + * See the License for the specific language governing permissions and + * limitations under the License\. + */ diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesApplication.java b/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesApplication.java new file mode 100644 index 00000000..5f442dc6 --- /dev/null +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesApplication.java @@ -0,0 +1,26 @@ +/* + * Copyright 2019 - 2025 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.application; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +/** + * Application initialization + */ +@ApplicationPath("/") +public class AddressesApplication extends Application { +} diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesResource.java b/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesResource.java new file mode 100644 index 00000000..3155f8d8 --- /dev/null +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesResource.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 -2025 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.application; + +import java.util.logging.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import de.openknowledge.sample.address.domain.Address; +import de.openknowledge.sample.address.domain.AddressRepository; +import de.openknowledge.sample.address.domain.AddressValidationService; +import de.openknowledge.sample.address.domain.CustomerNumber; + +/** + * RESTFul endpoint for delivery addresses + */ +@ApplicationScoped +@Path("/delivery-addresses") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class AddressesResource { + + private static final Logger LOG = Logger.getLogger(AddressesResource.class.getSimpleName()); + + @Inject + private AddressValidationService addressValidationService; + @Inject + private AddressRepository addressRepository; + + @GET + @Path("/{customerNumber}") + @Produces(MediaType.APPLICATION_JSON) + public Address getAddress(@PathParam("customerNumber") CustomerNumber number) { + LOG.info("RESTful call 'GET address'"); + return addressRepository.find(number).orElseThrow(NotFoundException::new); + } + + @POST + @Path("/{customerNumber}") + @Consumes(MediaType.APPLICATION_JSON) + public Response setAddress( + @PathParam("customerNumber") CustomerNumber customerNumber, + Address address, + @Context UriInfo uri) { + + LOG.info("RESTful call 'POST address'"); + addressValidationService.validate(address); + addressRepository.update(customerNumber, address); + return Response.ok().build(); + } +} diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Address.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Address.java new file mode 100644 index 00000000..f650f7f5 --- /dev/null +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Address.java @@ -0,0 +1,106 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static java.util.Optional.ofNullable; +import static org.apache.commons.lang3.Validate.notNull; + +import java.util.Objects; + +import javax.json.bind.annotation.JsonbCreator; +import javax.json.bind.annotation.JsonbProperty; + +public class Address { + private Recipient recipient; + private Street street; + private City city; + + @JsonbCreator + public Address(@JsonbProperty("recipient") Recipient recipient) { + this.recipient = notNull(recipient, "recipient may not be null"); + } + + public Address(Recipient recipient, Street street, City city) { + this(recipient); + setStreet(street); + setCity(city); + } + + public Recipient getRecipient() { + return recipient; + } + + public Street getStreet() { + return street; + } + + public void setStreet(Street street) { + this.street = street; + } + + public City getCity() { + return city; + } + + public void setCity(City city) { + this.city = city; + } + + @Override + public int hashCode() { + return recipient.hashCode() + ofNullable(street).map(Object::hashCode).orElse(0) + ofNullable(city).map(Object::hashCode).orElse(0); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || !getClass().equals(object.getClass())) { + return false; + } + Address address = (Address)object; + return recipient.equals(recipient) && Objects.equals(street, address.street) && Objects.equals(city, address.city); + } + + public static Builder of(String recipient) { + return new Builder(new Recipient(recipient)); + } + + public static class Builder { + + private Address address; + + private Builder(Recipient recipient) { + address = new Address(recipient); + } + + public Builder atStreet(String name) { + AddressLine addressLine = new AddressLine(name); + address.setStreet(new Street(addressLine.getStreetName(), addressLine.getHouseNumber())); + return this; + } + + public Builder inCity(String city) { + address.setCity(new City(city)); + return this; + } + + public Address build() { + return address; + } + } +} diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java new file mode 100644 index 00000000..864e5884 --- /dev/null +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java @@ -0,0 +1,115 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.AddressLine.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class AddressLine { + + public static final AddressLine EMPTY = new AddressLine(""); + + private String line; + + protected AddressLine() { + // for frameworks + } + + public AddressLine(String line) { + this.line = notNull(line, "line may not be null").trim(); + } + + public StreetName getStreetName() { + String firstSegment = line.substring(0, line.indexOf(' ')); + String lastSegment = line.substring(line.lastIndexOf(' ') + 1); + if (containsDigit(lastSegment)) { + return new StreetName(line.substring(0, line.length() - lastSegment.length())); + } else if (containsDigit(firstSegment)) { + return new StreetName(line.substring(firstSegment.length())); + } else { + throw new IllegalStateException("Could not determine street name"); + } + } + + public HouseNumber getHouseNumber() { + String firstSegment = line.substring(0, line.indexOf(' ')); + String lastSegment = line.substring(line.lastIndexOf(' ') + 1); + if (containsDigit(lastSegment)) { + return new HouseNumber(lastSegment); + } else if (containsDigit(firstSegment)) { + return new HouseNumber(firstSegment); + } else { + throw new IllegalStateException("Could not determine house number"); + } + } + + @Override + public String toString() { + return line; + } + + + @Override + public int hashCode() { + return line.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof AddressLine)) { + return false; + } + + AddressLine recipient = (AddressLine)object; + + return toString().equals(recipient.toString()); + } + + private boolean containsDigit(String name) { + return name.contains("0") + || name.contains("1") + || name.contains("2") + || name.contains("3") + || name.contains("4") + || name.contains("5") + || name.contains("6") + || name.contains("7") + || name.contains("8") + || name.contains("9"); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public AddressLine adaptFromJson(String name) throws Exception { + return new AddressLine(name); + } + + @Override + public String adaptToJson(AddressLine name) throws Exception { + return name.toString(); + } + } +} diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java new file mode 100644 index 00000000..a6b5a991 --- /dev/null +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java @@ -0,0 +1,58 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static java.lang.String.format; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; + +/** + * Addresses repository + */ +@ApplicationScoped +public class AddressRepository { + + private static final Logger LOGGER = Logger.getLogger(AddressRepository.class.getSimpleName()); + + private Map addresses; + + @PostConstruct + public void initialize() { + + addresses = new ConcurrentHashMap<>(); + + addresses.put(new CustomerNumber("0815"), new Address(new Recipient("Max Mustermann"), + new Street(new StreetName("Poststr."), new HouseNumber("1")), new City("26122 Oldenburg"))); + + addresses.put(new CustomerNumber("0816"), new Address(new Recipient("Erika Mustermann"), + new Street(new StreetName("II. Hagen"), new HouseNumber("7")), new City("45127 Essen"))); + LOGGER.info(format("address repository initialized with %d addresses: ", addresses.size())); + } + + public Optional
find(CustomerNumber number) { + return Optional.ofNullable(addresses.get(number)); + } + + public void update(CustomerNumber number, Address address) { + Optional.ofNullable(address).ifPresent(a -> addresses.put(number, a)); + } +} diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressValidationService.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressValidationService.java new file mode 100644 index 00000000..56bfff61 --- /dev/null +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressValidationService.java @@ -0,0 +1,59 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static javax.ws.rs.client.Entity.entity; + +import java.io.StringReader; +import java.util.logging.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.json.Json; +import javax.json.JsonObject; +import javax.validation.ValidationException; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.apache.johnzon.jaxrs.jsonb.jaxrs.JsonbJaxrsProvider; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@ApplicationScoped +public class AddressValidationService { + + private static final Logger LOG = Logger.getLogger(AddressValidationService.class.getSimpleName()); + private static final String ADDRESS_VALIDATION_PATH = "valid-addresses"; + + @Inject + @ConfigProperty(name = "address-validation-service.url") + String addressValidationServiceUrl; + + public void validate(Address address) { + Response validationResult = ClientBuilder + .newClient() + .register(JsonbJaxrsProvider.class) + .target(addressValidationServiceUrl) + .path(ADDRESS_VALIDATION_PATH) + .request(MediaType.APPLICATION_JSON) + .post(entity(address, MediaType.APPLICATION_JSON_TYPE)); + if (validationResult.getStatus() != Response.Status.OK.getStatusCode()) { + LOG.info("validation failed"); + JsonObject problem = Json.createReader(new StringReader(validationResult.readEntity(String.class))).readObject(); + throw new ValidationException(problem.getString("detail")); + } + } +} diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/City.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/City.java new file mode 100644 index 00000000..cc521fe9 --- /dev/null +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/City.java @@ -0,0 +1,113 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.City.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class City { + + private String name; + + public City(String name) { + this.name = notNull(name, "name may not be empty").trim(); + } + + protected City() { + // for framework + } + + public ZipCode getZipCode() { + String firstSegment = name.substring(0, name.indexOf(' ')); + String lastSegment = name.substring(name.lastIndexOf(' ') + 1); + if (containsDigit(firstSegment)) { + return new ZipCode(firstSegment); + } else if (containsDigit(lastSegment)) { + return new ZipCode(name.substring(firstSegment.length())); + } else { + throw new IllegalStateException("Could not determine zip code"); + } + } + + public CityName getCityName() { + String firstSegment = name.substring(0, name.indexOf(' ')); + String lastSegment = name.substring(name.lastIndexOf(' ') + 1); + if (containsDigit(firstSegment)) { + return new CityName(name.substring(firstSegment.length())); + } else if (containsDigit(lastSegment)) { + return new CityName(name.substring(0, firstSegment.length())); + } else { + throw new IllegalStateException("Could not determine city name"); + } + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + City city = (City)object; + + return toString().equals(city.toString()); + } + + private boolean containsDigit(String cityName) { + return cityName.contains("0") + || cityName.contains("1") + || cityName.contains("2") + || cityName.contains("3") + || cityName.contains("4") + || cityName.contains("5") + || cityName.contains("6") + || cityName.contains("7") + || cityName.contains("8") + || cityName.contains("9"); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public City adaptFromJson(String name) throws Exception { + return new City(name); + } + + @Override + public String adaptToJson(City name) throws Exception { + return name.toString(); + } + } + +} diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java new file mode 100644 index 00000000..1fea0874 --- /dev/null +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.CityName.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class CityName { + + private String name; + + public CityName(String name) { + this.name = notNull(name, "name may not be empty").trim(); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + CityName city = (CityName)object; + + return toString().equals(city.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public CityName adaptFromJson(String name) throws Exception { + return new CityName(name); + } + + @Override + public String adaptToJson(CityName name) throws Exception { + return name.toString(); + } + } + +} diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java new file mode 100644 index 00000000..2cf9dec8 --- /dev/null +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notBlank; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.CustomerNumber.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class CustomerNumber { + + private String number; + + protected CustomerNumber() { + // for frameworks + } + + public CustomerNumber(String number) { + this.number = notBlank(number, "number may not be empty").trim(); + } + + @Override + public String toString() { + return number; + } + + @Override + public int hashCode() { + return number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof CustomerNumber)) { + return false; + } + + CustomerNumber customerNumber = (CustomerNumber)object; + + return toString().equals(customerNumber.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public CustomerNumber adaptFromJson(String number) throws Exception { + return new CustomerNumber(number); + } + + @Override + public String adaptToJson(CustomerNumber number) throws Exception { + return number.toString(); + } + + } + +} diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java new file mode 100644 index 00000000..e54c14c7 --- /dev/null +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java @@ -0,0 +1,81 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notBlank; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.HouseNumber.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class HouseNumber { + + private String number; + + protected HouseNumber() { + // for frameworks + } + + public HouseNumber(String number) { + this.number = notBlank(number, "number may not be empty").trim(); + } + + public static HouseNumber valueOf(String number) { + return new HouseNumber(number); + } + + @Override + public String toString() { + return number; + } + + @Override + public int hashCode() { + return number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof HouseNumber)) { + return false; + } + + HouseNumber houseNumber = (HouseNumber)object; + + return toString().equals(houseNumber.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public HouseNumber adaptFromJson(String number) throws Exception { + return new HouseNumber(number); + } + + @Override + public String adaptToJson(HouseNumber number) throws Exception { + return number.toString(); + } + + } + +} diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Location.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Location.java new file mode 100644 index 00000000..2152f801 --- /dev/null +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Location.java @@ -0,0 +1,66 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.annotation.JsonbCreator; +import javax.json.bind.annotation.JsonbProperty; + +public class Location { + + private ZipCode zipCode; + private CityName cityName; + + @JsonbCreator + public Location(@JsonbProperty("zipCode") ZipCode zipCode, @JsonbProperty("cityName") CityName city) { + this.zipCode = notNull(zipCode, "zip code may not be null"); + this.cityName = notNull(city, "city name may not be null"); + } + + public ZipCode getZipCode() { + return zipCode; + } + + public CityName getCityName() { + return cityName; + } + + @Override + public int hashCode() { + return zipCode.hashCode() ^ cityName.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof Location)) { + return false; + } + + Location location = (Location)object; + + return zipCode.equals(location.zipCode) && cityName.equals(location.cityName); + } + + @Override + public String toString() { + return zipCode.isGerman() ? zipCode + " " + cityName : cityName + " " + zipCode; + } +} diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java new file mode 100644 index 00000000..cbf39d97 --- /dev/null +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java @@ -0,0 +1,81 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notBlank; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.Recipient.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class Recipient { + + private String name; + + protected Recipient() { + // for frameworks + } + + public Recipient(String name) { + this.name = notBlank(name, "name may not be empty").trim(); + } + + public static Recipient valueOf(String name) { + return new Recipient(name); + } + + @Override + public String toString() { + return name; + } + + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof Recipient)) { + return false; + } + + Recipient recipient = (Recipient)object; + + return toString().equals(recipient.toString()); + } + + + public static class Adapter implements JsonbAdapter { + + @Override + public Recipient adaptFromJson(String name) throws Exception { + return new Recipient(name); + } + + @Override + public String adaptToJson(Recipient name) throws Exception { + return name.toString(); + } + } +} diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Street.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Street.java new file mode 100644 index 00000000..bc0af1cd --- /dev/null +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Street.java @@ -0,0 +1,74 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.annotation.JsonbCreator; +import javax.json.bind.annotation.JsonbProperty; + +public class Street { + + private StreetName name; + private HouseNumber number; + + @JsonbCreator + public Street(@JsonbProperty("name") StreetName name, @JsonbProperty("number") HouseNumber houseNumber) { + this.name = notNull(name, "name may not be null"); + this.number = notNull(houseNumber, "house number may not be null"); + } + + public StreetName getName() { + return name; + } + + public HouseNumber getNumber() { + return number; + } + + @Override + public int hashCode() { + return name.hashCode() ^ number.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof Street)) { + return false; + } + + Street street = (Street)object; + + return name.equals(street.getName()) && number.equals(street.getNumber()); + } + + @Override + public String toString() { + if (isEnglish()) { + return number + " " + name; + } else { + return name + " " + number; + } + } + + private boolean isEnglish() { + return name.toString().toLowerCase().contains("street"); + } +} diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java new file mode 100644 index 00000000..16e7ae01 --- /dev/null +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notBlank; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.StreetName.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class StreetName { + + private String name; + + protected StreetName() { + // for frameworks + } + + public StreetName(String name) { + this.name = notBlank(name, "name may not be empty").trim(); + } + + public static StreetName valueOf(String name) { + return new StreetName(name); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof StreetName streetName)) { + return false; + } + + return toString().equals(streetName.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public StreetName adaptFromJson(String name) throws Exception { + return new StreetName(name); + } + + @Override + public String adaptToJson(StreetName name) throws Exception { + return name.toString(); + } + } +} diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java new file mode 100644 index 00000000..1e62563b --- /dev/null +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.domain; + +import static org.apache.commons.lang3.Validate.notNull; + +import javax.json.bind.adapter.JsonbAdapter; +import javax.json.bind.annotation.JsonbTypeAdapter; + +import de.openknowledge.sample.address.domain.ZipCode.Adapter; + +@JsonbTypeAdapter(Adapter.class) +public class ZipCode { + + private String code; + + public ZipCode(String code) { + this.code = notNull(code, "code may not be empty").trim(); + } + + public boolean isGerman() { + return code.length() == 5; + } + + @Override + public String toString() { + return code; + } + + @Override + public int hashCode() { + return code.hashCode(); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (object == null || getClass() != object.getClass()) { + return false; + } + + ZipCode zipCode = (ZipCode)object; + + return toString().equals(zipCode.toString()); + } + + public static class Adapter implements JsonbAdapter { + + @Override + public ZipCode adaptFromJson(String zip) throws Exception { + return new ZipCode(zip); + } + + @Override + public String adaptToJson(ZipCode zip) throws Exception { + return zip.toString(); + } + } + +} diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java b/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java new file mode 100644 index 00000000..8a84c0f2 --- /dev/null +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.infrastructure; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.ext.Provider; + +/** + * Filter to allow cross origin calls. + */ +@Provider +public class CorsFilter implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { + responseContext.getHeaders().add("Access-Control-Allow-Origin", "*"); + responseContext.getHeaders().add("Access-Control-Allow-Headers", "origin, content-type, accept, authorization"); + responseContext.getHeaders().add("Access-Control-Allow-Credentials", "true"); + responseContext.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD"); + responseContext.getHeaders().add("Access-Control-Max-Age", "1209600"); + } +} diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java b/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java new file mode 100644 index 00000000..fa5849f4 --- /dev/null +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java @@ -0,0 +1,49 @@ +/* + * Copyright 2019 - 2025 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.infrastructure; + +import javax.enterprise.context.ApplicationScoped; +import javax.validation.ValidationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +@ApplicationScoped +public class ValidationExceptionHandler implements ExceptionMapper { + + private static final String PROBLEM_JSON_TYPE = "application/problem+json"; + private static final String PROBLEM_JSON + = "{\"type\": \"%s\", \"title\": \"%s\", \"status\": %d, \"detail\": \"%s\", \"instance\": \"%s\"}"; + + @Context + private UriInfo uri; + + @Override + public Response toResponse(ValidationException exception) { + return Response.status(Response.Status.BAD_REQUEST) + .type(PROBLEM_JSON_TYPE) + .entity(String.format( + PROBLEM_JSON, + uri.getBaseUri().resolve("/errors/invalid-" + uri.getPathSegments().get(uri.getPathSegments().size() - 1)), + "bad request", Response.Status.BAD_REQUEST.getStatusCode(), + exception.getMessage(), + uri.getAbsolutePath().toString())) + .build(); + } +} diff --git a/delivery-service/src/main/resources/META-INF/microprofile-config.properties b/delivery-service/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 00000000..26b88bb3 --- /dev/null +++ b/delivery-service/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1 @@ +address-validation-service.url=http://localhost:4003 diff --git a/delivery-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java b/delivery-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java new file mode 100644 index 00000000..11bbc834 --- /dev/null +++ b/delivery-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java @@ -0,0 +1,35 @@ +/* + * Copyright 2019 - 2025 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.infrastructure; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.enterprise.inject.Alternative; +import javax.enterprise.inject.Stereotype; + +@Stereotype +@Alternative +@Retention(RUNTIME) +@Target({TYPE, METHOD, FIELD}) +public @interface CdiMock { + +} diff --git a/delivery-service/src/test/pacts/customer-service-delivery-service.json b/delivery-service/src/test/pacts/customer-service-delivery-service.json new file mode 100644 index 00000000..bea79489 --- /dev/null +++ b/delivery-service/src/test/pacts/customer-service-delivery-service.json @@ -0,0 +1,180 @@ +{ + "provider": { + "name": "delivery-service" + }, + "consumer": { + "name": "customer-service" + }, + "interactions": [ + { + "description": "GET request for 0815", + "request": { + "method": "GET", + "path": "/delivery-addresses/0815" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json; charset=UTF-8" + }, + "body": { + "city": "26122 Oldenburg", + "street": { + "number": "1", + "name": "Poststr." + }, + "recipient": "Max Mustermann" + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/json(;\\s?charset=[\\w\\-]+)?" + } + ], + "combine": "AND" + } + } + } + }, + "providerStates": [ + { + "name": "Three customers" + } + ] + }, + { + "description": "GET request for 0817", + "request": { + "method": "GET", + "path": "/delivery-addresses/0817" + }, + "response": { + "status": 404 + }, + "providerStates": [ + { + "name": "Three customers" + } + ] + }, + { + "description": "POST request for 007", + "request": { + "method": "POST", + "path": "/delivery-addresses/007", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "city": "London NW1 6XE", + "street": { + "number": "221B", + "name": "Baker Street" + }, + "recipient": "Sherlock Holmes" + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/json.*" + } + ], + "combine": "AND" + } + } + } + }, + "response": { + "status": 400, + "headers": { + "Content-Type": "application/problem+json" + }, + "body": { + "detail": "Addresses from UK are not supported for delivery" + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/problem\\+json.*" + } + ], + "combine": "AND" + } + }, + "body": { + "$.detail": { + "matchers": [ + { + "match": "regex", + "regex": ".*" + } + ], + "combine": "AND" + } + } + } + }, + "providerStates": [ + { + "name": "Three customers" + } + ] + }, + { + "description": "POST request for 0815", + "request": { + "method": "POST", + "path": "/delivery-addresses/0815", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "city": "45127 Essen", + "street": { + "number": "7", + "name": "II. Hagen" + }, + "recipient": "Erika Mustermann" + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/json.*" + } + ], + "combine": "AND" + } + } + } + }, + "response": { + "status": 200 + }, + "providerStates": [ + { + "name": "Three customers" + } + ] + } + ], + "metadata": { + "pactSpecification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "4.1.7" + } + } +} diff --git a/delivery-service/src/test/resources/de/openknowledge/sample/address/007-invalid.json b/delivery-service/src/test/resources/de/openknowledge/sample/address/007-invalid.json new file mode 100644 index 00000000..481cc898 --- /dev/null +++ b/delivery-service/src/test/resources/de/openknowledge/sample/address/007-invalid.json @@ -0,0 +1,8 @@ +{ + "recipient": "Sherlock Holmes", + "street": { + "name": "Baker Street", + "number": "221B" + }, + "city": "London NW1 6XE" +} diff --git a/delivery-service/src/test/resources/de/openknowledge/sample/address/0815-new.json b/delivery-service/src/test/resources/de/openknowledge/sample/address/0815-new.json new file mode 100644 index 00000000..9541839a --- /dev/null +++ b/delivery-service/src/test/resources/de/openknowledge/sample/address/0815-new.json @@ -0,0 +1,8 @@ +{ + "recipient": "Erika Mustermann", + "street": { + "name": "II. Hagen", + "number": "7" + }, + "city": "45127 Essen" +} \ No newline at end of file diff --git a/delivery-service/src/test/resources/de/openknowledge/sample/address/0815.json b/delivery-service/src/test/resources/de/openknowledge/sample/address/0815.json new file mode 100644 index 00000000..84bb8d04 --- /dev/null +++ b/delivery-service/src/test/resources/de/openknowledge/sample/address/0815.json @@ -0,0 +1,8 @@ +{ + "recipient": "Max Mustermann", + "street": { + "name": "Poststraße", + "number": "1" + }, + "city": "26122 Oldenburg" +} diff --git a/delivery-service/src/test/resources/de/openknowledge/sample/address/0816-invalid.json b/delivery-service/src/test/resources/de/openknowledge/sample/address/0816-invalid.json new file mode 100644 index 00000000..58068cc6 --- /dev/null +++ b/delivery-service/src/test/resources/de/openknowledge/sample/address/0816-invalid.json @@ -0,0 +1,8 @@ +{ + "recipient": "Max Mustermann", + "street": { + "name": "Poststraße", + "number": "1" + }, + "city": "26122 London" +} diff --git a/delivery-service/src/test/resources/de/openknowledge/sample/address/0816.json b/delivery-service/src/test/resources/de/openknowledge/sample/address/0816.json new file mode 100644 index 00000000..9541839a --- /dev/null +++ b/delivery-service/src/test/resources/de/openknowledge/sample/address/0816.json @@ -0,0 +1,8 @@ +{ + "recipient": "Erika Mustermann", + "street": { + "name": "II. Hagen", + "number": "7" + }, + "city": "45127 Essen" +} \ No newline at end of file diff --git a/delivery-service/src/test/resources/de/openknowledge/sample/address/0817.json b/delivery-service/src/test/resources/de/openknowledge/sample/address/0817.json new file mode 100644 index 00000000..77c04eb4 --- /dev/null +++ b/delivery-service/src/test/resources/de/openknowledge/sample/address/0817.json @@ -0,0 +1,8 @@ +{ + "recipient": "Erika Mustermann", + "street": { + "name": "II. Hagen", + "number": "7" + }, + "city": "45127 Essen" +} diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..ddda0c8a --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,28 @@ +services: + customer-service: + build: customer-service/ + ports: + - "8181:8080" + environment: + - BILLING_SERVICE_URL=http://billing-service:8080 + - DELIVERY_SERVICE_URL=http://delivery-service:8080 + + billing-service: + build: billing-service/ + container_name: billing-service + ports: + - "8182:8080" + + delivery-service: + build: delivery-service/ + container_name: delivery-service + ports: + - "8183:8080" + environment: + - ADDRESS_VALIDATION_SERVICE_URL=http://address-validation-service:8080 + + address-validation-service: + build: address-validation-service/ + container_name: address-validation_service + ports: + - "8184:8080" diff --git a/keycloak/Dockerfile b/keycloak/Dockerfile new file mode 100644 index 00000000..d9f932e7 --- /dev/null +++ b/keycloak/Dockerfile @@ -0,0 +1,33 @@ +FROM registry.access.redhat.com/ubi8-minimal + +ENV KEYCLOAK_VERSION 14.0.0 +ENV JDBC_POSTGRES_VERSION 42.3.3 +ENV JDBC_MYSQL_VERSION 8.0.22 +ENV JDBC_MARIADB_VERSION 2.5.4 +ENV JDBC_MSSQL_VERSION 10.2.1.jre11 + +ENV LAUNCH_JBOSS_IN_BACKGROUND 1 +ENV PROXY_ADDRESS_FORWARDING false +ENV JBOSS_HOME /opt/jboss/keycloak +ENV LANG en_US.UTF-8 + +ARG GIT_REPO +ARG GIT_BRANCH +ARG KEYCLOAK_DIST=https://github.com/keycloak/keycloak/releases/download/$KEYCLOAK_VERSION/keycloak-$KEYCLOAK_VERSION.tar.gz + +USER root + +RUN microdnf update -y && microdnf install -y glibc-langpack-en gzip hostname java-11-openjdk-headless openssl tar which && microdnf clean all + +ADD containers/tools /opt/jboss/tools +RUN /opt/jboss/tools/build-keycloak.sh + +USER 1000 + +EXPOSE 8080 +EXPOSE 8443 + +ENTRYPOINT [ "/opt/jboss/tools/docker-entrypoint.sh" ] + +CMD ["-b", "0.0.0.0"] + From d7a21a7086b4aeab115a5b685b424ac567f3f955 Mon Sep 17 00:00:00 2001 From: Arne Limburg Date: Wed, 26 Nov 2025 22:23:55 +0100 Subject: [PATCH 2/6] Change namespace to jakarta --- address-validation-service/pom.xml | 84 ++++-------------- .../src/main/checkstyle/configuration.xml | 2 +- .../application/AddressApplication.java | 4 +- .../address/application/AddressResource.java | 24 ++--- .../address/domain/AddressRepository.java | 4 +- .../sample/address/domain/City.java | 4 +- .../sample/address/domain/CityName.java | 4 +- .../sample/address/domain/HouseNumber.java | 4 +- .../sample/address/domain/Street.java | 4 +- .../sample/address/domain/StreetName.java | 4 +- .../sample/address/domain/ZipCode.java | 4 +- .../address/infrastructure/CorsFilter.java | 8 +- billing-service/pom.xml | 88 ++++--------------- .../src/main/checkstyle/configuration.xml | 2 +- .../application/AddressApplication.java | 4 +- .../address/application/AddressResource.java | 26 +++--- .../sample/address/domain/Address.java | 4 +- .../sample/address/domain/AddressLine.java | 4 +- .../address/domain/AddressRepository.java | 4 +- .../sample/address/domain/City.java | 4 +- .../sample/address/domain/CityName.java | 4 +- .../sample/address/domain/CustomerNumber.java | 4 +- .../sample/address/domain/HouseNumber.java | 4 +- .../sample/address/domain/Location.java | 4 +- .../sample/address/domain/Recipient.java | 4 +- .../sample/address/domain/Street.java | 4 +- .../sample/address/domain/StreetName.java | 4 +- .../sample/address/domain/ZipCode.java | 4 +- .../address/infrastructure/CorsFilter.java | 8 +- .../sample/address/JsonObjectComparision.java | 6 +- .../address/domain/TestAddressRepository.java | 4 +- customer-service/pom.xml | 88 ++++--------------- .../src/main/checkstyle/configuration.xml | 2 +- .../sample/address/domain/Address.java | 4 +- .../sample/address/domain/AddressLine.java | 4 +- .../domain/BillingAddressRepository.java | 18 ++-- .../sample/address/domain/City.java | 4 +- .../sample/address/domain/CityName.java | 4 +- .../domain/DeliveryAddressRepository.java | 23 ++--- .../sample/address/domain/HouseNumber.java | 4 +- .../sample/address/domain/Recipient.java | 4 +- .../sample/address/domain/Street.java | 4 +- .../sample/address/domain/StreetName.java | 4 +- .../sample/address/domain/ZipCode.java | 4 +- .../address/infrastructure/CorsFilter.java | 8 +- .../ValidationExceptionHandler.java | 11 +-- .../application/CustomerApplication.java | 4 +- .../application/CustomerResource.java | 28 +++--- .../sample/customer/domain/Customer.java | 4 +- .../sample/customer/domain/CustomerName.java | 4 +- .../customer/domain/CustomerNumber.java | 4 +- .../customer/domain/CustomerRepository.java | 4 +- .../sample/customer/CustomerServiceTest.java | 16 ++-- .../customer/JsonObjectComparision.java | 6 +- .../sample/infrastructure/CdiMock.java | 4 +- delivery-service/pom.xml | 88 ++++--------------- .../src/main/checkstyle/configuration.xml | 2 +- .../application/AddressesApplication.java | 4 +- .../application/AddressesResource.java | 26 +++--- .../sample/address/domain/Address.java | 4 +- .../sample/address/domain/AddressLine.java | 4 +- .../address/domain/AddressRepository.java | 4 +- .../domain/AddressValidationService.java | 17 ++-- .../sample/address/domain/City.java | 4 +- .../sample/address/domain/CityName.java | 4 +- .../sample/address/domain/CustomerNumber.java | 4 +- .../sample/address/domain/HouseNumber.java | 4 +- .../sample/address/domain/Location.java | 4 +- .../sample/address/domain/Recipient.java | 4 +- .../sample/address/domain/Street.java | 4 +- .../sample/address/domain/StreetName.java | 4 +- .../sample/address/domain/ZipCode.java | 4 +- .../address/infrastructure/CorsFilter.java | 8 +- .../ValidationExceptionHandler.java | 13 +-- .../sample/infrastructure/CdiMock.java | 4 +- 75 files changed, 305 insertions(+), 501 deletions(-) diff --git a/address-validation-service/pom.xml b/address-validation-service/pom.xml index 3f87a412..58852c5e 100644 --- a/address-validation-service/pom.xml +++ b/address-validation-service/pom.xml @@ -1,14 +1,8 @@ - - - + 4.0.0 de.openknowledge.sample.shop @@ -21,13 +15,14 @@ 17 false UTF-8 - 1.2.15 + 2.0.0 1.9.6 5.8.2 + org.apache.meecrowave meecrowave-specs-api @@ -42,85 +37,39 @@ org.apache.geronimo.specs - geronimo-validation_1.0_spec - 1.1 + geronimo-validation_2.0_spec + 1.0 org.eclipse.microprofile.config microprofile-config-api - 1.4 + 3.0 org.apache.geronimo.config geronimo-config-impl - 1.2.2 + 1.2.3 + jakarta org.eclipse.microprofile.jwt microprofile-jwt-auth-api - 1.2.1 - - - org.apache.geronimo - geronimo-jwt-auth - 1.0.4 - - - org.apache.deltaspike.modules - deltaspike-security-module-api - ${deltaspike.version} - compile - - - org.apache.deltaspike.modules - deltaspike-security-module-impl - ${deltaspike.version} - runtime + 2.0 org.eclipse.microprofile.opentracing microprofile-opentracing-api - 1.2 - - - io.opentracing - opentracing-api - 0.31.0 - - - org.apache.geronimo - geronimo-opentracing - 1.0.3 + 2.0 org.eclipse.microprofile.health microprofile-health-api - 3.0-RC3 - - - org.apache.geronimo - geronimo-health - 2.0.1 - - - org.microprofile-ext.health-ext - healthprobe-jvm - 1.2.0 - - - org.microprofile-ext.health-ext - healthprobe-system - 1.2.0 + 4.0 org.eclipse.microprofile.metrics microprofile-metrics-api - 3.0-RC2 - - - org.apache.geronimo - geronimo-metrics - 1.0.6 + 4.0 org.apache.commons @@ -128,6 +77,7 @@ 3.9 + org.junit.jupiter junit-jupiter-api @@ -161,14 +111,14 @@ rocks.limburg.cdimock cdimock - 1.0.4 + 2.0.2 test com.tngtech.archunit archunit 1.0.1 - test + test diff --git a/address-validation-service/src/main/checkstyle/configuration.xml b/address-validation-service/src/main/checkstyle/configuration.xml index 3392d8cf..e91302e6 100644 --- a/address-validation-service/src/main/checkstyle/configuration.xml +++ b/address-validation-service/src/main/checkstyle/configuration.xml @@ -109,7 +109,7 @@ - + diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java index 89397e09..d9a6ac46 100644 --- a/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java @@ -15,8 +15,8 @@ */ package de.openknowledge.sample.address.application; -import javax.ws.rs.ApplicationPath; -import javax.ws.rs.core.Application; +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; /** * Application initialization diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java index 91bf2c84..a846960d 100644 --- a/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java @@ -22,18 +22,18 @@ import java.util.List; import java.util.logging.Logger; -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; -import javax.ws.rs.core.UriInfo; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriBuilder; +import jakarta.ws.rs.core.UriInfo; import de.openknowledge.sample.address.domain.Address; import de.openknowledge.sample.address.domain.AddressRepository; diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java index f568c161..9d058f68 100644 --- a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java @@ -26,8 +26,8 @@ import java.util.Set; import java.util.logging.Logger; -import javax.annotation.PostConstruct; -import javax.enterprise.context.ApplicationScoped; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped public class AddressRepository { diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/City.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/City.java index c21639b3..5bde9088 100644 --- a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/City.java +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/City.java @@ -22,8 +22,8 @@ import java.util.List; import java.util.stream.Stream; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.City.Adapter; diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java index 1fea0874..c0b754c3 100644 --- a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notNull; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.CityName.Adapter; diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java index e54c14c7..5df7382e 100644 --- a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notBlank; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.HouseNumber.Adapter; diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/Street.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/Street.java index bc0af1cd..1333cf7c 100644 --- a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/Street.java +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/Street.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notNull; -import javax.json.bind.annotation.JsonbCreator; -import javax.json.bind.annotation.JsonbProperty; +import jakarta.json.bind.annotation.JsonbCreator; +import jakarta.json.bind.annotation.JsonbProperty; public class Street { diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java index 16e7ae01..e95b451b 100644 --- a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notBlank; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.StreetName.Adapter; diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java index 1e62563b..2e810454 100644 --- a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notNull; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.ZipCode.Adapter; diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java index 8a84c0f2..8bf945ec 100644 --- a/address-validation-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java @@ -15,10 +15,10 @@ */ package de.openknowledge.sample.address.infrastructure; -import javax.ws.rs.container.ContainerRequestContext; -import javax.ws.rs.container.ContainerResponseContext; -import javax.ws.rs.container.ContainerResponseFilter; -import javax.ws.rs.ext.Provider; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.ext.Provider; /** * Filter to allow cross origin calls. diff --git a/billing-service/pom.xml b/billing-service/pom.xml index f21e858d..58852c5e 100644 --- a/billing-service/pom.xml +++ b/billing-service/pom.xml @@ -1,14 +1,8 @@ - - - + 4.0.0 de.openknowledge.sample.shop @@ -21,13 +15,14 @@ 17 false UTF-8 - 1.2.15 + 2.0.0 1.9.6 5.8.2 + org.apache.meecrowave meecrowave-specs-api @@ -42,85 +37,39 @@ org.apache.geronimo.specs - geronimo-validation_1.0_spec - 1.1 + geronimo-validation_2.0_spec + 1.0 org.eclipse.microprofile.config microprofile-config-api - 1.4 + 3.0 org.apache.geronimo.config geronimo-config-impl - 1.2.2 + 1.2.3 + jakarta org.eclipse.microprofile.jwt microprofile-jwt-auth-api - 1.2.1 - - - org.apache.geronimo - geronimo-jwt-auth - 1.0.4 - - - org.apache.deltaspike.modules - deltaspike-security-module-api - ${deltaspike.version} - compile - - - org.apache.deltaspike.modules - deltaspike-security-module-impl - ${deltaspike.version} - runtime + 2.0 org.eclipse.microprofile.opentracing microprofile-opentracing-api - 1.2 - - - io.opentracing - opentracing-api - 0.31.0 - - - org.apache.geronimo - geronimo-opentracing - 1.0.3 + 2.0 org.eclipse.microprofile.health microprofile-health-api - 3.0-RC3 - - - org.apache.geronimo - geronimo-health - 2.0.1 - - - org.microprofile-ext.health-ext - healthprobe-jvm - 1.2.0 - - - org.microprofile-ext.health-ext - healthprobe-system - 1.2.0 + 4.0 org.eclipse.microprofile.metrics microprofile-metrics-api - 3.0-RC2 - - - org.apache.geronimo - geronimo-metrics - 1.0.6 + 4.0 org.apache.commons @@ -128,6 +77,7 @@ 3.9 + org.junit.jupiter junit-jupiter-api @@ -161,14 +111,14 @@ rocks.limburg.cdimock cdimock - 1.0.4 + 2.0.2 test com.tngtech.archunit archunit 1.0.1 - test + test @@ -177,7 +127,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M5 + 3.5.4 org.apache.meecrowave @@ -186,7 +136,7 @@ maven-checkstyle-plugin - 3.2.0 + 3.2.1 true ${project.basedir}/src/main/checkstyle/java.header diff --git a/billing-service/src/main/checkstyle/configuration.xml b/billing-service/src/main/checkstyle/configuration.xml index 3392d8cf..e91302e6 100644 --- a/billing-service/src/main/checkstyle/configuration.xml +++ b/billing-service/src/main/checkstyle/configuration.xml @@ -109,7 +109,7 @@ - + diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java b/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java index 89397e09..d9a6ac46 100644 --- a/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java +++ b/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressApplication.java @@ -15,8 +15,8 @@ */ package de.openknowledge.sample.address.application; -import javax.ws.rs.ApplicationPath; -import javax.ws.rs.core.Application; +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; /** * Application initialization diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java b/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java index 3caa46af..f5aaa334 100644 --- a/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java +++ b/billing-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java @@ -17,19 +17,19 @@ import java.util.logging.Logger; -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.NotFoundException; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; import de.openknowledge.sample.address.domain.Address; import de.openknowledge.sample.address.domain.AddressRepository; diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/Address.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/Address.java index f650f7f5..dade9515 100644 --- a/billing-service/src/main/java/de/openknowledge/sample/address/domain/Address.java +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/Address.java @@ -20,8 +20,8 @@ import java.util.Objects; -import javax.json.bind.annotation.JsonbCreator; -import javax.json.bind.annotation.JsonbProperty; +import jakarta.json.bind.annotation.JsonbCreator; +import jakarta.json.bind.annotation.JsonbProperty; public class Address { private Recipient recipient; diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java index 596ce2b3..f1b69491 100644 --- a/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notNull; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.AddressLine.Adapter; diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java index ea48ac32..f7ee9a79 100644 --- a/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java @@ -22,8 +22,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Logger; -import javax.annotation.PostConstruct; -import javax.enterprise.context.ApplicationScoped; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped public class AddressRepository { diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/City.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/City.java index cc521fe9..c299b21a 100644 --- a/billing-service/src/main/java/de/openknowledge/sample/address/domain/City.java +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/City.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notNull; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.City.Adapter; diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java index 1fea0874..c0b754c3 100644 --- a/billing-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notNull; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.CityName.Adapter; diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java index 2cf9dec8..cb95a130 100644 --- a/billing-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notBlank; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.CustomerNumber.Adapter; diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java index e54c14c7..5df7382e 100644 --- a/billing-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notBlank; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.HouseNumber.Adapter; diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/Location.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/Location.java index 2152f801..2463226a 100644 --- a/billing-service/src/main/java/de/openknowledge/sample/address/domain/Location.java +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/Location.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notNull; -import javax.json.bind.annotation.JsonbCreator; -import javax.json.bind.annotation.JsonbProperty; +import jakarta.json.bind.annotation.JsonbCreator; +import jakarta.json.bind.annotation.JsonbProperty; public class Location { diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java index cbf39d97..fa2cd791 100644 --- a/billing-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notBlank; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.Recipient.Adapter; diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/Street.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/Street.java index bc0af1cd..1333cf7c 100644 --- a/billing-service/src/main/java/de/openknowledge/sample/address/domain/Street.java +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/Street.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notNull; -import javax.json.bind.annotation.JsonbCreator; -import javax.json.bind.annotation.JsonbProperty; +import jakarta.json.bind.annotation.JsonbCreator; +import jakarta.json.bind.annotation.JsonbProperty; public class Street { diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java index 16e7ae01..e95b451b 100644 --- a/billing-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notBlank; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.StreetName.Adapter; diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java b/billing-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java index 1e62563b..2e810454 100644 --- a/billing-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java +++ b/billing-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notNull; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.ZipCode.Adapter; diff --git a/billing-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java b/billing-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java index 8a84c0f2..8bf945ec 100644 --- a/billing-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java +++ b/billing-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java @@ -15,10 +15,10 @@ */ package de.openknowledge.sample.address.infrastructure; -import javax.ws.rs.container.ContainerRequestContext; -import javax.ws.rs.container.ContainerResponseContext; -import javax.ws.rs.container.ContainerResponseFilter; -import javax.ws.rs.ext.Provider; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.ext.Provider; /** * Filter to allow cross origin calls. diff --git a/billing-service/src/test/java/de/openknowledge/sample/address/JsonObjectComparision.java b/billing-service/src/test/java/de/openknowledge/sample/address/JsonObjectComparision.java index e7e08f86..612b44b2 100644 --- a/billing-service/src/test/java/de/openknowledge/sample/address/JsonObjectComparision.java +++ b/billing-service/src/test/java/de/openknowledge/sample/address/JsonObjectComparision.java @@ -18,9 +18,9 @@ import java.io.InputStream; import java.util.Map; -import javax.json.Json; -import javax.json.JsonObject; -import javax.json.JsonValue; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonValue; import org.assertj.core.api.Condition; diff --git a/billing-service/src/test/java/de/openknowledge/sample/address/domain/TestAddressRepository.java b/billing-service/src/test/java/de/openknowledge/sample/address/domain/TestAddressRepository.java index 180e75e9..365fe39a 100644 --- a/billing-service/src/test/java/de/openknowledge/sample/address/domain/TestAddressRepository.java +++ b/billing-service/src/test/java/de/openknowledge/sample/address/domain/TestAddressRepository.java @@ -15,8 +15,8 @@ */ package de.openknowledge.sample.address.domain; -import javax.enterprise.context.RequestScoped; -import javax.enterprise.inject.Specializes; +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.inject.Specializes; @Specializes @RequestScoped diff --git a/customer-service/pom.xml b/customer-service/pom.xml index f21e858d..58852c5e 100644 --- a/customer-service/pom.xml +++ b/customer-service/pom.xml @@ -1,14 +1,8 @@ - - - + 4.0.0 de.openknowledge.sample.shop @@ -21,13 +15,14 @@ 17 false UTF-8 - 1.2.15 + 2.0.0 1.9.6 5.8.2 + org.apache.meecrowave meecrowave-specs-api @@ -42,85 +37,39 @@ org.apache.geronimo.specs - geronimo-validation_1.0_spec - 1.1 + geronimo-validation_2.0_spec + 1.0 org.eclipse.microprofile.config microprofile-config-api - 1.4 + 3.0 org.apache.geronimo.config geronimo-config-impl - 1.2.2 + 1.2.3 + jakarta org.eclipse.microprofile.jwt microprofile-jwt-auth-api - 1.2.1 - - - org.apache.geronimo - geronimo-jwt-auth - 1.0.4 - - - org.apache.deltaspike.modules - deltaspike-security-module-api - ${deltaspike.version} - compile - - - org.apache.deltaspike.modules - deltaspike-security-module-impl - ${deltaspike.version} - runtime + 2.0 org.eclipse.microprofile.opentracing microprofile-opentracing-api - 1.2 - - - io.opentracing - opentracing-api - 0.31.0 - - - org.apache.geronimo - geronimo-opentracing - 1.0.3 + 2.0 org.eclipse.microprofile.health microprofile-health-api - 3.0-RC3 - - - org.apache.geronimo - geronimo-health - 2.0.1 - - - org.microprofile-ext.health-ext - healthprobe-jvm - 1.2.0 - - - org.microprofile-ext.health-ext - healthprobe-system - 1.2.0 + 4.0 org.eclipse.microprofile.metrics microprofile-metrics-api - 3.0-RC2 - - - org.apache.geronimo - geronimo-metrics - 1.0.6 + 4.0 org.apache.commons @@ -128,6 +77,7 @@ 3.9 + org.junit.jupiter junit-jupiter-api @@ -161,14 +111,14 @@ rocks.limburg.cdimock cdimock - 1.0.4 + 2.0.2 test com.tngtech.archunit archunit 1.0.1 - test + test @@ -177,7 +127,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M5 + 3.5.4 org.apache.meecrowave @@ -186,7 +136,7 @@ maven-checkstyle-plugin - 3.2.0 + 3.2.1 true ${project.basedir}/src/main/checkstyle/java.header diff --git a/customer-service/src/main/checkstyle/configuration.xml b/customer-service/src/main/checkstyle/configuration.xml index 3392d8cf..e91302e6 100644 --- a/customer-service/src/main/checkstyle/configuration.xml +++ b/customer-service/src/main/checkstyle/configuration.xml @@ -109,7 +109,7 @@ - + diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/Address.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/Address.java index f650f7f5..dade9515 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/address/domain/Address.java +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/Address.java @@ -20,8 +20,8 @@ import java.util.Objects; -import javax.json.bind.annotation.JsonbCreator; -import javax.json.bind.annotation.JsonbProperty; +import jakarta.json.bind.annotation.JsonbCreator; +import jakarta.json.bind.annotation.JsonbProperty; public class Address { private Recipient recipient; diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java index 864e5884..50c0c233 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notNull; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.AddressLine.Adapter; diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/BillingAddressRepository.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/BillingAddressRepository.java index d5075f5c..2a4ac6a6 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/address/domain/BillingAddressRepository.java +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/BillingAddressRepository.java @@ -15,19 +15,19 @@ */ package de.openknowledge.sample.address.domain; -import static javax.ws.rs.client.Entity.entity; -import static javax.ws.rs.core.Response.Status.Family.SUCCESSFUL; +import static jakarta.ws.rs.client.Entity.entity; +import static jakarta.ws.rs.core.Response.Status.Family.SUCCESSFUL; import java.util.Optional; import java.util.logging.Logger; -import javax.annotation.PostConstruct; -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import org.apache.johnzon.jaxrs.jsonb.jaxrs.JsonbJaxrsProvider; import org.eclipse.microprofile.config.inject.ConfigProperty; diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/City.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/City.java index cc521fe9..c299b21a 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/address/domain/City.java +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/City.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notNull; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.City.Adapter; diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java index 1fea0874..c0b754c3 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notNull; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.CityName.Adapter; diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/DeliveryAddressRepository.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/DeliveryAddressRepository.java index ed530fa5..0bf7050b 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/address/domain/DeliveryAddressRepository.java +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/DeliveryAddressRepository.java @@ -15,23 +15,24 @@ */ package de.openknowledge.sample.address.domain; -import static javax.ws.rs.client.Entity.entity; -import static javax.ws.rs.core.Response.Status.Family.SUCCESSFUL; +import static jakarta.ws.rs.client.Entity.entity; +import static jakarta.ws.rs.core.Response.Status.Family.SUCCESSFUL; import java.io.StringReader; import java.util.Optional; import java.util.logging.Logger; -import javax.annotation.PostConstruct; -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; -import javax.json.Json; -import javax.json.JsonObject; import javax.validation.ValidationException; -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import org.eclipse.microprofile.config.inject.ConfigProperty; diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java index e54c14c7..5df7382e 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notBlank; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.HouseNumber.Adapter; diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java index cbf39d97..fa2cd791 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notBlank; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.Recipient.Adapter; diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/Street.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/Street.java index bc0af1cd..1333cf7c 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/address/domain/Street.java +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/Street.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notNull; -import javax.json.bind.annotation.JsonbCreator; -import javax.json.bind.annotation.JsonbProperty; +import jakarta.json.bind.annotation.JsonbCreator; +import jakarta.json.bind.annotation.JsonbProperty; public class Street { diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java index 16e7ae01..e95b451b 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notBlank; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.StreetName.Adapter; diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java index 1e62563b..2e810454 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notNull; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.ZipCode.Adapter; diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java b/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java index 8a84c0f2..8bf945ec 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java +++ b/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java @@ -15,10 +15,10 @@ */ package de.openknowledge.sample.address.infrastructure; -import javax.ws.rs.container.ContainerRequestContext; -import javax.ws.rs.container.ContainerResponseContext; -import javax.ws.rs.container.ContainerResponseFilter; -import javax.ws.rs.ext.Provider; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.ext.Provider; /** * Filter to allow cross origin calls. diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java b/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java index 4ece55c7..fc9d7090 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java +++ b/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java @@ -16,11 +16,12 @@ package de.openknowledge.sample.address.infrastructure; import javax.validation.ValidationException; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; + +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; @Provider public class ValidationExceptionHandler implements ExceptionMapper { diff --git a/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerApplication.java b/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerApplication.java index 0786ef68..f89f46bd 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerApplication.java +++ b/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerApplication.java @@ -15,8 +15,8 @@ */ package de.openknowledge.sample.customer.application; -import javax.ws.rs.ApplicationPath; -import javax.ws.rs.core.Application; +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; /** * Application initialization diff --git a/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerResource.java b/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerResource.java index d28fdbac..0fe6c0a1 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerResource.java +++ b/customer-service/src/main/java/de/openknowledge/sample/customer/application/CustomerResource.java @@ -20,20 +20,20 @@ import java.util.function.Supplier; import java.util.logging.Logger; -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.NotFoundException; -import javax.ws.rs.POST; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; import de.openknowledge.sample.address.domain.Address; import de.openknowledge.sample.address.domain.BillingAddressRepository; diff --git a/customer-service/src/main/java/de/openknowledge/sample/customer/domain/Customer.java b/customer-service/src/main/java/de/openknowledge/sample/customer/domain/Customer.java index 06b82bad..69b3aa94 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/customer/domain/Customer.java +++ b/customer-service/src/main/java/de/openknowledge/sample/customer/domain/Customer.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notNull; -import javax.json.bind.annotation.JsonbCreator; -import javax.json.bind.annotation.JsonbProperty; +import jakarta.json.bind.annotation.JsonbCreator; +import jakarta.json.bind.annotation.JsonbProperty; import de.openknowledge.sample.address.domain.Address; diff --git a/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerName.java b/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerName.java index 44093e2d..deef9988 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerName.java +++ b/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerName.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notBlank; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.customer.domain.CustomerName.Adapter; diff --git a/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerNumber.java b/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerNumber.java index d6fbeb6a..cac44656 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerNumber.java +++ b/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerNumber.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notBlank; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.customer.domain.CustomerNumber.Adapter; diff --git a/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerRepository.java b/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerRepository.java index 8c079e51..df7ed683 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerRepository.java +++ b/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerRepository.java @@ -25,8 +25,8 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Logger; -import javax.annotation.PostConstruct; -import javax.enterprise.context.ApplicationScoped; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; /** * Addresses repository diff --git a/customer-service/src/test/java/de/openknowledge/sample/customer/CustomerServiceTest.java b/customer-service/src/test/java/de/openknowledge/sample/customer/CustomerServiceTest.java index 5e1e785c..2168b93a 100644 --- a/customer-service/src/test/java/de/openknowledge/sample/customer/CustomerServiceTest.java +++ b/customer-service/src/test/java/de/openknowledge/sample/customer/CustomerServiceTest.java @@ -17,7 +17,7 @@ import static de.openknowledge.sample.customer.JsonObjectComparision.sameAs; import static de.openknowledge.sample.customer.JsonObjectComparision.thatIsSameAs; -import static javax.ws.rs.client.Entity.entity; +import static jakarta.ws.rs.client.Entity.entity; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; @@ -25,13 +25,13 @@ import java.net.URI; import java.util.Optional; -import javax.inject.Inject; -import javax.json.Json; -import javax.json.JsonArray; -import javax.json.JsonObject; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; +import jakarta.inject.Inject; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import org.apache.meecrowave.Meecrowave; import org.apache.meecrowave.junit5.MeecrowaveConfig; diff --git a/customer-service/src/test/java/de/openknowledge/sample/customer/JsonObjectComparision.java b/customer-service/src/test/java/de/openknowledge/sample/customer/JsonObjectComparision.java index b2f4cf26..470cd907 100644 --- a/customer-service/src/test/java/de/openknowledge/sample/customer/JsonObjectComparision.java +++ b/customer-service/src/test/java/de/openknowledge/sample/customer/JsonObjectComparision.java @@ -18,9 +18,9 @@ import java.io.InputStream; import java.util.Map; -import javax.json.Json; -import javax.json.JsonObject; -import javax.json.JsonValue; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonValue; import org.assertj.core.api.Condition; diff --git a/customer-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java b/customer-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java index 11bbc834..462cf7d8 100644 --- a/customer-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java +++ b/customer-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java @@ -23,8 +23,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; -import javax.enterprise.inject.Alternative; -import javax.enterprise.inject.Stereotype; +import jakarta.enterprise.inject.Alternative; +import jakarta.enterprise.inject.Stereotype; @Stereotype @Alternative diff --git a/delivery-service/pom.xml b/delivery-service/pom.xml index f21e858d..58852c5e 100644 --- a/delivery-service/pom.xml +++ b/delivery-service/pom.xml @@ -1,14 +1,8 @@ - - - + 4.0.0 de.openknowledge.sample.shop @@ -21,13 +15,14 @@ 17 false UTF-8 - 1.2.15 + 2.0.0 1.9.6 5.8.2 + org.apache.meecrowave meecrowave-specs-api @@ -42,85 +37,39 @@ org.apache.geronimo.specs - geronimo-validation_1.0_spec - 1.1 + geronimo-validation_2.0_spec + 1.0 org.eclipse.microprofile.config microprofile-config-api - 1.4 + 3.0 org.apache.geronimo.config geronimo-config-impl - 1.2.2 + 1.2.3 + jakarta org.eclipse.microprofile.jwt microprofile-jwt-auth-api - 1.2.1 - - - org.apache.geronimo - geronimo-jwt-auth - 1.0.4 - - - org.apache.deltaspike.modules - deltaspike-security-module-api - ${deltaspike.version} - compile - - - org.apache.deltaspike.modules - deltaspike-security-module-impl - ${deltaspike.version} - runtime + 2.0 org.eclipse.microprofile.opentracing microprofile-opentracing-api - 1.2 - - - io.opentracing - opentracing-api - 0.31.0 - - - org.apache.geronimo - geronimo-opentracing - 1.0.3 + 2.0 org.eclipse.microprofile.health microprofile-health-api - 3.0-RC3 - - - org.apache.geronimo - geronimo-health - 2.0.1 - - - org.microprofile-ext.health-ext - healthprobe-jvm - 1.2.0 - - - org.microprofile-ext.health-ext - healthprobe-system - 1.2.0 + 4.0 org.eclipse.microprofile.metrics microprofile-metrics-api - 3.0-RC2 - - - org.apache.geronimo - geronimo-metrics - 1.0.6 + 4.0 org.apache.commons @@ -128,6 +77,7 @@ 3.9 + org.junit.jupiter junit-jupiter-api @@ -161,14 +111,14 @@ rocks.limburg.cdimock cdimock - 1.0.4 + 2.0.2 test com.tngtech.archunit archunit 1.0.1 - test + test @@ -177,7 +127,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M5 + 3.5.4 org.apache.meecrowave @@ -186,7 +136,7 @@ maven-checkstyle-plugin - 3.2.0 + 3.2.1 true ${project.basedir}/src/main/checkstyle/java.header diff --git a/delivery-service/src/main/checkstyle/configuration.xml b/delivery-service/src/main/checkstyle/configuration.xml index 3392d8cf..e91302e6 100644 --- a/delivery-service/src/main/checkstyle/configuration.xml +++ b/delivery-service/src/main/checkstyle/configuration.xml @@ -109,7 +109,7 @@ - + diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesApplication.java b/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesApplication.java index 5f442dc6..4f08e2f8 100644 --- a/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesApplication.java +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesApplication.java @@ -15,8 +15,8 @@ */ package de.openknowledge.sample.address.application; -import javax.ws.rs.ApplicationPath; -import javax.ws.rs.core.Application; +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; /** * Application initialization diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesResource.java b/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesResource.java index 3155f8d8..993c579a 100644 --- a/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesResource.java +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/application/AddressesResource.java @@ -17,19 +17,19 @@ import java.util.logging.Logger; -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.NotFoundException; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; import de.openknowledge.sample.address.domain.Address; import de.openknowledge.sample.address.domain.AddressRepository; diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Address.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Address.java index f650f7f5..dade9515 100644 --- a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Address.java +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Address.java @@ -20,8 +20,8 @@ import java.util.Objects; -import javax.json.bind.annotation.JsonbCreator; -import javax.json.bind.annotation.JsonbProperty; +import jakarta.json.bind.annotation.JsonbCreator; +import jakarta.json.bind.annotation.JsonbProperty; public class Address { private Recipient recipient; diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java index 864e5884..50c0c233 100644 --- a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressLine.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notNull; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.AddressLine.Adapter; diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java index a6b5a991..0d40d7be 100644 --- a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java @@ -22,8 +22,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Logger; -import javax.annotation.PostConstruct; -import javax.enterprise.context.ApplicationScoped; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; /** * Addresses repository diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressValidationService.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressValidationService.java index 56bfff61..26aef503 100644 --- a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressValidationService.java +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressValidationService.java @@ -15,19 +15,20 @@ */ package de.openknowledge.sample.address.domain; -import static javax.ws.rs.client.Entity.entity; +import static jakarta.ws.rs.client.Entity.entity; import java.io.StringReader; import java.util.logging.Logger; -import javax.enterprise.context.ApplicationScoped; -import javax.inject.Inject; -import javax.json.Json; -import javax.json.JsonObject; import javax.validation.ValidationException; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import org.apache.johnzon.jaxrs.jsonb.jaxrs.JsonbJaxrsProvider; import org.eclipse.microprofile.config.inject.ConfigProperty; diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/City.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/City.java index cc521fe9..c299b21a 100644 --- a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/City.java +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/City.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notNull; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.City.Adapter; diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java index 1fea0874..c0b754c3 100644 --- a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CityName.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notNull; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.CityName.Adapter; diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java index 2cf9dec8..cb95a130 100644 --- a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/CustomerNumber.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notBlank; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.CustomerNumber.Adapter; diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java index e54c14c7..5df7382e 100644 --- a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/HouseNumber.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notBlank; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.HouseNumber.Adapter; diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Location.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Location.java index 2152f801..2463226a 100644 --- a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Location.java +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Location.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notNull; -import javax.json.bind.annotation.JsonbCreator; -import javax.json.bind.annotation.JsonbProperty; +import jakarta.json.bind.annotation.JsonbCreator; +import jakarta.json.bind.annotation.JsonbProperty; public class Location { diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java index cbf39d97..fa2cd791 100644 --- a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Recipient.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notBlank; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.Recipient.Adapter; diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Street.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Street.java index bc0af1cd..1333cf7c 100644 --- a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Street.java +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/Street.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notNull; -import javax.json.bind.annotation.JsonbCreator; -import javax.json.bind.annotation.JsonbProperty; +import jakarta.json.bind.annotation.JsonbCreator; +import jakarta.json.bind.annotation.JsonbProperty; public class Street { diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java index 16e7ae01..e95b451b 100644 --- a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/StreetName.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notBlank; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.StreetName.Adapter; diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java index 1e62563b..2e810454 100644 --- a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/ZipCode.java @@ -17,8 +17,8 @@ import static org.apache.commons.lang3.Validate.notNull; -import javax.json.bind.adapter.JsonbAdapter; -import javax.json.bind.annotation.JsonbTypeAdapter; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.annotation.JsonbTypeAdapter; import de.openknowledge.sample.address.domain.ZipCode.Adapter; diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java b/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java index 8a84c0f2..8bf945ec 100644 --- a/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java @@ -15,10 +15,10 @@ */ package de.openknowledge.sample.address.infrastructure; -import javax.ws.rs.container.ContainerRequestContext; -import javax.ws.rs.container.ContainerResponseContext; -import javax.ws.rs.container.ContainerResponseFilter; -import javax.ws.rs.ext.Provider; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.ext.Provider; /** * Filter to allow cross origin calls. diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java b/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java index fa5849f4..a46e8357 100644 --- a/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java @@ -15,13 +15,14 @@ */ package de.openknowledge.sample.address.infrastructure; -import javax.enterprise.context.ApplicationScoped; import javax.validation.ValidationException; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; @Provider @ApplicationScoped diff --git a/delivery-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java b/delivery-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java index 11bbc834..462cf7d8 100644 --- a/delivery-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java +++ b/delivery-service/src/test/java/de/openknowledge/sample/infrastructure/CdiMock.java @@ -23,8 +23,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; -import javax.enterprise.inject.Alternative; -import javax.enterprise.inject.Stereotype; +import jakarta.enterprise.inject.Alternative; +import jakarta.enterprise.inject.Stereotype; @Stereotype @Alternative From d45306401b97d04fd674682acfc8c55e42f87982 Mon Sep 17 00:00:00 2001 From: Arne Limburg Date: Tue, 3 Feb 2026 13:17:50 +0100 Subject: [PATCH 3/6] feat: Add client --- .github/workflows/build.yaml | 67 + address-validation-service/pom.xml | 6 +- .../address/application/AddressResource.java | 48 +- .../address/domain/AddressRepository.java | 7 +- billing-service/pom.xml | 6 +- customer-client/.gitignore | 28 + customer-client/.prettierrc | 7 + customer-client/Dockerfile | 41 + customer-client/README-APP.md | 77 + customer-client/README.md | 73 + customer-client/eslint.config.js | 25 + customer-client/index.html | 13 + customer-client/package-lock.json | 3628 +++++++++++++++++ customer-client/package.json | 43 + customer-client/playwright.config.ts | 27 + customer-client/public/vite.svg | 1 + customer-client/src/App.css | 371 ++ customer-client/src/App.tsx | 39 + customer-client/src/api/client.ts | 89 + customer-client/src/api/hooks.ts | 70 + customer-client/src/assets/react.svg | 1 + .../src/components/BillingAddressForm.tsx | 217 + .../src/components/DeliveryAddressForm.tsx | 250 ++ customer-client/src/index.css | 66 + customer-client/src/main.tsx | 40 + customer-client/src/pages/CreateCustomer.tsx | 108 + customer-client/src/pages/CustomerDetail.tsx | 148 + customer-client/src/pages/CustomerList.tsx | 126 + customer-client/src/types/customer.ts | 54 + customer-client/test/create-customer.spec.ts | 41 + customer-client/test/customer-detail.spec.ts | 253 ++ customer-client/test/customer-list.spec.ts | 69 + customer-client/tsconfig.app.json | 28 + customer-client/tsconfig.json | 4 + customer-client/tsconfig.node.json | 26 + customer-client/vite.config.ts | 7 + customer-service/pom.xml | 6 +- .../domain/DeliveryAddressRepository.java | 31 +- .../address/infrastructure/CorsFilter.java | 2 + .../infrastructure/LanguageProvider.java | 34 + .../ValidationExceptionHandler.java | 5 +- delivery-service/pom.xml | 6 +- .../domain/AddressValidationService.java | 20 +- .../infrastructure/LanguageProvider.java | 34 + .../ValidationExceptionHandler.java | 3 +- docker-compose.yaml | 11 + 46 files changed, 6220 insertions(+), 36 deletions(-) create mode 100644 customer-client/.gitignore create mode 100644 customer-client/.prettierrc create mode 100644 customer-client/Dockerfile create mode 100644 customer-client/README-APP.md create mode 100644 customer-client/README.md create mode 100644 customer-client/eslint.config.js create mode 100644 customer-client/index.html create mode 100644 customer-client/package-lock.json create mode 100644 customer-client/package.json create mode 100644 customer-client/playwright.config.ts create mode 100644 customer-client/public/vite.svg create mode 100644 customer-client/src/App.css create mode 100644 customer-client/src/App.tsx create mode 100644 customer-client/src/api/client.ts create mode 100644 customer-client/src/api/hooks.ts create mode 100644 customer-client/src/assets/react.svg create mode 100644 customer-client/src/components/BillingAddressForm.tsx create mode 100644 customer-client/src/components/DeliveryAddressForm.tsx create mode 100644 customer-client/src/index.css create mode 100644 customer-client/src/main.tsx create mode 100644 customer-client/src/pages/CreateCustomer.tsx create mode 100644 customer-client/src/pages/CustomerDetail.tsx create mode 100644 customer-client/src/pages/CustomerList.tsx create mode 100644 customer-client/src/types/customer.ts create mode 100644 customer-client/test/create-customer.spec.ts create mode 100644 customer-client/test/customer-detail.spec.ts create mode 100644 customer-client/test/customer-list.spec.ts create mode 100644 customer-client/tsconfig.app.json create mode 100644 customer-client/tsconfig.json create mode 100644 customer-client/tsconfig.node.json create mode 100644 customer-client/vite.config.ts create mode 100644 customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/LanguageProvider.java create mode 100644 delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/LanguageProvider.java diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2748e7c6..a77696fc 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -3,8 +3,37 @@ on: push: branches: [ "**" ] jobs: + build-client: + runs-on: ubuntu-latest + defaults: + run: + working-directory: customer-client + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + cache-dependency-path: customer-client/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Check Prettier formatting + run: npm run format:check + + - name: Lint + run: npm run lint + + - name: Build + run: npm run build + build: runs-on: ubuntu-latest + needs: build-client steps: - name: Checkout uses: actions/checkout@v5 @@ -16,6 +45,21 @@ jobs: distribution: 'temurin' cache: maven + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + cache-dependency-path: customer-client/package-lock.json + + - name: Install customer-client dependencies + run: npm ci + working-directory: customer-client + + - name: Build customer-client + run: npm run build + working-directory: customer-client + - name: Build customer-service run: mvn clean package -f customer-service/pom.xml -Dservice.name=customer-service @@ -34,5 +78,28 @@ jobs: - name: Start with Docker run: docker compose up -d + - name: Wait for customer-service + run: | + until curl -sf http://localhost:8181/customers/ > /dev/null; do + echo "Waiting for customer-service..." + sleep 2 + done + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + working-directory: customer-client + + - name: Run Playwright tests + run: npm run test + working-directory: customer-client + + - name: Upload Playwright report + uses: actions/upload-artifact@v4 + if: failure() + with: + name: playwright-report + path: customer-client/playwright-report/ + - name: Stop Docker Containers + if: always() run: docker compose down diff --git a/address-validation-service/pom.xml b/address-validation-service/pom.xml index 58852c5e..8d6fdd18 100644 --- a/address-validation-service/pom.xml +++ b/address-validation-service/pom.xml @@ -36,9 +36,9 @@ provided - org.apache.geronimo.specs - geronimo-validation_2.0_spec - 1.0 + jakarta.validation + jakarta.validation-api + 3.1.1 org.eclipse.microprofile.config diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java index a846960d..d0e32578 100644 --- a/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/application/AddressResource.java @@ -15,17 +15,21 @@ */ package de.openknowledge.sample.address.application; +import static jakarta.ws.rs.core.HttpHeaders.ACCEPT_LANGUAGE; +import static java.util.Locale.GERMANY; +import static java.util.Optional.ofNullable; import static java.util.stream.Collectors.joining; import java.net.URI; -import java.net.URISyntaxException; import java.util.List; +import java.util.Locale; import java.util.logging.Logger; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; @@ -64,10 +68,9 @@ public Response healthCheck() { @POST @Path("/") @Consumes(MediaType.APPLICATION_JSON) - public Response validateAddress(Address address, @Context UriInfo uri) throws URISyntaxException { + public Response validateAddress(Address address, @Context UriInfo uri, @HeaderParam(ACCEPT_LANGUAGE) Locale locale) { LOGGER.info("RESTful call 'POST valid address'"); if (addressesRepository.isValid(address)) { - LOGGER.fine("address is valid"); return Response.ok().build(); } else { URI type = uri.getRequestUri().resolve("/errors/invalid-city"); @@ -76,24 +79,49 @@ public Response validateAddress(Address address, @Context UriInfo uri) throws UR LOGGER.fine(suggestions.size() + " suggestions found: " + suggestions); if (suggestions.size() == 1) { return Response.status(Response.Status.BAD_REQUEST).type(PROBLEM_JSON_TYPE) - .entity(String.format(PROBLEM_JSON, type, "invalid city", + .entity(String.format(PROBLEM_JSON, type, + translate("invalid city", locale), Response.Status.BAD_REQUEST.getStatusCode(), - "Did you mean " + suggestions.iterator().next() + "?", instance)) + translate("Did you mean %s?", locale, suggestions.iterator().next()), + instance)) .build(); } else if (!suggestions.isEmpty()) { + String suggestionList = suggestions.stream().map(Object::toString).collect(joining(", ")); return Response.status(Response.Status.BAD_REQUEST).type(PROBLEM_JSON_TYPE) - .entity(String.format(PROBLEM_JSON, type, "invalid city", + .entity(String.format(PROBLEM_JSON, type, + translate("invalid city", locale), Response.Status.BAD_REQUEST.getStatusCode(), - "Did you mean one of " - + suggestions.stream().map(Object::toString).collect(joining(", ")) + "?", + translate("Did you mean one of %s?", locale, suggestionList), instance)) .build(); } else { return Response.status(Response.Status.BAD_REQUEST).type(PROBLEM_JSON_TYPE) - .entity(String.format(PROBLEM_JSON, type, "invalid city", - Response.Status.BAD_REQUEST.getStatusCode(), "no matching city found", instance)) + .entity(String.format(PROBLEM_JSON, type, + translate("invalid city", locale), + Response.Status.BAD_REQUEST.getStatusCode(), + translate("no matching city found", + locale), + instance)) .build(); } } } + + private String translate(String template, Locale language, Object... parameters) { + if (ofNullable(language).map(Locale::getLanguage).filter(GERMANY.getLanguage()::equals).isPresent()) { + switch (template) { + case "Did you mean %s?": + return "Meinten Sie %s?".formatted(parameters); + case "invalid city": + return "ungültige Stadt"; + case "no matching city found": + return "keine passende Stadt gefunden"; + case "Did you mean one of %s?": + return "Meinten Sie eine von %s?".formatted(parameters); + default: + return template.formatted(parameters); + } + } + return template.formatted(parameters); + } } diff --git a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java index 9d058f68..e0cb794a 100644 --- a/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java +++ b/address-validation-service/src/main/java/de/openknowledge/sample/address/domain/AddressRepository.java @@ -60,6 +60,11 @@ public boolean isValid(Address address) { public List findSuggestions(City city) { ZipCode zipCode = city.getZipCode(); - return cities.stream().filter(c -> c.getZipCode().equals(zipCode)).collect(toList()); + List suggestions = cities.stream().filter(c -> c.getZipCode().equals(zipCode)).collect(toList()); + if (suggestions.isEmpty()) { + CityName name = city.getCityName(); + suggestions = cities.stream().filter(c -> c.getCityNames().contains(name)).collect(toList()); + } + return suggestions; } } diff --git a/billing-service/pom.xml b/billing-service/pom.xml index 58852c5e..8d6fdd18 100644 --- a/billing-service/pom.xml +++ b/billing-service/pom.xml @@ -36,9 +36,9 @@ provided - org.apache.geronimo.specs - geronimo-validation_2.0_spec - 1.0 + jakarta.validation + jakarta.validation-api + 3.1.1 org.eclipse.microprofile.config diff --git a/customer-client/.gitignore b/customer-client/.gitignore new file mode 100644 index 00000000..c3af59d8 --- /dev/null +++ b/customer-client/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Playwright +playwright-report +test-results + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/customer-client/.prettierrc b/customer-client/.prettierrc new file mode 100644 index 00000000..4cbc711c --- /dev/null +++ b/customer-client/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2 +} diff --git a/customer-client/Dockerfile b/customer-client/Dockerfile new file mode 100644 index 00000000..4129c8bc --- /dev/null +++ b/customer-client/Dockerfile @@ -0,0 +1,41 @@ +# Build Stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY . . + +# Build argument for API URL (can be overridden at build time) +ARG VITE_API_URL=http://localhost:8181 +ENV VITE_API_URL=$VITE_API_URL + +# Build the application +RUN npm run build + +# Production Stage +FROM nginx:alpine + +# Copy built assets from builder stage +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy nginx configuration for SPA routing +RUN echo 'server { \ + listen 80; \ + server_name localhost; \ + root /usr/share/nginx/html; \ + index index.html; \ + location / { \ + try_files $uri $uri/ /index.html; \ + } \ +}' > /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/customer-client/README-APP.md b/customer-client/README-APP.md new file mode 100644 index 00000000..a2bbdd30 --- /dev/null +++ b/customer-client/README-APP.md @@ -0,0 +1,77 @@ +# Kundenpflege App + +Eine React-basierte Web-Applikation zur Kundenverwaltung mit Master-Detail-Ansicht. + +## Tech Stack + +- **React 19** - UI Framework +- **TypeScript** - Type Safety +- **Vite** - Build Tool +- **Tanstack Query** - Server State Management +- **Tanstack Table** - Tabellen-Komponente +- **Tanstack Forms** - Formular-Management +- **Zod** - Schema-Validierung +- **React Router** - Client-seitiges Routing + +## Features + +- **Kundenliste (Master-Ansicht)**: Übersicht aller Kunden mit Sortierung +- **Kundendetails (Detail-Ansicht)**: Detailansicht eines einzelnen Kunden +- **Kunde anlegen**: Formular zum Erstellen neuer Kunden +- **Adressen bearbeiten**: Rechnungs- und Lieferadresse hinzufügen/bearbeiten +- **Validierung**: Client-seitige Validierung mit Zod + +## Voraussetzungen + +Das Backend (customer-service) muss auf `http://localhost:8080` laufen. + +## Installation & Start + +```bash +# Dependencies installieren +npm install + +# Entwicklungsserver starten +npm run dev + +# Build für Produktion +npm run build + +# Build-Preview +npm run preview +``` + +## Umgebungsvariablen + +`.env` Datei: + +``` +VITE_API_URL=http://localhost:8080/api +``` + +## API-Endpunkte + +Die App nutzt folgende Backend-Endpunkte: + +- `GET /api/customers/` - Alle Kunden abrufen +- `POST /api/customers/` - Neuen Kunden erstellen +- `GET /api/customers/{customerNumber}` - Einzelnen Kunden abrufen +- `PUT /api/customers/{customerNumber}/billing-address` - Rechnungsadresse aktualisieren +- `PUT /api/customers/{customerNumber}/delivery-address` - Lieferadresse aktualisieren + +## Projektstruktur + +``` +src/ +├── api/ # API-Client und React Query Hooks +├── components/ # Wiederverwendbare Komponenten (Formulare) +├── pages/ # Seiten-Komponenten (Routen) +├── types/ # TypeScript-Typen und Zod-Schemas +├── App.tsx # Haupt-App-Komponente mit Routing +├── App.css # Globale Styles +└── main.tsx # Entry Point +``` + +## Entwickelt mit + +Claude Code - AI-gestützte Entwicklung diff --git a/customer-client/README.md b/customer-client/README.md new file mode 100644 index 00000000..c987b941 --- /dev/null +++ b/customer-client/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]); +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x'; +import reactDom from 'eslint-plugin-react-dom'; + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]); +``` diff --git a/customer-client/eslint.config.js b/customer-client/eslint.config.js new file mode 100644 index 00000000..e60ff885 --- /dev/null +++ b/customer-client/eslint.config.js @@ -0,0 +1,25 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; +import { defineConfig, globalIgnores } from 'eslint/config'; +import eslintConfigPrettier from 'eslint-config-prettier'; + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + eslintConfigPrettier, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]); diff --git a/customer-client/index.html b/customer-client/index.html new file mode 100644 index 00000000..f89fa110 --- /dev/null +++ b/customer-client/index.html @@ -0,0 +1,13 @@ + + + + + + + customer-client + + +
+ + + diff --git a/customer-client/package-lock.json b/customer-client/package-lock.json new file mode 100644 index 00000000..63fd3ed0 --- /dev/null +++ b/customer-client/package-lock.json @@ -0,0 +1,3628 @@ +{ + "name": "customer-client", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "customer-client", + "version": "0.0.0", + "dependencies": { + "@tanstack/react-form": "^1.28.0", + "@tanstack/react-query": "^5.90.20", + "@tanstack/react-table": "^8.21.3", + "@tanstack/zod-form-adapter": "^0.42.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.0", + "zod": "^3.25.76" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@playwright/test": "^1.49.0", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "prettier": "^3.8.1", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", + "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tanstack/devtools-event-client": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@tanstack/devtools-event-client/-/devtools-event-client-0.4.0.tgz", + "integrity": "sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/form-core": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-1.28.0.tgz", + "integrity": "sha512-MX3YveB6SKHAJ2yUwp+Ca/PCguub8bVEnLcLUbFLwdkSRMkP0lMGdaZl+F0JuEgZw56c6iFoRyfILhS7OQpydA==", + "license": "MIT", + "dependencies": { + "@tanstack/devtools-event-client": "^0.4.0", + "@tanstack/pacer-lite": "^0.1.1", + "@tanstack/store": "^0.7.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/pacer-lite": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@tanstack/pacer-lite/-/pacer-lite-0.1.1.tgz", + "integrity": "sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-form": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-form/-/react-form-1.28.0.tgz", + "integrity": "sha512-ibLcf5QkTogV0Ly944CuqGxWTpHyreNA4Cy8Wtky7zE9wtE3HVapQt4/hUuXo51zihfTkv5URiXpoTSKF5Xosg==", + "license": "MIT", + "dependencies": { + "@tanstack/form-core": "1.28.0", + "@tanstack/react-store": "^0.8.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@tanstack/react-start": { + "optional": true + } + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.20.tgz", + "integrity": "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-store": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.8.0.tgz", + "integrity": "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "0.8.0", + "use-sync-external-store": "^1.6.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/react-store/node_modules/@tanstack/store": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.8.0.tgz", + "integrity": "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/store": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.7.tgz", + "integrity": "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/zod-form-adapter": { + "version": "0.42.1", + "resolved": "https://registry.npmjs.org/@tanstack/zod-form-adapter/-/zod-form-adapter-0.42.1.tgz", + "integrity": "sha512-hPRM0lawVKP64yurW4c6KHZH6altMo2MQN14hfi+GMBTKjO9S7bW1x5LPZ5cayoJE3mBvdlahpSGT5rYZtSbXQ==", + "license": "MIT", + "dependencies": { + "@tanstack/form-core": "0.42.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "zod": "^3.x" + } + }, + "node_modules/@tanstack/zod-form-adapter/node_modules/@tanstack/form-core": { + "version": "0.42.1", + "resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-0.42.1.tgz", + "integrity": "sha512-jTU0jyHqFceujdtPNv3jPVej1dTqBwa8TYdIyWB5BCwRVUBZEp1PiYEBkC9r92xu5fMpBiKc+JKud3eeVjuMiA==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "^0.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.10.tgz", + "integrity": "sha512-+0/4J266CBGPUq/ELg7QUHhN25WYjE0wYTPSQJn1xeu8DOlIOPxXxrNGiLmfAWl7HMMgWFWXpt9IDjMWrF5Iow==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", + "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", + "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/type-utils": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.54.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", + "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", + "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.54.0", + "@typescript-eslint/types": "^8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", + "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", + "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", + "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", + "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", + "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.54.0", + "@typescript-eslint/tsconfig-utils": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", + "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", + "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.3.tgz", + "integrity": "sha512-NVUnA6gQCl8jfoYqKqQU5Clv0aPw14KkZYCsX6T9Lfu9slI0LOU10OTwFHS/WmptsMMpshNd/1tuWsHQ2Uk+cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.2", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001767", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz", + "integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", + "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz", + "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz", + "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.54.0", + "@typescript-eslint/parser": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/customer-client/package.json b/customer-client/package.json new file mode 100644 index 00000000..7930bdb5 --- /dev/null +++ b/customer-client/package.json @@ -0,0 +1,43 @@ +{ + "name": "customer-client", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "format": "prettier --write .", + "format:check": "prettier --check .", + "preview": "vite preview", + "test": "playwright test", + "test:ui": "playwright test --ui" + }, + "dependencies": { + "@tanstack/react-form": "^1.28.0", + "@tanstack/react-query": "^5.90.20", + "@tanstack/react-table": "^8.21.3", + "@tanstack/zod-form-adapter": "^0.42.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.0", + "zod": "^3.25.76" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "prettier": "^3.8.1", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4", + "@playwright/test": "^1.49.0" + } +} diff --git a/customer-client/playwright.config.ts b/customer-client/playwright.config.ts new file mode 100644 index 00000000..766da8cc --- /dev/null +++ b/customer-client/playwright.config.ts @@ -0,0 +1,27 @@ +import { defineConfig, devices } from '@playwright/test'; + +process.env.VITE_API_URL ??= 'http://localhost:8181'; + +export default defineConfig({ + testDir: './test', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 1, + reporter: 'html', + use: { + baseURL: 'http://localhost:5173', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + command: 'npm run dev', + url: 'http://localhost:5173', + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/customer-client/public/vite.svg b/customer-client/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/customer-client/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/customer-client/src/App.css b/customer-client/src/App.css new file mode 100644 index 00000000..109bafda --- /dev/null +++ b/customer-client/src/App.css @@ -0,0 +1,371 @@ +/* CSS Variables */ +:root { + --color-primary: #006491; + --color-primary-hover: #004d6e; + --color-primary-dark: #005a8d; + --color-text-primary: #1f1f1f; + --color-text-secondary: #5a5a52; + --color-text-on-dark: #ffffff; + --color-border: #d4d4d0; + --color-error: #c91e70; + --color-error-on-dark: #ff6b9d; + --color-background: #f5f5f5; +} + +/* App Layout */ +.app { + min-height: 100vh; + display: flex; + flex-direction: column; + width: 100%; +} + +.app-header { + background-color: var(--color-primary-dark); + padding: 1.5rem 2rem; + border-bottom: 2px solid var(--color-primary); +} + +.app-header h1 { + margin: 0; + color: #fff; + font-size: 1.8rem; +} + +.app-main { + flex: 1; + max-width: 1200px; + width: 100%; + margin: 0 auto; + padding: 2rem; + box-sizing: border-box; +} + +/* Buttons */ +.button { + padding: 0.6rem 1.2rem; + border: 1px solid var(--color-primary); + background-color: transparent; + color: var(--color-primary); + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + transition: all 0.3s; +} + +.button:hover { + background-color: var(--color-primary-hover); + color: white; +} + +.button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.button-primary { + background-color: var(--color-primary); + color: white; +} + +.button-primary:hover { + background-color: var(--color-primary-hover); +} + +.button-small { + padding: 0.4rem 0.8rem; + font-size: 0.9rem; +} + +.button-back { + margin-bottom: 1rem; +} + +/* Customer List */ +.customer-list { + padding: 1rem; + width: 100%; + box-sizing: border-box; + overflow-x: auto; +} + +.customer-list-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.customer-list-header h2 { + margin: 0; +} + +/* Table */ +.table { + width: 100%; + min-width: 600px; + border-collapse: collapse; + background-color: var(--color-primary-dark); + border-radius: 8px; + overflow: hidden; + color: var(--color-text-on-dark); +} + +.table thead { + background-color: var(--color-primary-dark); +} + +.table th { + padding: 1rem; + text-align: left; + font-weight: 600; + border-bottom: 2px solid var(--color-primary); +} + +.table td { + padding: 1rem; + border-bottom: 1px solid var(--color-border); +} + +.table tbody tr:last-child td { + border-bottom: none; +} + +.table-row-clickable { + cursor: pointer; + transition: background-color 0.2s; +} + +.table-row-clickable:hover { + background-color: var(--color-primary-dark); +} + +.cursor-pointer { + cursor: pointer; +} + +.select-none { + user-select: none; +} + +/* Customer Detail */ +.customer-detail { + padding: 1rem; +} + +.customer-detail-header { + margin-bottom: 2rem; +} + +.customer-info { + margin-bottom: 2rem; +} + +.info-row { + padding: 0.5rem 0; + display: flex; + gap: 1rem; +} + +.info-row strong { + min-width: 140px; + color: var(--color-text-on-dark); +} + +/* Addresses */ +.addresses { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 2rem; +} + +/* Additional address-section styles */ + +.address-section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--color-border); +} + +.address-section-header h3 { + margin: 0; +} + +/* Dark background containers (shared styles) */ +.customer-info, +.address-section, +.form-container { + background-color: var(--color-primary-dark); + padding: 1.5rem; + border-radius: 8px; + color: var(--color-text-on-dark); +} + +/* Dark background form styles */ +.address-section .form-field label, +.form-container .form-field label { + color: var(--color-text-on-dark); +} + +.address-section .button, +.form-container .button { + border-color: var(--color-text-on-dark); + color: var(--color-text-on-dark); +} + +.address-section .button:hover, +.form-container .button:hover { + background-color: var(--color-primary-hover); + color: var(--color-text-on-dark); +} + +.form-container .button-primary { + background-color: var(--color-primary); + border-color: var(--color-primary); +} + +.form-container .button-primary:hover { + background-color: var(--color-primary-hover); +} + +.address-section .input-error, +.form-container .input-error { + border-color: var(--color-error-on-dark) !important; +} + +.address-section .field-error, +.form-container .field-error { + color: var(--color-error-on-dark); +} + +.address-section .error-message, +.form-container .error-message { + background-color: rgba(255, 107, 157, 0.2); + border: 1px solid var(--color-error-on-dark); + color: var(--color-error-on-dark); +} + +.address-display p { + margin: 0.5rem 0; +} + +.no-address { + color: var(--color-text-on-dark); + font-style: italic; +} + +/* Forms */ +.form-container { + max-width: 600px; + margin: 0 auto; +} + +.form-container h2 { + margin-top: 0; + color: var(--color-text-on-dark); +} + +.form-field { + margin-bottom: 1.5rem; +} + +.form-field label { + display: block; + margin-bottom: 0.5rem; + color: var(--color-text-secondary); + font-weight: 500; +} + +.form-field input { + width: 100%; + padding: 0.6rem; + border: 1px solid var(--color-border); + background-color: var(--color-primary-dark); + color: var(--color-text-on-dark); + border-radius: 4px; + font-size: 1rem; + box-sizing: border-box; +} + +.form-field input:focus { + outline: none; + border-color: var(--color-primary); +} + +.input-error { + border-color: var(--color-error) !important; +} + +.field-error { + display: block; + margin-top: 0.3rem; + color: var(--color-error); + font-size: 0.875rem; +} + +.form-actions { + display: flex; + gap: 1rem; + justify-content: flex-end; + margin-top: 2rem; +} + +.address-form { + margin-top: 1rem; +} + +/* States */ +.loading, +.error { + text-align: center; + padding: 3rem; +} + +.error { + color: var(--color-error); +} + +.error-message { + background-color: rgba(201, 30, 112, 0.1); + border: 1px solid var(--color-error); + color: var(--color-error); + padding: 1rem; + border-radius: 4px; + margin-bottom: 1rem; +} + +.empty-state { + text-align: center; + color: var(--color-text-secondary); + padding: 2rem; + font-style: italic; +} + +/* Create Customer */ +.create-customer { + padding: 1rem; +} + +/* Responsive */ +@media (max-width: 768px) { + .addresses { + grid-template-columns: 1fr; + } + + .customer-list-header { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .form-actions { + flex-direction: column; + } + + .form-actions button { + width: 100%; + } +} diff --git a/customer-client/src/App.tsx b/customer-client/src/App.tsx new file mode 100644 index 00000000..7399cc98 --- /dev/null +++ b/customer-client/src/App.tsx @@ -0,0 +1,39 @@ +/* + * Copyright 2026 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Routes, Route } from 'react-router-dom'; +import './App.css'; +import CustomerList from './pages/CustomerList'; +import CustomerDetail from './pages/CustomerDetail'; +import CreateCustomer from './pages/CreateCustomer'; + +function App() { + return ( +
+
+

Kundenpflege

+
+
+ + } /> + } /> + } /> + +
+
+ ); +} + +export default App; diff --git a/customer-client/src/api/client.ts b/customer-client/src/api/client.ts new file mode 100644 index 00000000..f7d5fe2d --- /dev/null +++ b/customer-client/src/api/client.ts @@ -0,0 +1,89 @@ +/* + * Copyright 2026 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { Customer, Address, CreateCustomerInput } from '../types/customer'; + +const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api'; + +async function fetchApi(endpoint: string, options?: RequestInit): Promise { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + ...options, + headers: { + 'Content-Type': 'application/json', + 'Accept-Language': 'de', + ...options?.headers, + }, + }); + + if (!response.ok) { + let errorMessage = `HTTP Error ${response.status}`; + + try { + const errorText = await response.text(); + + // Try to parse as Problem JSON + const problemJson = JSON.parse(errorText); + + // Extract detail field if present + if (problemJson && typeof problemJson === 'object' && 'detail' in problemJson) { + errorMessage = problemJson.detail; + } else { + errorMessage = errorText; + } + } catch { + // If parsing fails, keep the default error message + } + + throw new Error(errorMessage); + } + + if (response.status === 204 || response.status === 201) { + return {} as T; + } + + const text = await response.text(); + return text ? JSON.parse(text) : ({} as T); +} + +export const customerApi = { + getCustomers: async (): Promise => { + return fetchApi('/customers/'); + }, + + getCustomer: async (customerNumber: string): Promise => { + return fetchApi(`/customers/${customerNumber}`); + }, + + createCustomer: async (customer: CreateCustomerInput): Promise => { + await fetchApi('/customers/', { + method: 'POST', + body: JSON.stringify(customer), + }); + }, + + updateBillingAddress: async (customerNumber: string, address: Address): Promise => { + await fetchApi(`/customers/${customerNumber}/billing-address`, { + method: 'PUT', + body: JSON.stringify(address), + }); + }, + + updateDeliveryAddress: async (customerNumber: string, address: Address): Promise => { + await fetchApi(`/customers/${customerNumber}/delivery-address`, { + method: 'PUT', + body: JSON.stringify(address), + }); + }, +}; diff --git a/customer-client/src/api/hooks.ts b/customer-client/src/api/hooks.ts new file mode 100644 index 00000000..2d5ed812 --- /dev/null +++ b/customer-client/src/api/hooks.ts @@ -0,0 +1,70 @@ +/* + * Copyright 2026 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { customerApi } from './client'; +import type { Address, CreateCustomerInput } from '../types/customer'; + +export const useCustomers = () => { + return useQuery({ + queryKey: ['customers'], + queryFn: customerApi.getCustomers, + }); +}; + +export const useCustomer = (customerNumber: string | undefined) => { + return useQuery({ + queryKey: ['customer', customerNumber], + queryFn: () => customerApi.getCustomer(customerNumber!), + enabled: !!customerNumber, + }); +}; + +export const useCreateCustomer = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (customer: CreateCustomerInput) => customerApi.createCustomer(customer), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['customers'] }); + }, + }); +}; + +export const useUpdateBillingAddress = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ customerNumber, address }: { customerNumber: string; address: Address }) => + customerApi.updateBillingAddress(customerNumber, address), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ['customer', variables.customerNumber] }); + queryClient.invalidateQueries({ queryKey: ['customers'] }); + }, + }); +}; + +export const useUpdateDeliveryAddress = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ customerNumber, address }: { customerNumber: string; address: Address }) => + customerApi.updateDeliveryAddress(customerNumber, address), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ['customer', variables.customerNumber] }); + queryClient.invalidateQueries({ queryKey: ['customers'] }); + }, + }); +}; diff --git a/customer-client/src/assets/react.svg b/customer-client/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/customer-client/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/customer-client/src/components/BillingAddressForm.tsx b/customer-client/src/components/BillingAddressForm.tsx new file mode 100644 index 00000000..c6a364b0 --- /dev/null +++ b/customer-client/src/components/BillingAddressForm.tsx @@ -0,0 +1,217 @@ +/* + * Copyright 2026 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useForm } from '@tanstack/react-form'; +import { useState } from 'react'; +import { useUpdateBillingAddress } from '../api/hooks'; +import { addressSchema } from '../types/customer'; +import type { Address } from '../types/customer'; +import { z, type ZodIssue } from 'zod'; + +interface BillingAddressFormProps { + customerNumber: string; + initialAddress?: Address; + onSuccess: () => void; + onCancel: () => void; +} + +export default function BillingAddressForm({ + customerNumber, + initialAddress, + onSuccess, + onCancel, +}: BillingAddressFormProps) { + const updateBillingAddress = useUpdateBillingAddress(); + const [submitError, setSubmitError] = useState(null); + + const form = useForm({ + defaultValues: { + recipient: initialAddress?.recipient || '', + streetName: initialAddress?.street?.name || '', + houseNumber: initialAddress?.street?.number || '', + city: initialAddress?.city || '', + }, + onSubmit: async ({ value }) => { + try { + setSubmitError(null); + const address: Address = { + recipient: value.recipient, + street: + value.streetName || value.houseNumber + ? { + name: value.streetName, + number: value.houseNumber, + } + : undefined, + city: value.city || undefined, + }; + await updateBillingAddress.mutateAsync({ customerNumber, address }); + onSuccess(); + } catch (error) { + setSubmitError( + error instanceof Error ? error.message : 'Fehler beim Speichern der Adresse', + ); + } + }, + }); + + return ( +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + > + + {(field) => ( +
+ + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + placeholder="Name des Empfängers" + className={field.state.meta.errors.length > 0 ? 'input-error' : ''} + /> + {field.state.meta.errors.length > 0 && ( + + {typeof field.state.meta.errors[0] === 'string' + ? field.state.meta.errors[0] + : (field.state.meta.errors[0] as ZodIssue).message} + + )} +
+ )} +
+ + { + const houseNumber = fieldApi.form.getFieldValue('houseNumber'); + return z + .string() + .refine( + (v) => !houseNumber || v.length > 0, + 'Straße ist erforderlich wenn Hausnummer angegeben', + ) + .safeParse(value).error?.issues[0]; + }, + }} + > + {(field) => ( +
+ + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + placeholder="Straßenname" + className={field.state.meta.errors.length > 0 ? 'input-error' : ''} + /> + {field.state.meta.errors.length > 0 && ( + + {typeof field.state.meta.errors[0] === 'string' + ? field.state.meta.errors[0] + : (field.state.meta.errors[0] as ZodIssue).message} + + )} +
+ )} +
+ + { + const streetName = fieldApi.form.getFieldValue('streetName'); + return z + .string() + .refine( + (v) => !streetName || v.length > 0, + 'Hausnummer ist erforderlich wenn Straße angegeben', + ) + .safeParse(value).error?.issues[0]; + }, + }} + > + {(field) => ( +
+ + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + placeholder="Hausnummer" + className={field.state.meta.errors.length > 0 ? 'input-error' : ''} + /> + {field.state.meta.errors.length > 0 && ( + + {typeof field.state.meta.errors[0] === 'string' + ? field.state.meta.errors[0] + : (field.state.meta.errors[0] as ZodIssue).message} + + )} +
+ )} +
+ + + {(field) => ( +
+ + field.handleChange(e.target.value)} + placeholder="z.B. 10115 Berlin oder SE1 7TP London" + /> +
+ )} +
+ + {submitError &&
{submitError}
} + +
+ + +
+
+ ); +} diff --git a/customer-client/src/components/DeliveryAddressForm.tsx b/customer-client/src/components/DeliveryAddressForm.tsx new file mode 100644 index 00000000..f7ac2670 --- /dev/null +++ b/customer-client/src/components/DeliveryAddressForm.tsx @@ -0,0 +1,250 @@ +/* + * Copyright 2026 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useForm } from '@tanstack/react-form'; +import { useState } from 'react'; +import { useUpdateDeliveryAddress } from '../api/hooks'; +import { addressSchema } from '../types/customer'; +import type { Address } from '../types/customer'; +import { z, type ZodIssue } from 'zod'; + +interface DeliveryAddressFormProps { + customerNumber: string; + initialAddress?: Address; + onSuccess: () => void; + onCancel: () => void; +} + +export default function DeliveryAddressForm({ + customerNumber, + initialAddress, + onSuccess, + onCancel, +}: DeliveryAddressFormProps) { + const updateDeliveryAddress = useUpdateDeliveryAddress(); + const [submitError, setSubmitError] = useState(null); + + const form = useForm({ + defaultValues: { + recipient: initialAddress?.recipient || '', + streetName: initialAddress?.street?.name || '', + houseNumber: initialAddress?.street?.number || '', + zipCode: initialAddress?.city?.split(' ')[0] || '', + cityName: initialAddress?.city?.split(' ').slice(1).join(' ') || '', + }, + onSubmit: async ({ value }) => { + try { + setSubmitError(null); + const address: Address = { + recipient: value.recipient, + street: + value.streetName || value.houseNumber + ? { + name: value.streetName, + number: value.houseNumber, + } + : undefined, + city: value.zipCode && value.cityName ? `${value.zipCode} ${value.cityName}` : undefined, + }; + await updateDeliveryAddress.mutateAsync({ customerNumber, address }); + onSuccess(); + } catch (error) { + setSubmitError( + error instanceof Error ? error.message : 'Fehler beim Speichern der Adresse', + ); + } + }, + }); + + return ( +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + > + + {(field) => ( +
+ + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + placeholder="Name des Empfängers" + className={field.state.meta.errors.length > 0 ? 'input-error' : ''} + /> + {field.state.meta.errors.length > 0 && ( + + {typeof field.state.meta.errors[0] === 'string' + ? field.state.meta.errors[0] + : (field.state.meta.errors[0] as ZodIssue).message} + + )} +
+ )} +
+ + { + const houseNumber = fieldApi.form.getFieldValue('houseNumber'); + return z + .string() + .refine( + (v) => !houseNumber || v.length > 0, + 'Straße ist erforderlich wenn Hausnummer angegeben', + ) + .safeParse(value).error?.issues[0]; + }, + }} + > + {(field) => ( +
+ + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + placeholder="Straßenname" + className={field.state.meta.errors.length > 0 ? 'input-error' : ''} + /> + {field.state.meta.errors.length > 0 && ( + + {typeof field.state.meta.errors[0] === 'string' + ? field.state.meta.errors[0] + : (field.state.meta.errors[0] as ZodIssue).message} + + )} +
+ )} +
+ + { + const streetName = fieldApi.form.getFieldValue('streetName'); + return z + .string() + .refine( + (v) => !streetName || v.length > 0, + 'Hausnummer ist erforderlich wenn Straße angegeben', + ) + .safeParse(value).error?.issues[0]; + }, + }} + > + {(field) => ( +
+ + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + placeholder="Hausnummer" + className={field.state.meta.errors.length > 0 ? 'input-error' : ''} + /> + {field.state.meta.errors.length > 0 && ( + + {typeof field.state.meta.errors[0] === 'string' + ? field.state.meta.errors[0] + : (field.state.meta.errors[0] as ZodIssue).message} + + )} +
+ )} +
+ + + {(field) => ( +
+ + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + placeholder="Postleitzahl" + className={field.state.meta.errors.length > 0 ? 'input-error' : ''} + /> + {field.state.meta.errors.length > 0 && ( + + {typeof field.state.meta.errors[0] === 'string' + ? field.state.meta.errors[0] + : (field.state.meta.errors[0] as ZodIssue).message} + + )} +
+ )} +
+ + + {(field) => ( +
+ + field.handleChange(e.target.value)} + placeholder="Stadtname" + /> +
+ )} +
+ + {submitError &&
{submitError}
} + +
+ + +
+
+ ); +} diff --git a/customer-client/src/index.css b/customer-client/src/index.css new file mode 100644 index 00000000..ac336eb3 --- /dev/null +++ b/customer-client/src/index.css @@ -0,0 +1,66 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #1a1a1a; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #006491; + text-decoration: inherit; +} +a:hover { + color: #006491; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #006491; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #1f1f1f; + background-color: #f5f5f5; + } + a:hover { + color: #006491; + } + button { + background-color: #f9f9f9; + } +} diff --git a/customer-client/src/main.tsx b/customer-client/src/main.tsx new file mode 100644 index 00000000..5ad79f10 --- /dev/null +++ b/customer-client/src/main.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2026 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { BrowserRouter } from 'react-router-dom'; +import './index.css'; +import App from './App.tsx'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}); + +createRoot(document.getElementById('root')!).render( + + + + + + + , +); diff --git a/customer-client/src/pages/CreateCustomer.tsx b/customer-client/src/pages/CreateCustomer.tsx new file mode 100644 index 00000000..60d6227f --- /dev/null +++ b/customer-client/src/pages/CreateCustomer.tsx @@ -0,0 +1,108 @@ +/* + * Copyright 2026 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useNavigate } from 'react-router-dom'; +import { useForm } from '@tanstack/react-form'; +import { useState } from 'react'; +import { useCreateCustomer } from '../api/hooks'; +import { createCustomerSchema } from '../types/customer'; +import type { ZodIssue } from 'zod'; + +export default function CreateCustomer() { + const navigate = useNavigate(); + const createCustomer = useCreateCustomer(); + const [submitError, setSubmitError] = useState(null); + + const form = useForm({ + defaultValues: { + name: '', + }, + onSubmit: async ({ value }) => { + try { + setSubmitError(null); + await createCustomer.mutateAsync({ + name: value.name, + }); + navigate('/'); + } catch (error) { + setSubmitError(error instanceof Error ? error.message : 'Fehler beim Erstellen des Kunden'); + } + }, + }); + + return ( +
+ + +
+

Neuer Kunde

+ +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + > + + {(field) => ( +
+ + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + placeholder="Kundenname" + className={field.state.meta.errors.length > 0 ? 'input-error' : ''} + /> + {field.state.meta.errors.length > 0 && ( + + {typeof field.state.meta.errors[0] === 'string' + ? field.state.meta.errors[0] + : (field.state.meta.errors[0] as ZodIssue).message} + + )} +
+ )} +
+ + {submitError &&
{submitError}
} + +
+ + +
+
+
+
+ ); +} diff --git a/customer-client/src/pages/CustomerDetail.tsx b/customer-client/src/pages/CustomerDetail.tsx new file mode 100644 index 00000000..a90ca3e5 --- /dev/null +++ b/customer-client/src/pages/CustomerDetail.tsx @@ -0,0 +1,148 @@ +/* + * Copyright 2026 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useParams, useNavigate } from 'react-router-dom'; +import { useState } from 'react'; +import { useCustomer } from '../api/hooks'; +import BillingAddressForm from '../components/BillingAddressForm'; +import DeliveryAddressForm from '../components/DeliveryAddressForm'; +import type { Address } from '../types/customer'; + +export default function CustomerDetail() { + const { customerNumber } = useParams<{ customerNumber: string }>(); + const navigate = useNavigate(); + const { data: customer, isLoading, error } = useCustomer(customerNumber); + const [editingBilling, setEditingBilling] = useState(false); + const [editingDelivery, setEditingDelivery] = useState(false); + + if (isLoading) { + return ( +
+

Lade Kundendetails...

+
+ ); + } + + if (error) { + return ( +
+

Fehler beim Laden des Kunden: {error.message}

+ +
+ ); + } + + if (!customer) { + return ( +
+

Kunde nicht gefunden

+ +
+ ); + } + + const renderAddress = (address: Address | undefined) => { + if (!address) { + return

Keine Adresse hinterlegt

; + } + + return ( +
+

+ Empfänger: {address.recipient} +

+ {address.street && ( +

+ Straße: {address.street.name} {address.street.number} +

+ )} + {address.city && ( +

+ Ort: {address.city} +

+ )} +
+ ); + }; + + return ( +
+ + +
+

Kundendetails

+
+ +
+
+ Kundennummer: {customer.number} +
+
+ Name: {customer.name} +
+
+ +
+
+
+

Rechnungsadresse

+ {!editingBilling && ( + + )} +
+ {editingBilling ? ( + setEditingBilling(false)} + onCancel={() => setEditingBilling(false)} + /> + ) : ( + renderAddress(customer.billingAddress) + )} +
+ +
+
+

Lieferadresse

+ {!editingDelivery && ( + + )} +
+ {editingDelivery ? ( + setEditingDelivery(false)} + onCancel={() => setEditingDelivery(false)} + /> + ) : ( + renderAddress(customer.deliveryAddress) + )} +
+
+
+ ); +} diff --git a/customer-client/src/pages/CustomerList.tsx b/customer-client/src/pages/CustomerList.tsx new file mode 100644 index 00000000..c05bb21a --- /dev/null +++ b/customer-client/src/pages/CustomerList.tsx @@ -0,0 +1,126 @@ +/* + * Copyright 2026 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useNavigate } from 'react-router-dom'; +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + flexRender, + createColumnHelper, + type SortingState, +} from '@tanstack/react-table'; +import { useState } from 'react'; +import { useCustomers } from '../api/hooks'; +import type { Customer } from '../types/customer'; + +const columnHelper = createColumnHelper(); + +const columns = [ + columnHelper.accessor('number', { + header: 'Kundennummer', + cell: (info) => info.getValue(), + }), + columnHelper.accessor('name', { + header: 'Name', + cell: (info) => info.getValue(), + }), +]; + +export default function CustomerList() { + const navigate = useNavigate(); + const { data: customers, isLoading, error } = useCustomers(); + const [sorting, setSorting] = useState([]); + + const table = useReactTable({ + data: customers || [], + columns, + state: { + sorting, + }, + onSortingChange: setSorting, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + }); + + if (isLoading) { + return ( +
+

Lade Kunden...

+
+ ); + } + + if (error) { + return ( +
+

Fehler beim Laden der Kunden: {error.message}

+
+ ); + } + + return ( +
+
+

Kundenliste

+ +
+ + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + navigate(`/customers/${row.original.number}`)} + > + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : ( +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: ' 🔼', + desc: ' 🔽', + }[header.column.getIsSorted() as string] ?? null} +
+ )} +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+ + {(!customers || customers.length === 0) && ( +

Keine Kunden vorhanden

+ )} +
+ ); +} diff --git a/customer-client/src/types/customer.ts b/customer-client/src/types/customer.ts new file mode 100644 index 00000000..0f2b227c --- /dev/null +++ b/customer-client/src/types/customer.ts @@ -0,0 +1,54 @@ +/* + * Copyright 2026 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { z } from 'zod'; + +// Zod Schemas for validation +export const streetSchema = z.object({ + name: z.string().min(1, 'Straßenname ist erforderlich'), + number: z.string().min(1, 'Hausnummer ist erforderlich'), +}); + +export const citySchema = z.string().min(1, 'Stadt ist erforderlich'); + +export const addressSchema = z.object({ + recipient: z.string().min(1, 'Empfänger ist erforderlich'), + street: streetSchema.optional(), + city: citySchema.optional(), +}); + +export const customerNameSchema = z.string().min(1, 'Name ist erforderlich'); + +export const customerNumberSchema = z.string(); + +export const customerSchema = z.object({ + number: customerNumberSchema.optional(), + name: customerNameSchema, + billingAddress: addressSchema.optional(), + deliveryAddress: addressSchema.optional(), +}); + +export const createCustomerSchema = z.object({ + name: customerNameSchema, +}); + +// TypeScript Types +export type Street = z.infer; +export type City = z.infer; +export type Address = z.infer; +export type CustomerName = z.infer; +export type CustomerNumber = z.infer; +export type Customer = z.infer; +export type CreateCustomerInput = z.infer; diff --git a/customer-client/test/create-customer.spec.ts b/customer-client/test/create-customer.spec.ts new file mode 100644 index 00000000..8e1f7b2f --- /dev/null +++ b/customer-client/test/create-customer.spec.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2026 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { test, expect } from '@playwright/test'; + +test.describe('Neuer Kunde', () => { + test('zeigt Fehler bei leerem Namen', async ({ page }) => { + // Given + await page.goto('/customers/new'); + + // When + await page.getByLabel('Name *').fill('Max'); + await page.getByLabel('Name *').fill(''); + + // Then + await expect(page.locator('.field-error')).toContainText('Name ist erforderlich'); + }); + + test('erstellt Kunden erfolgreich und navigiert zur Liste', async ({ page }) => { + // Given + await page.goto('/customers/new'); + + // When + await page.getByLabel('Name *').fill('Test Kunde'); + await page.getByRole('button', { name: 'Kunde erstellen' }).click(); + + await expect(page.getByRole('cell', { name: 'Test Kunde' })).toBeVisible(); + }); +}); diff --git a/customer-client/test/customer-detail.spec.ts b/customer-client/test/customer-detail.spec.ts new file mode 100644 index 00000000..dfba492b --- /dev/null +++ b/customer-client/test/customer-detail.spec.ts @@ -0,0 +1,253 @@ +/* + * Copyright 2026 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { test, expect } from '@playwright/test'; + +test.describe('Adressen anlegen', () => { + test('Rechnungsadresse anlegen', async ({ page }) => { + // Given + await page.goto('/customers/007'); + await expect(page.getByText('007')).toBeVisible(); + await expect(page.getByText('Name: James Bond')).toBeVisible(); + + // When + const billingSection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Rechnungsadresse' }), + }); + await expect(billingSection.getByText('Keine Adresse hinterlegt')).toBeVisible(); + await billingSection.getByRole('button', { name: 'Hinzufügen' }).click(); + await billingSection.getByRole('textbox', { name: 'Empfänger *' }).fill('James Bond'); + await billingSection.getByRole('textbox', { name: 'Straße' }).fill('Albert Embankment'); + await billingSection.getByRole('textbox', { name: 'Hausnummer' }).fill('85'); + await billingSection.getByRole('textbox', { name: 'PLZ / Ort' }).fill('SE1 7TP London'); + await billingSection.getByRole('button', { name: 'Speichern' }).click(); + + // Then + await expect(billingSection.getByRole('button', { name: 'Speichern' })).not.toBeVisible(); + await expect(billingSection.getByRole('button', { name: 'Hinzufügen' })).not.toBeVisible(); + }); + + test('Lieferadresse anlegen', async ({ page }) => { + // Given + await page.goto('/customers/007'); + await expect(page.getByText('007')).toBeVisible(); + await expect(page.getByText('Name: James Bond')).toBeVisible(); + + // When + const deliverySection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Lieferadresse' }), + }); + await expect(deliverySection.getByText('Keine Adresse hinterlegt')).toBeVisible(); + await deliverySection.getByRole('button', { name: 'Hinzufügen' }).click(); + await deliverySection.getByRole('textbox', { name: 'Empfänger *' }).fill('James Bond'); + await deliverySection.getByRole('textbox', { name: 'Straße' }).fill('Chausseestraße'); + await deliverySection.getByRole('textbox', { name: 'Hausnummer' }).fill('96 - 99a'); + await deliverySection.getByRole('textbox', { name: 'PLZ' }).fill('10115'); + await deliverySection.getByRole('textbox', { name: 'Stadt' }).fill('Berlin Mitte'); + await deliverySection.getByRole('button', { name: 'Speichern' }).click(); + + // Then + await expect(deliverySection.getByRole('button', { name: 'Speichern' })).not.toBeVisible(); + await expect(deliverySection.getByRole('button', { name: 'Hinzufügen' })).not.toBeVisible(); + }); +}); + +test.describe('Adressen bearbeiten', () => { + test('Rechnungsadresse: Straße ohne Hausnummer zeigt Fehlermeldung', async ({ page }) => { + // Given + await page.goto('/customers/0815'); + const billingSection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Rechnungsadresse' }), + }); + await billingSection.getByRole('button', { name: 'Bearbeiten' }).click(); + + // When + await billingSection.getByRole('textbox', { name: 'Hausnummer' }).fill(''); + + // Then + await expect( + billingSection.getByText('Hausnummer ist erforderlich wenn Straße angegeben'), + ).toBeVisible(); + }); + + test('Rechnungsadresse: Hausnummer ohne Straße zeigt Fehlermeldung', async ({ page }) => { + // Given + await page.goto('/customers/0815'); + const billingSection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Rechnungsadresse' }), + }); + await billingSection.getByRole('button', { name: 'Bearbeiten' }).click(); + + // When + await billingSection.getByRole('textbox', { name: 'Straße' }).fill(''); + + // Then + await expect( + billingSection.getByText('Straße ist erforderlich wenn Hausnummer angegeben'), + ).toBeVisible(); + }); + + test('Lieferadresse: Straße ohne Hausnummer zeigt Fehlermeldung', async ({ page }) => { + // Given + await page.goto('/customers/0815'); + const deliverySection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Lieferadresse' }), + }); + await deliverySection.getByRole('button', { name: 'Bearbeiten' }).click(); + + // When + await deliverySection.getByRole('textbox', { name: 'Hausnummer' }).fill(''); + + // Then + await expect( + deliverySection.getByText('Hausnummer ist erforderlich wenn Straße angegeben'), + ).toBeVisible(); + }); + + test('Lieferadresse: Hausnummer ohne Straße zeigt Fehlermeldung', async ({ page }) => { + // Given + await page.goto('/customers/0815'); + const deliverySection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Lieferadresse' }), + }); + await deliverySection.getByRole('button', { name: 'Bearbeiten' }).click(); + + // When + await deliverySection.getByRole('textbox', { name: 'Straße' }).fill(''); + + // Then + await expect( + deliverySection.getByText('Straße ist erforderlich wenn Hausnummer angegeben'), + ).toBeVisible(); + }); + + test('Lieferadresse: PLZ existiert nicht zeigt Fehlermeldung', async ({ page }) => { + // Given + await page.goto('/customers/0815'); + const deliverySection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Lieferadresse' }), + }); + await deliverySection.getByRole('button', { name: 'Bearbeiten' }).click(); + + // When + await deliverySection.getByRole('textbox', { name: 'PLZ' }).fill('12345'); + await deliverySection.getByRole('button', { name: 'Speichern' }).click(); + + // Then + await expect(deliverySection.getByText('Meinten Sie 26122 Oldenburg?')).toBeVisible(); + }); + + test('Lieferadresse: PLZ passt nicht zum Ort zeigt Fehlermeldung', async ({ page }) => { + // Given + await page.goto('/customers/0815'); + const deliverySection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Lieferadresse' }), + }); + await deliverySection.getByRole('button', { name: 'Bearbeiten' }).click(); + + // When + await deliverySection.getByRole('textbox', { name: 'PLZ' }).fill('10115'); + await deliverySection.getByRole('textbox', { name: 'Stadt' }).fill('Berlin'); + await deliverySection.getByRole('button', { name: 'Speichern' }).click(); + + // Then + await expect(deliverySection.getByText('Meinten Sie 10115 Berlin Mitte?')).toBeVisible(); + }); + + test('Lieferadresse: Unbekannte PLZ mit bekannter Stadt zeigt mögliche PLZ', async ({ page }) => { + // Given + await page.goto('/customers/0815'); + const deliverySection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Lieferadresse' }), + }); + await deliverySection.getByRole('button', { name: 'Bearbeiten' }).click(); + + // When + await deliverySection.getByRole('textbox', { name: 'PLZ' }).fill('12345'); + await deliverySection.getByRole('textbox', { name: 'Stadt' }).fill('Hannover'); + await deliverySection.getByRole('button', { name: 'Speichern' }).click(); + + // Then + await expect(deliverySection.locator('.error-message')).toContainText('Meinten Sie eine von'); + await expect(deliverySection.locator('.error-message')).toContainText('Hannover'); + }); + + test('Lieferadresse: PLZ mit mehreren möglichen Orten zeigt Fehlermeldung', async ({ page }) => { + // Given + await page.goto('/customers/0815'); + const deliverySection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Lieferadresse' }), + }); + await deliverySection.getByRole('button', { name: 'Bearbeiten' }).click(); + + // When + await deliverySection.getByRole('textbox', { name: 'PLZ' }).fill('19322'); + await deliverySection.getByRole('button', { name: 'Speichern' }).click(); + + // Then + await expect(deliverySection.locator('.error-message')).toContainText('Meinten Sie eine von'); + await expect(deliverySection.locator('.error-message')).toContainText('19322 Wittenberge'); + await expect(deliverySection.locator('.error-message')).toContainText('19322 Rühstädt'); + }); + + test('Rechnungsadresse ändern', async ({ page }) => { + // Given + await page.goto('/customers/0815'); + + // When + const billingSection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Rechnungsadresse' }), + }); + await expect(billingSection.getByText('Empfänger: Max Mustermann')).toBeVisible(); + await expect(billingSection.getByText('Straße: Poststr. 1')).toBeVisible(); + await expect(billingSection.getByText('Ort: 26122 Oldenburg')).toBeVisible(); + await billingSection.getByRole('button', { name: 'Bearbeiten' }).click(); + await billingSection.getByRole('textbox', { name: 'Empfänger *' }).fill('James Bond'); + await billingSection.getByRole('textbox', { name: 'Straße' }).fill('Albert Embankment'); + await billingSection.getByRole('textbox', { name: 'Hausnummer' }).fill('85'); + await billingSection.getByRole('textbox', { name: 'PLZ / Ort' }).fill('SE1 7TP London'); + await billingSection.getByRole('button', { name: 'Speichern' }).click(); + + // Then + await expect(billingSection.getByRole('button', { name: 'Speichern' })).not.toBeVisible(); + await expect(billingSection.getByRole('button', { name: 'Hinzufügen' })).not.toBeVisible(); + }); + + test('Lieferadresse ändern', async ({ page }) => { + // Given + await page.goto('/customers/0815'); + await expect(page.getByText('0815')).toBeVisible(); + await expect(page.getByText('Name: Max Mustermann')).toBeVisible(); + + // When + const deliverySection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Lieferadresse' }), + }); + await expect(deliverySection.getByText('Empfänger: Max Mustermann')).toBeVisible(); + await expect(deliverySection.getByText('Straße: Poststr. 1')).toBeVisible(); + await expect(deliverySection.getByText('Ort: 26122 Oldenburg')).toBeVisible(); + await deliverySection.getByRole('button', { name: 'Bearbeiten' }).click(); + await deliverySection.getByRole('textbox', { name: 'Empfänger *' }).fill('James Bond'); + await deliverySection.getByRole('textbox', { name: 'Straße' }).fill('Chausseestraße'); + await deliverySection.getByRole('textbox', { name: 'Hausnummer' }).fill('96 - 99a'); + await deliverySection.getByRole('textbox', { name: 'PLZ' }).fill('10115'); + await deliverySection.getByRole('textbox', { name: 'Stadt' }).fill('Berlin Mitte'); + await deliverySection.getByRole('button', { name: 'Speichern' }).click(); + + // Then + await expect(deliverySection.getByRole('button', { name: 'Speichern' })).not.toBeVisible(); + await expect(deliverySection.getByRole('button', { name: 'Hinzufügen' })).not.toBeVisible(); + }); +}); diff --git a/customer-client/test/customer-list.spec.ts b/customer-client/test/customer-list.spec.ts new file mode 100644 index 00000000..f43b1d58 --- /dev/null +++ b/customer-client/test/customer-list.spec.ts @@ -0,0 +1,69 @@ +/* + * Copyright 2026 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { test, expect } from '@playwright/test'; + +test('zeigt Kunden mit Kundennummer und Name in der Tabelle', async ({ page }) => { + await page.goto('/'); + + await expect(page.getByRole('cell', { name: '0815' })).toBeVisible(); + await expect(page.getByRole('cell', { name: 'Max Mustermann' })).toBeVisible(); + await expect(page.getByRole('cell', { name: '0816' })).toBeVisible(); + await expect(page.getByRole('cell', { name: 'Erika Mustermann' })).toBeVisible(); + await expect(page.getByRole('cell', { name: '007' })).toBeVisible(); + await expect(page.getByRole('cell', { name: 'James Bond' })).toBeVisible(); +}); + +test('navigiert zu Kundendetails beim Klick auf eine Zeile', async ({ page }) => { + await page.goto('/'); + await page.getByRole('cell', { name: 'Max Mustermann' }).click(); + + await expect(page).toHaveURL('/customers/0815'); +}); + +test('navigiert zu Neuer Kunde beim Klick auf den Button', async ({ page }) => { + await page.goto('/'); + await page.getByRole('button', { name: 'Neuer Kunde' }).click(); + + await expect(page).toHaveURL('/customers/new'); +}); + +test('sortiert nach Kundennummer aufsteigend und absteigend', async ({ page }) => { + await page.goto('/'); + + await page.getByRole('columnheader', { name: 'Kundennummer' }).click(); + let numbers = await page.locator('tbody tr td:first-child').allTextContents(); + expect(numbers.indexOf('007')).toBeLessThan(numbers.indexOf('0815')); + expect(numbers.indexOf('0815')).toBeLessThan(numbers.indexOf('0816')); + + await page.getByRole('columnheader', { name: 'Kundennummer' }).click(); + numbers = await page.locator('tbody tr td:first-child').allTextContents(); + expect(numbers.indexOf('0816')).toBeLessThan(numbers.indexOf('0815')); + expect(numbers.indexOf('0815')).toBeLessThan(numbers.indexOf('007')); +}); + +test('sortiert nach Name aufsteigend und absteigend', async ({ page }) => { + await page.goto('/'); + + await page.getByRole('columnheader', { name: 'Name' }).click(); + let names = await page.locator('tbody tr td:nth-child(2)').allTextContents(); + expect(names.indexOf('Erika Mustermann')).toBeLessThan(names.indexOf('James Bond')); + expect(names.indexOf('James Bond')).toBeLessThan(names.indexOf('Max Mustermann')); + + await page.getByRole('columnheader', { name: 'Name' }).click(); + names = await page.locator('tbody tr td:nth-child(2)').allTextContents(); + expect(names.indexOf('Max Mustermann')).toBeLessThan(names.indexOf('James Bond')); + expect(names.indexOf('James Bond')).toBeLessThan(names.indexOf('Erika Mustermann')); +}); diff --git a/customer-client/tsconfig.app.json b/customer-client/tsconfig.app.json new file mode 100644 index 00000000..a9b5a59c --- /dev/null +++ b/customer-client/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/customer-client/tsconfig.json b/customer-client/tsconfig.json new file mode 100644 index 00000000..d32ff682 --- /dev/null +++ b/customer-client/tsconfig.json @@ -0,0 +1,4 @@ +{ + "files": [], + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] +} diff --git a/customer-client/tsconfig.node.json b/customer-client/tsconfig.node.json new file mode 100644 index 00000000..8a67f62f --- /dev/null +++ b/customer-client/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/customer-client/vite.config.ts b/customer-client/vite.config.ts new file mode 100644 index 00000000..4a5def4c --- /dev/null +++ b/customer-client/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}); diff --git a/customer-service/pom.xml b/customer-service/pom.xml index 58852c5e..8d6fdd18 100644 --- a/customer-service/pom.xml +++ b/customer-service/pom.xml @@ -36,9 +36,9 @@ provided
- org.apache.geronimo.specs - geronimo-validation_2.0_spec - 1.0 + jakarta.validation + jakarta.validation-api + 3.1.1 org.eclipse.microprofile.config diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/domain/DeliveryAddressRepository.java b/customer-service/src/main/java/de/openknowledge/sample/address/domain/DeliveryAddressRepository.java index 0bf7050b..5d14345f 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/address/domain/DeliveryAddressRepository.java +++ b/customer-service/src/main/java/de/openknowledge/sample/address/domain/DeliveryAddressRepository.java @@ -19,16 +19,17 @@ import static jakarta.ws.rs.core.Response.Status.Family.SUCCESSFUL; import java.io.StringReader; +import java.util.Locale; import java.util.Optional; import java.util.logging.Logger; -import javax.validation.ValidationException; - import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.inject.Provider; import jakarta.json.Json; import jakarta.json.JsonObject; +import jakarta.validation.ValidationException; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.core.MediaType; @@ -47,6 +48,8 @@ public class DeliveryAddressRepository { @Inject @ConfigProperty(name = "delivery-service.url") String deliveryServiceUrl; + @Inject + Provider localeProvider; Client client; @@ -75,6 +78,7 @@ public void update(CustomerNumber customerNumber, Address deliveryAddress) { .path(DELIVERY_ADDRESSES_PATH) .path(customerNumber.toString()) .request(MediaType.APPLICATION_JSON) + .header("Accept-Language", localeProvider.get().getLanguage()) .post(entity(deliveryAddress, MediaType.APPLICATION_JSON_TYPE)); handleValidationError(response); } @@ -84,9 +88,26 @@ private void handleValidationError(Response response) { return; } if (!response.hasEntity()) { - throw new ValidationException("invalid address"); + throw new ValidationException("invalid address: empty"); + } + + try { + String responseBody = response.readEntity(String.class); + JsonObject problem = Json.createReader(new StringReader(responseBody)).readObject(); + + // Check if "detail" field exists, fallback to full JSON if not present + String detail = problem.containsKey("detail") + ? problem.getString("detail") + : responseBody; + LOG.info("Error saving address: " + detail); + + throw new ValidationException(detail); + } catch (ValidationException e) { + throw e; // Re-throw ValidationException + } catch (Exception e) { + // If JSON parsing fails, throw a generic validation exception + LOG.warning("Failed to parse problem JSON from delivery service: " + e.getMessage()); + throw new ValidationException("invalid address - delivery service validation failed"); } - JsonObject problem = Json.createReader(new StringReader(response.readEntity(String.class))).readObject(); - throw new ValidationException(problem.getString("detail")); } } diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java b/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java index 8bf945ec..e83384c1 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java +++ b/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/CorsFilter.java @@ -15,6 +15,7 @@ */ package de.openknowledge.sample.address.infrastructure; +import jakarta.enterprise.context.ApplicationScoped; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerResponseContext; import jakarta.ws.rs.container.ContainerResponseFilter; @@ -24,6 +25,7 @@ * Filter to allow cross origin calls. */ @Provider +@ApplicationScoped public class CorsFilter implements ContainerResponseFilter { @Override diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/LanguageProvider.java b/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/LanguageProvider.java new file mode 100644 index 00000000..a69ea8c1 --- /dev/null +++ b/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/LanguageProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright 2026 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.infrastructure; + +import static java.util.Locale.ENGLISH; +import static java.util.Optional.ofNullable; + +import java.util.Locale; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.servlet.http.HttpServletRequest; + +@ApplicationScoped +public class LanguageProvider { + + @Produces + public Locale getLanguage(HttpServletRequest request) { + return ofNullable(request.getLocale()).orElse(ENGLISH); + } +} diff --git a/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java b/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java index fc9d7090..d7963ab0 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java +++ b/customer-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java @@ -15,8 +15,8 @@ */ package de.openknowledge.sample.address.infrastructure; -import javax.validation.ValidationException; - +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.validation.ValidationException; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; @@ -24,6 +24,7 @@ import jakarta.ws.rs.ext.Provider; @Provider +@ApplicationScoped public class ValidationExceptionHandler implements ExceptionMapper { private static final String PROBLEM_JSON_TYPE = "application/problem+json"; diff --git a/delivery-service/pom.xml b/delivery-service/pom.xml index 58852c5e..8d6fdd18 100644 --- a/delivery-service/pom.xml +++ b/delivery-service/pom.xml @@ -36,9 +36,9 @@ provided - org.apache.geronimo.specs - geronimo-validation_2.0_spec - 1.0 + jakarta.validation + jakarta.validation-api + 3.1.1 org.eclipse.microprofile.config diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressValidationService.java b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressValidationService.java index 26aef503..5ce56ede 100644 --- a/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressValidationService.java +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/domain/AddressValidationService.java @@ -18,14 +18,15 @@ import static jakarta.ws.rs.client.Entity.entity; import java.io.StringReader; +import java.util.Locale; import java.util.logging.Logger; -import javax.validation.ValidationException; - import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.inject.Provider; import jakarta.json.Json; import jakarta.json.JsonObject; +import jakarta.validation.ValidationException; import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; @@ -42,6 +43,8 @@ public class AddressValidationService { @Inject @ConfigProperty(name = "address-validation-service.url") String addressValidationServiceUrl; + @Inject + Provider localeProvider; public void validate(Address address) { Response validationResult = ClientBuilder @@ -50,11 +53,20 @@ public void validate(Address address) { .target(addressValidationServiceUrl) .path(ADDRESS_VALIDATION_PATH) .request(MediaType.APPLICATION_JSON) + .header("Accept-Language", localeProvider.get().getLanguage()) .post(entity(address, MediaType.APPLICATION_JSON_TYPE)); if (validationResult.getStatus() != Response.Status.OK.getStatusCode()) { LOG.info("validation failed"); - JsonObject problem = Json.createReader(new StringReader(validationResult.readEntity(String.class))).readObject(); - throw new ValidationException(problem.getString("detail")); + String responseBody = validationResult.readEntity(String.class); + JsonObject problem = Json.createReader(new StringReader(responseBody)).readObject(); + + // Check if "detail" field exists, fallback to full JSON if not present + String detail = problem.containsKey("detail") + ? problem.getString("detail") + : responseBody; + LOG.info("Error saving address: " + detail); + + throw new ValidationException(detail); } } } diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/LanguageProvider.java b/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/LanguageProvider.java new file mode 100644 index 00000000..a69ea8c1 --- /dev/null +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/LanguageProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright 2026 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.address.infrastructure; + +import static java.util.Locale.ENGLISH; +import static java.util.Optional.ofNullable; + +import java.util.Locale; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.servlet.http.HttpServletRequest; + +@ApplicationScoped +public class LanguageProvider { + + @Produces + public Locale getLanguage(HttpServletRequest request) { + return ofNullable(request.getLocale()).orElse(ENGLISH); + } +} diff --git a/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java b/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java index a46e8357..03793c84 100644 --- a/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java +++ b/delivery-service/src/main/java/de/openknowledge/sample/address/infrastructure/ValidationExceptionHandler.java @@ -15,9 +15,8 @@ */ package de.openknowledge.sample.address.infrastructure; -import javax.validation.ValidationException; - import jakarta.enterprise.context.ApplicationScoped; +import jakarta.validation.ValidationException; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; diff --git a/docker-compose.yaml b/docker-compose.yaml index ddda0c8a..017597b4 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -26,3 +26,14 @@ services: container_name: address-validation_service ports: - "8184:8080" + + customer-client: + build: + context: customer-client/ + args: + - VITE_API_URL=http://localhost:8181 + container_name: customer-client + ports: + - "3000:80" + depends_on: + - customer-service From 878464be8762d13d1d84f3bc6587f4551efa6ce2 Mon Sep 17 00:00:00 2001 From: Arne Limburg Date: Wed, 25 Feb 2026 10:02:35 +0100 Subject: [PATCH 4/6] test: Add playwright mocking --- .github/workflows/build.yaml | 31 +++--- customer-client/test/create-customer.spec.ts | 18 ++++ customer-client/test/customer-detail.spec.ts | 107 +++++++++++++++++++ customer-client/test/customer-list.spec.ts | 26 +++++ 4 files changed, 166 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a77696fc..43a5faa6 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -31,9 +31,23 @@ jobs: - name: Build run: npm run build + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + working-directory: customer-client + + - name: Run Playwright tests + run: npm run test + working-directory: customer-client + + - name: Upload Playwright report + uses: actions/upload-artifact@v4 + if: failure() + with: + name: playwright-report + path: customer-client/playwright-report/ + build: runs-on: ubuntu-latest - needs: build-client steps: - name: Checkout uses: actions/checkout@v5 @@ -85,21 +99,6 @@ jobs: sleep 2 done - - name: Install Playwright browsers - run: npx playwright install --with-deps chromium - working-directory: customer-client - - - name: Run Playwright tests - run: npm run test - working-directory: customer-client - - - name: Upload Playwright report - uses: actions/upload-artifact@v4 - if: failure() - with: - name: playwright-report - path: customer-client/playwright-report/ - - name: Stop Docker Containers if: always() run: docker compose down diff --git a/customer-client/test/create-customer.spec.ts b/customer-client/test/create-customer.spec.ts index 8e1f7b2f..f8a312cc 100644 --- a/customer-client/test/create-customer.spec.ts +++ b/customer-client/test/create-customer.spec.ts @@ -15,6 +15,8 @@ */ import { test, expect } from '@playwright/test'; +const API_URL = 'http://localhost:8181'; + test.describe('Neuer Kunde', () => { test('zeigt Fehler bei leerem Namen', async ({ page }) => { // Given @@ -30,6 +32,22 @@ test.describe('Neuer Kunde', () => { test('erstellt Kunden erfolgreich und navigiert zur Liste', async ({ page }) => { // Given + const customerList: object[] = []; + + await page.route(`${API_URL}/customers/`, async (route) => { + if (route.request().method() === 'POST') { + const body = JSON.parse(route.request().postData() || '{}'); + customerList.push({ number: '0001', name: body.name }); + await route.fulfill({ status: 201 }); + } else { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(customerList), + }); + } + }); + await page.goto('/customers/new'); // When diff --git a/customer-client/test/customer-detail.spec.ts b/customer-client/test/customer-detail.spec.ts index dfba492b..305f86c8 100644 --- a/customer-client/test/customer-detail.spec.ts +++ b/customer-client/test/customer-detail.spec.ts @@ -15,9 +15,44 @@ */ import { test, expect } from '@playwright/test'; +const API_URL = 'http://localhost:8181'; + +const customer0815 = { + number: '0815', + name: 'Max Mustermann', + billingAddress: { + recipient: 'Max Mustermann', + street: { name: 'Poststr.', number: '1' }, + city: '26122 Oldenburg', + }, + deliveryAddress: { + recipient: 'Max Mustermann', + street: { name: 'Poststr.', number: '1' }, + city: '26122 Oldenburg', + }, +}; + test.describe('Adressen anlegen', () => { test('Rechnungsadresse anlegen', async ({ page }) => { // Given + let billingAddress: object | undefined; + + await page.route(`${API_URL}/customers/007`, async (route) => { + const customer = billingAddress + ? { number: '007', name: 'James Bond', billingAddress } + : { number: '007', name: 'James Bond' }; + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(customer), + }); + }); + + await page.route(`${API_URL}/customers/007/billing-address`, async (route) => { + billingAddress = JSON.parse(route.request().postData() || '{}'); + await route.fulfill({ status: 204 }); + }); + await page.goto('/customers/007'); await expect(page.getByText('007')).toBeVisible(); await expect(page.getByText('Name: James Bond')).toBeVisible(); @@ -41,6 +76,24 @@ test.describe('Adressen anlegen', () => { test('Lieferadresse anlegen', async ({ page }) => { // Given + let deliveryAddress: object | undefined; + + await page.route(`${API_URL}/customers/007`, async (route) => { + const customer = deliveryAddress + ? { number: '007', name: 'James Bond', deliveryAddress } + : { number: '007', name: 'James Bond' }; + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(customer), + }); + }); + + await page.route(`${API_URL}/customers/007/delivery-address`, async (route) => { + deliveryAddress = JSON.parse(route.request().postData() || '{}'); + await route.fulfill({ status: 204 }); + }); + await page.goto('/customers/007'); await expect(page.getByText('007')).toBeVisible(); await expect(page.getByText('Name: James Bond')).toBeVisible(); @@ -65,6 +118,16 @@ test.describe('Adressen anlegen', () => { }); test.describe('Adressen bearbeiten', () => { + test.beforeEach(async ({ page }) => { + await page.route(`${API_URL}/customers/0815`, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(customer0815), + }); + }); + }); + test('Rechnungsadresse: Straße ohne Hausnummer zeigt Fehlermeldung', async ({ page }) => { // Given await page.goto('/customers/0815'); @@ -135,6 +198,14 @@ test.describe('Adressen bearbeiten', () => { test('Lieferadresse: PLZ existiert nicht zeigt Fehlermeldung', async ({ page }) => { // Given + await page.route(`${API_URL}/customers/0815/delivery-address`, async (route) => { + await route.fulfill({ + status: 422, + contentType: 'application/problem+json', + body: JSON.stringify({ detail: 'Meinten Sie 26122 Oldenburg?' }), + }); + }); + await page.goto('/customers/0815'); const deliverySection = page.locator('.address-section', { has: page.locator('h3', { hasText: 'Lieferadresse' }), @@ -151,6 +222,14 @@ test.describe('Adressen bearbeiten', () => { test('Lieferadresse: PLZ passt nicht zum Ort zeigt Fehlermeldung', async ({ page }) => { // Given + await page.route(`${API_URL}/customers/0815/delivery-address`, async (route) => { + await route.fulfill({ + status: 422, + contentType: 'application/problem+json', + body: JSON.stringify({ detail: 'Meinten Sie 10115 Berlin Mitte?' }), + }); + }); + await page.goto('/customers/0815'); const deliverySection = page.locator('.address-section', { has: page.locator('h3', { hasText: 'Lieferadresse' }), @@ -168,6 +247,16 @@ test.describe('Adressen bearbeiten', () => { test('Lieferadresse: Unbekannte PLZ mit bekannter Stadt zeigt mögliche PLZ', async ({ page }) => { // Given + await page.route(`${API_URL}/customers/0815/delivery-address`, async (route) => { + await route.fulfill({ + status: 422, + contentType: 'application/problem+json', + body: JSON.stringify({ + detail: 'Meinten Sie eine von 30159 Hannover oder 30161 Hannover?', + }), + }); + }); + await page.goto('/customers/0815'); const deliverySection = page.locator('.address-section', { has: page.locator('h3', { hasText: 'Lieferadresse' }), @@ -186,6 +275,16 @@ test.describe('Adressen bearbeiten', () => { test('Lieferadresse: PLZ mit mehreren möglichen Orten zeigt Fehlermeldung', async ({ page }) => { // Given + await page.route(`${API_URL}/customers/0815/delivery-address`, async (route) => { + await route.fulfill({ + status: 422, + contentType: 'application/problem+json', + body: JSON.stringify({ + detail: 'Meinten Sie eine von 19322 Wittenberge oder 19322 Rühstädt?', + }), + }); + }); + await page.goto('/customers/0815'); const deliverySection = page.locator('.address-section', { has: page.locator('h3', { hasText: 'Lieferadresse' }), @@ -204,6 +303,10 @@ test.describe('Adressen bearbeiten', () => { test('Rechnungsadresse ändern', async ({ page }) => { // Given + await page.route(`${API_URL}/customers/0815/billing-address`, async (route) => { + await route.fulfill({ status: 204 }); + }); + await page.goto('/customers/0815'); // When @@ -227,6 +330,10 @@ test.describe('Adressen bearbeiten', () => { test('Lieferadresse ändern', async ({ page }) => { // Given + await page.route(`${API_URL}/customers/0815/delivery-address`, async (route) => { + await route.fulfill({ status: 204 }); + }); + await page.goto('/customers/0815'); await expect(page.getByText('0815')).toBeVisible(); await expect(page.getByText('Name: Max Mustermann')).toBeVisible(); diff --git a/customer-client/test/customer-list.spec.ts b/customer-client/test/customer-list.spec.ts index f43b1d58..895589a1 100644 --- a/customer-client/test/customer-list.spec.ts +++ b/customer-client/test/customer-list.spec.ts @@ -15,6 +15,24 @@ */ import { test, expect } from '@playwright/test'; +const API_URL = 'http://localhost:8181'; + +const customers = [ + { number: '0815', name: 'Max Mustermann' }, + { number: '0816', name: 'Erika Mustermann' }, + { number: '007', name: 'James Bond' }, +]; + +test.beforeEach(async ({ page }) => { + await page.route(`${API_URL}/customers/`, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(customers), + }); + }); +}); + test('zeigt Kunden mit Kundennummer und Name in der Tabelle', async ({ page }) => { await page.goto('/'); @@ -27,6 +45,14 @@ test('zeigt Kunden mit Kundennummer und Name in der Tabelle', async ({ page }) = }); test('navigiert zu Kundendetails beim Klick auf eine Zeile', async ({ page }) => { + await page.route(`${API_URL}/customers/0815`, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ number: '0815', name: 'Max Mustermann' }), + }); + }); + await page.goto('/'); await page.getByRole('cell', { name: 'Max Mustermann' }).click(); From d0cec184f0836bba08f1fd4f9a362b0c03988ab1 Mon Sep 17 00:00:00 2001 From: Arne Limburg Date: Mon, 23 Feb 2026 08:26:57 +0100 Subject: [PATCH 5/6] test: Add Pact mock server --- .github/workflows/build.yaml | 13 + README.md | 56 +- customer-client/package-lock.json | 1773 ++++++++++++++++- customer-client/package.json | 6 +- customer-client/src/api/hooks.ts | 14 +- customer-client/test/create-customer.spec.ts | 54 +- customer-client/test/customer-detail.spec.ts | 14 +- customer-client/test/customer-list.spec.ts | 2 +- customer-client/test/pact-proxy.ts | 59 + customer-service/pom.xml | 12 + .../customer/domain/CustomerNumber.java | 8 +- .../customer/domain/CustomerRepository.java | 6 +- .../sample/customer/CustomerServiceTest.java | 184 +- .../domain/TestCustomerRepository.java | 25 + 14 files changed, 1997 insertions(+), 229 deletions(-) create mode 100644 customer-client/test/pact-proxy.ts create mode 100644 customer-service/src/test/java/de/openknowledge/sample/customer/domain/TestCustomerRepository.java diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 43a5faa6..041bc18c 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -39,6 +39,12 @@ jobs: run: npm run test working-directory: customer-client + - name: Upload pact files + uses: actions/upload-artifact@v4 + with: + name: pact-files + path: pacts/ + - name: Upload Playwright report uses: actions/upload-artifact@v4 if: failure() @@ -47,6 +53,7 @@ jobs: path: customer-client/playwright-report/ build: + needs: build-client runs-on: ubuntu-latest steps: - name: Checkout @@ -59,6 +66,12 @@ jobs: distribution: 'temurin' cache: maven + - name: Download pact files + uses: actions/download-artifact@v4 + with: + name: pact-files + path: pacts/ + - name: Set up Node.js uses: actions/setup-node@v4 with: diff --git a/README.md b/README.md index 27853ff1..871fa6c4 100644 --- a/README.md +++ b/README.md @@ -15,25 +15,55 @@ Herzlich willkommen zum Workshop API Design. - [Pact](https://github.com/openknowledge/workshop-api-design/tree/pact-mock-server) - [Pact Pipeline](https://github.com/openknowledge/workshop-api-design/tree/pact) -### API Security +## Übung: Von Playwright-Mocking zu Pact Consumer Tests -- [JWT](https://github.com/openknowledge/workshop-api-design/tree/jwt) -- [OAuth2](https://github.com/openknowledge/workshop-api-design/tree/oauth2) -- [OAuth2 mit PKCE](https://github.com/openknowledge/workshop-api-design/tree/oauth2-pkce) +### Ausgangssituation -### API Governance +Im Branch `playwright-pact` befinden sich drei Playwright-Testdateien im Verzeichnis `customer-client/test/`: -- [Linting](https://github.com/openknowledge/workshop-api-design/tree/linting) +- `customer-list.spec.ts` – Tests für die Kundenliste +- `customer-detail.spec.ts` – Tests für die Kundendetailansicht inkl. Adressen +- `create-customer.spec.ts` – Tests für das Anlegen eines neuen Kunden (**bereits auf Pact umgebaut, dient als Referenz**) -### API Management +Die Tests in `customer-list.spec.ts` und `customer-detail.spec.ts` verwenden aktuell Playwright's eingebautes `page.route()`, um API-Aufrufe direkt im Browser abzufangen und mit statischen Antworten zu mocken. Dieser Ansatz dokumentiert den API-Vertrag nicht maschinenlesbar und kann nicht automatisch gegen den echten Service verifiziert werden. -- [Rate Limiting](https://github.com/openknowledge/workshop-api-design/tree/rate-limiting) -- [Backstage](https://github.com/openknowledge/workshop-api-design/tree/backstage) +Das Hilfsmuster `pact-proxy.ts` stellt zwei Funktionen bereit: +- `createProvider()` – erstellt einen Pact-Mock-Server mit Consumer- und Providernamen +- `setupApiProxy(page, mockServerUrl)` – leitet alle API-Aufrufe der Seite transparent an den Pact-Mock-Server weiter -### API Operation +### Aufgabe -- [Observability](https://github.com/openknowledge/workshop-api-design/tree/observability) +Baue die Playwright-Tests in `customer-list.spec.ts` und `customer-detail.spec.ts` so um, dass sie statt `page.route()` den Pact-Mock-Server nutzen. Orientiere dich dabei an `create-customer.spec.ts`. -### API Evolution +Für jeden Test, der API-Aufrufe mockt, gilt folgendes Muster: + +```typescript +await createProvider() + .addInteraction({ + uponReceiving: 'Beschreibung der Anfrage', + withRequest: { + method: 'GET', + path: '/customers/', + }, + willRespondWith: { + status: 200, + headers: { 'Content-Type': 'application/json' }, + body: [ /* ... */ ], + }, + }) + .executeTest(async (mockServer) => { + await setupApiProxy(page, mockServer.url); + // eigentlicher Test + }); +``` + +**Hinweise:** +- Tests, die keinerlei API-Aufrufe machen (z. B. reine UI-Validierungen), müssen nicht umgebaut werden. +- Wenn ein Test mehrere API-Endpunkte anspricht, können mehrere `.addInteraction()`-Aufrufe verkettet werden. +- Nach dem Umbau werden die Pact-Dateien automatisch im Verzeichnis `pacts/` erzeugt. Prüfe, ob die erzeugten Pacts den erwarteten Vertrag korrekt abbilden. +- `beforeEach`-Blöcke mit `page.route()` müssen in die jeweiligen `executeTest`-Callbacks verschoben werden, weil `setupApiProxy` erst innerhalb des Callbacks aufgerufen werden kann. + +### Ziel + +Nach dem Umbau laufen alle Tests weiterhin erfolgreich, und im Verzeichnis `pacts/` liegt eine aktualisierte JSON-Datei mit den Consumer-Driven Contracts, die später gegen den echten Customer Service verifiziert werden können. -- [Versioning](https://github.com/openknowledge/workshop-api-design/tree/versioning) diff --git a/customer-client/package-lock.json b/customer-client/package-lock.json index 63fd3ed0..9a0f04a8 100644 --- a/customer-client/package-lock.json +++ b/customer-client/package-lock.json @@ -19,7 +19,9 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@pact-foundation/pact": "^16.2.0", "@playwright/test": "^1.49.0", + "@types/express": "^5.0.6", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", @@ -1019,6 +1021,175 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@pact-foundation/pact": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact/-/pact-16.2.0.tgz", + "integrity": "sha512-PFedoP49sR9EKEvhREsXiDTb+rS3yv016DIlufey6pXC6ssQKhgq78ngZDRGl7mc6PqLJdOBCZUDPUrR890k2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pact-foundation/pact-core": "^18.1.0", + "axios": "^1.12.2", + "body-parser": "^2.2.0", + "chalk": "4.1.2", + "express": "^5.1.0", + "graphql": "^16.11.0", + "graphql-tag": "^2.12.6", + "http-proxy": "^1.18.1", + "https-proxy-agent": "^7.0.6", + "js-base64": "^3.7.8", + "lodash": "^4.17.21", + "ramda": "^0.32.0", + "randexp": "^0.5.3", + "router": "^2.2.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@pact-foundation/pact-core": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core/-/pact-core-18.1.0.tgz", + "integrity": "sha512-QWdntTsTT32r3SOTDaKjB9QyEbbgfsshsY2X/OwBJaNq28jKYEw50t2lYrITN+SdhkgfkEZJ9Y0XNfxtOUDVnA==", + "cpu": [ + "x64", + "ia32", + "arm64" + ], + "dev": true, + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32" + ], + "dependencies": { + "check-types": "11.2.3", + "detect-libc": "^2.0.3", + "node-gyp-build": "^4.6.0", + "pino": "^10.0.0", + "pino-pretty": "^13.1.1", + "underscore": "1.13.7" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "@pact-foundation/pact-core-darwin-arm64": "18.1.0", + "@pact-foundation/pact-core-darwin-x64": "18.1.0", + "@pact-foundation/pact-core-linux-arm64-glibc": "18.1.0", + "@pact-foundation/pact-core-linux-arm64-musl": "18.1.0", + "@pact-foundation/pact-core-linux-x64-glibc": "18.1.0", + "@pact-foundation/pact-core-linux-x64-musl": "18.1.0", + "@pact-foundation/pact-core-windows-x64": "18.1.0" + } + }, + "node_modules/@pact-foundation/pact-core-darwin-arm64": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-darwin-arm64/-/pact-core-darwin-arm64-18.1.0.tgz", + "integrity": "sha512-j1GSx7zN011xOveWeBP8RJ77bryFSLHIhe1ZcepfhMiB6VoUjGq1BNQ9rbhO4FSF2HBUCddMAw2/Xa+scHQJvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pact-foundation/pact-core-darwin-x64": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-darwin-x64/-/pact-core-darwin-x64-18.1.0.tgz", + "integrity": "sha512-KV08M7WJm/uuXv2qES4s8oJw8uTgFjQXmHLq4tKGT8HkaYCuSoQAhWYafxBJ/sMHeZ1LoGfv4nJkUr8bNakCYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pact-foundation/pact-core-linux-arm64-glibc": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-arm64-glibc/-/pact-core-linux-arm64-glibc-18.1.0.tgz", + "integrity": "sha512-BeJqmtBR6IdOg2IU0Y+N1NsH1pzr5H3PeOJ+vTSgNiTnV/wYkKk6GZDT1dzwUVi+OjfarGbK86022EoHk/WpkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pact-foundation/pact-core-linux-arm64-musl": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-arm64-musl/-/pact-core-linux-arm64-musl-18.1.0.tgz", + "integrity": "sha512-twakwextRNwkAKntYnSBBAs3yugORGDZwgihVm9p+eYqded/agFTu2v4/E7xksLEotGoFlewnAvLSCTvyNf0uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pact-foundation/pact-core-linux-x64-glibc": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-x64-glibc/-/pact-core-linux-x64-glibc-18.1.0.tgz", + "integrity": "sha512-rhR5iZS77Ie0ocJmtoTub82lyMQmjjn15UPhD5Tv1i2kYkbLCVPSYZpTrKak/OCDm5/AM0Lb7YqIZlOipMm/mA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pact-foundation/pact-core-linux-x64-musl": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-x64-musl/-/pact-core-linux-x64-musl-18.1.0.tgz", + "integrity": "sha512-u0N5uU3hwupGCkSmIiehR4yw5Foln+qqkUWXjtKdnttsj8Dz065K8osNGy18jmPRWr5XfRZurnnmw0+tFyaZPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pact-foundation/pact-core-windows-x64": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-windows-x64/-/pact-core-windows-x64-18.1.0.tgz", + "integrity": "sha512-ST7XNlI78c2MCvWvqv7on9M+DmNGlrihiD3Uk5jwZIAWEXeXEVinpfnM/Z5qFik4m3UCDe/Zu/1aNNiY1RN3dQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "dev": true, + "license": "MIT" + }, "node_modules/@playwright/test": { "version": "1.58.2", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", @@ -1626,6 +1797,27 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1633,6 +1825,38 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1646,11 +1870,24 @@ "integrity": "sha512-+0/4J266CBGPUq/ELg7QUHhN25WYjE0wYTPSQJn1xeu8DOlIOPxXxrNGiLmfAWl7HMMgWFWXpt9IDjMWrF5Iow==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.10", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", @@ -1672,6 +1909,27 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.54.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", @@ -1963,6 +2221,20 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1987,6 +2259,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2027,6 +2309,35 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2044,6 +2355,31 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2090,6 +2426,47 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2138,6 +2515,13 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-types": { + "version": "11.2.3", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", + "integrity": "sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==", + "dev": true, + "license": "MIT" + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2158,6 +2542,26 @@ "dev": true, "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2165,6 +2569,30 @@ "dev": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2185,6 +2613,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2207,6 +2645,16 @@ "dev": true, "license": "MIT" }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2232,26 +2680,157 @@ "dev": true, "license": "MIT" }, - "node_modules/electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, - "license": "ISC" + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } }, - "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, "engines": { - "node": ">=18" - }, + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/drange": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/drange/-/drange-1.1.1.tgz", + "integrity": "sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", @@ -2291,6 +2870,13 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2505,6 +3091,84 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fast-copy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz", + "integrity": "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2526,6 +3190,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2557,6 +3228,28 @@ "node": ">=16.0.0" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2595,6 +3288,87 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2610,6 +3384,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2620,6 +3404,45 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2646,6 +3469,46 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphql": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", + "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/graphql-tag": { + "version": "2.12.6", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", + "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2656,6 +3519,55 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true, + "license": "MIT" + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -2673,6 +3585,73 @@ "hermes-estree": "0.25.1" } }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2710,6 +3689,23 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2733,6 +3729,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2740,6 +3743,23 @@ "dev": true, "license": "ISC" }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-base64": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", + "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2847,6 +3867,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2864,6 +3891,66 @@ "yallist": "^3.0.2" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2877,6 +3964,16 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2910,6 +4007,28 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -2917,6 +4036,52 @@ "dev": true, "license": "MIT" }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2980,6 +4145,16 @@ "node": ">=6" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3000,6 +4175,17 @@ "node": ">=8" } }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3007,20 +4193,98 @@ "dev": true, "license": "ISC" }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^4.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", "dev": true, "license": "MIT", - "peer": true, "engines": { - "node": ">=12" + "node": ">=14.16" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "dev": true, + "license": "MIT" + }, "node_modules/playwright": { "version": "1.58.2", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", @@ -3123,6 +4387,55 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3133,6 +4446,80 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ramda": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.32.0.tgz", + "integrity": "sha512-GQWAHhxhxWBWA8oIBr1XahFVjQ9Fic6MK9ikijfd4TZHfE2+urfk+irVlR5VOn48uwMgM+loRRBJd6Yjsbc0zQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ramda" + } + }, + "node_modules/randexp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.5.3.tgz", + "integrity": "sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "drange": "^1.0.2", + "ret": "^0.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -3204,6 +4591,23 @@ "react-dom": ">=18" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3214,6 +4618,16 @@ "node": ">=4" } }, + "node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/rollup": { "version": "4.57.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", @@ -3259,12 +4673,63 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -3275,12 +4740,66 @@ "semver": "bin/semver.js" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/set-cookie-parser": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3304,6 +4823,92 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3314,6 +4919,49 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3340,6 +4988,19 @@ "node": ">=8" } }, + "node_modules/thread-stream": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz", + "integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3357,6 +5018,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -3370,6 +5041,13 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3383,6 +5061,21 @@ "node": ">= 0.8.0" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -3422,6 +5115,13 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "dev": true, + "license": "MIT" + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -3429,6 +5129,16 @@ "dev": true, "license": "MIT" }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -3479,6 +5189,16 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", @@ -3581,6 +5301,13 @@ "node": ">=0.10.0" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/customer-client/package.json b/customer-client/package.json index 7930bdb5..79cd1a67 100644 --- a/customer-client/package.json +++ b/customer-client/package.json @@ -25,6 +25,9 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@pact-foundation/pact": "^16.2.0", + "@playwright/test": "^1.49.0", + "@types/express": "^5.0.6", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", @@ -37,7 +40,6 @@ "prettier": "^3.8.1", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", - "vite": "^7.2.4", - "@playwright/test": "^1.49.0" + "vite": "^7.2.4" } } diff --git a/customer-client/src/api/hooks.ts b/customer-client/src/api/hooks.ts index 2d5ed812..4232e667 100644 --- a/customer-client/src/api/hooks.ts +++ b/customer-client/src/api/hooks.ts @@ -15,7 +15,7 @@ */ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { customerApi } from './client'; -import type { Address, CreateCustomerInput } from '../types/customer'; +import type { Address, CreateCustomerInput, Customer } from '../types/customer'; export const useCustomers = () => { return useQuery({ @@ -50,8 +50,10 @@ export const useUpdateBillingAddress = () => { mutationFn: ({ customerNumber, address }: { customerNumber: string; address: Address }) => customerApi.updateBillingAddress(customerNumber, address), onSuccess: (_, variables) => { - queryClient.invalidateQueries({ queryKey: ['customer', variables.customerNumber] }); - queryClient.invalidateQueries({ queryKey: ['customers'] }); + queryClient.setQueryData( + ['customer', variables.customerNumber], + (old: Customer | undefined) => (old ? { ...old, billingAddress: variables.address } : old), + ); }, }); }; @@ -63,8 +65,10 @@ export const useUpdateDeliveryAddress = () => { mutationFn: ({ customerNumber, address }: { customerNumber: string; address: Address }) => customerApi.updateDeliveryAddress(customerNumber, address), onSuccess: (_, variables) => { - queryClient.invalidateQueries({ queryKey: ['customer', variables.customerNumber] }); - queryClient.invalidateQueries({ queryKey: ['customers'] }); + queryClient.setQueryData( + ['customer', variables.customerNumber], + (old: Customer | undefined) => (old ? { ...old, deliveryAddress: variables.address } : old), + ); }, }); }; diff --git a/customer-client/test/create-customer.spec.ts b/customer-client/test/create-customer.spec.ts index f8a312cc..6c50c8f5 100644 --- a/customer-client/test/create-customer.spec.ts +++ b/customer-client/test/create-customer.spec.ts @@ -14,8 +14,7 @@ * limitations under the License. */ import { test, expect } from '@playwright/test'; - -const API_URL = 'http://localhost:8181'; +import { createProvider, setupApiProxy } from './pact-proxy'; test.describe('Neuer Kunde', () => { test('zeigt Fehler bei leerem Namen', async ({ page }) => { @@ -31,29 +30,38 @@ test.describe('Neuer Kunde', () => { }); test('erstellt Kunden erfolgreich und navigiert zur Liste', async ({ page }) => { - // Given - const customerList: object[] = []; - - await page.route(`${API_URL}/customers/`, async (route) => { - if (route.request().method() === 'POST') { - const body = JSON.parse(route.request().postData() || '{}'); - customerList.push({ number: '0001', name: body.name }); - await route.fulfill({ status: 201 }); - } else { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify(customerList), - }); - } - }); + const provider = createProvider(); - await page.goto('/customers/new'); + provider + .addInteraction() + .uponReceiving('a request to create a customer') + .withRequest('POST', '/customers/', (builder) => { + builder + .headers({ 'Content-Type': 'application/json' }) + .jsonBody({ name: 'Max Mustermann' }); + }) + .willRespondWith(201); - // When - await page.getByLabel('Name *').fill('Test Kunde'); - await page.getByRole('button', { name: 'Kunde erstellen' }).click(); + await provider + .addInteraction() + .uponReceiving('a request to get all customers') + .withRequest('GET', '/customers/') + .willRespondWith(200, (builder) => { + builder.headers({ 'Content-Type': 'application/json' }).jsonBody([ + { number: '007', name: 'James Bond' }, + { number: '0815', name: 'Max Mustermann' }, + { number: '0816', name: 'Erika Mustermann' }, + ]); + }) + .executeTest(async (mockServer) => { + await setupApiProxy(page, mockServer.url); + await page.goto('/customers/new'); + + // When + await page.getByLabel('Name *').fill('Max Mustermann'); + await page.getByRole('button', { name: 'Kunde erstellen' }).click(); - await expect(page.getByRole('cell', { name: 'Test Kunde' })).toBeVisible(); + await expect(page.getByRole('cell', { name: 'Max Mustermann' })).toBeVisible(); + }); }); }); diff --git a/customer-client/test/customer-detail.spec.ts b/customer-client/test/customer-detail.spec.ts index 305f86c8..7b56fdc0 100644 --- a/customer-client/test/customer-detail.spec.ts +++ b/customer-client/test/customer-detail.spec.ts @@ -35,12 +35,8 @@ const customer0815 = { test.describe('Adressen anlegen', () => { test('Rechnungsadresse anlegen', async ({ page }) => { // Given - let billingAddress: object | undefined; - await page.route(`${API_URL}/customers/007`, async (route) => { - const customer = billingAddress - ? { number: '007', name: 'James Bond', billingAddress } - : { number: '007', name: 'James Bond' }; + const customer = { number: '007', name: 'James Bond' }; await route.fulfill({ status: 200, contentType: 'application/json', @@ -49,7 +45,6 @@ test.describe('Adressen anlegen', () => { }); await page.route(`${API_URL}/customers/007/billing-address`, async (route) => { - billingAddress = JSON.parse(route.request().postData() || '{}'); await route.fulfill({ status: 204 }); }); @@ -76,12 +71,8 @@ test.describe('Adressen anlegen', () => { test('Lieferadresse anlegen', async ({ page }) => { // Given - let deliveryAddress: object | undefined; - await page.route(`${API_URL}/customers/007`, async (route) => { - const customer = deliveryAddress - ? { number: '007', name: 'James Bond', deliveryAddress } - : { number: '007', name: 'James Bond' }; + const customer = { number: '007', name: 'James Bond' }; await route.fulfill({ status: 200, contentType: 'application/json', @@ -90,7 +81,6 @@ test.describe('Adressen anlegen', () => { }); await page.route(`${API_URL}/customers/007/delivery-address`, async (route) => { - deliveryAddress = JSON.parse(route.request().postData() || '{}'); await route.fulfill({ status: 204 }); }); diff --git a/customer-client/test/customer-list.spec.ts b/customer-client/test/customer-list.spec.ts index 895589a1..f19b9615 100644 --- a/customer-client/test/customer-list.spec.ts +++ b/customer-client/test/customer-list.spec.ts @@ -18,9 +18,9 @@ import { test, expect } from '@playwright/test'; const API_URL = 'http://localhost:8181'; const customers = [ + { number: '007', name: 'James Bond' }, { number: '0815', name: 'Max Mustermann' }, { number: '0816', name: 'Erika Mustermann' }, - { number: '007', name: 'James Bond' }, ]; test.beforeEach(async ({ page }) => { diff --git a/customer-client/test/pact-proxy.ts b/customer-client/test/pact-proxy.ts new file mode 100644 index 00000000..568cac9b --- /dev/null +++ b/customer-client/test/pact-proxy.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2026 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { Page } from '@playwright/test'; +import { PactV4 } from '@pact-foundation/pact'; +import path from 'path'; + +export const API_URL = process.env.VITE_API_URL || 'http://localhost:8181'; + +export function createProvider(): PactV4 { + return new PactV4({ + consumer: 'customer-client', + provider: 'customer-service', + dir: path.resolve(process.cwd(), '../pacts'), + logLevel: 'warn', + }); +} + +export async function setupApiProxy(page: Page, mockServerUrl: string): Promise { + const baseUrl = mockServerUrl.replace(/\/$/, ''); + await page.route(`${API_URL}/**`, async (route) => { + const proxiedUrl = route.request().url().replace(API_URL, baseUrl); + const body = route.request().postDataBuffer(); + + // Problematic headers that must not be forwarded to the mock server + const { host, origin, referer, ...headers } = route.request().headers(); + void host; + void origin; + void referer; + + try { + const nativeResponse = await fetch(proxiedUrl, { + method: route.request().method(), + headers, + body: body || undefined, + }); + const responseBody = await nativeResponse.arrayBuffer(); + await route.fulfill({ + status: nativeResponse.status, + headers: Object.fromEntries(nativeResponse.headers.entries()), + body: Buffer.from(responseBody), + }); + } catch { + // Request was aborted during navigation, ignore + } + }); +} diff --git a/customer-service/pom.xml b/customer-service/pom.xml index 8d6fdd18..26d74de8 100644 --- a/customer-service/pom.xml +++ b/customer-service/pom.xml @@ -102,6 +102,18 @@ 2.28.2 test + + au.com.dius.pact.consumer + junit5 + 4.3.5 + test + + + au.com.dius.pact.provider + junit5 + 4.3.5 + test + org.apache.meecrowave meecrowave-junit diff --git a/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerNumber.java b/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerNumber.java index cac44656..b0d76006 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerNumber.java +++ b/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerNumber.java @@ -23,7 +23,7 @@ import de.openknowledge.sample.customer.domain.CustomerNumber.Adapter; @JsonbTypeAdapter(Adapter.class) -public class CustomerNumber { +public class CustomerNumber implements Comparable { private String number; protected CustomerNumber() { @@ -59,6 +59,11 @@ public boolean equals(Object object) { return toString().equals(customerNumber.toString()); } + @Override + public int compareTo(CustomerNumber n) { + return number.compareTo(n.number); + } + public static class Adapter implements JsonbAdapter { @Override @@ -72,5 +77,4 @@ public String adaptToJson(CustomerNumber number) throws Exception { } } - } diff --git a/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerRepository.java b/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerRepository.java index df7ed683..86827d15 100644 --- a/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerRepository.java +++ b/customer-service/src/main/java/de/openknowledge/sample/customer/domain/CustomerRepository.java @@ -53,7 +53,11 @@ public void initialize() { } public List findAll() { - return customers.values().stream().map(Customer::clearAddresses).collect(toList()); + return customers.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(Map.Entry::getValue) + .map(Customer::clearAddresses) + .collect(toList()); } public void persist(Customer customer) { diff --git a/customer-service/src/test/java/de/openknowledge/sample/customer/CustomerServiceTest.java b/customer-service/src/test/java/de/openknowledge/sample/customer/CustomerServiceTest.java index 2168b93a..3394e0ed 100644 --- a/customer-service/src/test/java/de/openknowledge/sample/customer/CustomerServiceTest.java +++ b/customer-service/src/test/java/de/openknowledge/sample/customer/CustomerServiceTest.java @@ -15,38 +15,39 @@ */ package de.openknowledge.sample.customer; -import static de.openknowledge.sample.customer.JsonObjectComparision.sameAs; -import static de.openknowledge.sample.customer.JsonObjectComparision.thatIsSameAs; -import static jakarta.ws.rs.client.Entity.entity; -import static org.assertj.core.api.Assertions.assertThat; +import static java.util.Optional.ofNullable; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; -import java.io.StringReader; -import java.net.URI; import java.util.Optional; import jakarta.inject.Inject; -import jakarta.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonObject; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; +import jakarta.validation.ValidationException; import org.apache.meecrowave.Meecrowave; -import org.apache.meecrowave.junit5.MeecrowaveConfig; +import org.apache.meecrowave.junit5.MonoMeecrowaveConfig; import org.apache.meecrowave.testing.ConfigurationInject; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; + +import au.com.dius.pact.provider.junit5.HttpTestTarget; +import au.com.dius.pact.provider.junit5.PactVerificationContext; +import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider; +import au.com.dius.pact.provider.junitsupport.Provider; +import au.com.dius.pact.provider.junitsupport.loader.PactFolder; import de.openknowledge.sample.address.domain.Address; import de.openknowledge.sample.address.domain.BillingAddressRepository; import de.openknowledge.sample.address.domain.DeliveryAddressRepository; import de.openknowledge.sample.customer.domain.CustomerNumber; import rocks.limburg.cdimock.MockitoBeans; +@Provider("customer-service") +@PactFolder("../pacts") @MockitoBeans(types = {BillingAddressRepository.class, DeliveryAddressRepository.class}) -@MeecrowaveConfig +@MonoMeecrowaveConfig public class CustomerServiceTest { @ConfigurationInject @@ -58,8 +59,6 @@ public class CustomerServiceTest { @Inject private DeliveryAddressRepository deliveryAddressRepository; - private URI uri; - @BeforeEach public void setUp() { when(deliveryAddressRepository.find(new CustomerNumber("0815"))) @@ -72,138 +71,29 @@ public void setUp() { when(billingAddressRepository.find(new CustomerNumber("007"))) .thenReturn(Optional.of(Address.of("Sherlock Holmes").atStreet("221B Baker Street").inCity("London NW1 6XE").build())); - uri = URI.create("http://localhost:" + config.getHttpPort()); - } - - @Test - public void getCustomers() { - JsonArray result = Json.createReader(new StringReader(ClientBuilder - .newClient() - .target(uri) - .path("customers") - .request(MediaType.APPLICATION_JSON) - .get() - .readEntity(String.class))) - .readArray(); - assertThat(result).haveAtLeastOne(thatIsSameAs(getClass().getResourceAsStream("max.json"))); - assertThat(result).haveAtLeastOne(thatIsSameAs(getClass().getResourceAsStream("erika.json"))); - assertThat(result).haveAtLeastOne(thatIsSameAs(getClass().getResourceAsStream("james.json"))); - } - - @Test - public void getCustomerWithAddresses() { - JsonObject result = Json.createReader(new StringReader(ClientBuilder - .newClient() - .target(uri) - .path("customers") - .path("0815") - .request(MediaType.APPLICATION_JSON) - .get() - .readEntity(String.class))) - .readObject(); - assertThat(result).is(sameAs(getClass().getResourceAsStream("max-with-addresses.json"))); - } - - @Test - public void createCustomer() { - Response response = ClientBuilder - .newClient() - .target(uri) - .path("customers") - .request(MediaType.APPLICATION_JSON) - .post(entity(getClass().getResourceAsStream("sherlock.json"), MediaType.APPLICATION_JSON)); - assertThat(response.getStatus()).isEqualTo(Response.Status.CREATED.getStatusCode()); - - JsonObject result = Json.createReader(new StringReader(ClientBuilder - .newClient() - .target(response.getLocation()) - .request(MediaType.APPLICATION_JSON) - .get() - .readEntity(String.class))) - .readObject(); - assertThat(result).is(sameAs(getClass().getResourceAsStream("sherlock.json"))); - - JsonArray customers = Json.createReader(new StringReader(ClientBuilder - .newClient() - .target(uri) - .path("customers") - .request(MediaType.APPLICATION_JSON) - .get() - .readEntity(String.class))) - .readArray(); - assertThat(customers).haveAtLeastOne(thatIsSameAs(getClass().getResourceAsStream("sherlock.json"))); + doThrow(new ValidationException("Meinten Sie 26122 Oldenburg?")) + .when(deliveryAddressRepository).update(any(CustomerNumber.class), + argThat(a -> "12345 Oldenburg".equals(a.getCity().toString()))); + doThrow(new ValidationException("Meinten Sie 10115 Berlin Mitte?")) + .when(deliveryAddressRepository).update(any(CustomerNumber.class), + argThat(a -> "10115 Berlin".equals(a.getCity().toString()))); + doThrow(new ValidationException("Meinten Sie eine von 30159 Hannover oder 30161 Hannover?")) + .when(deliveryAddressRepository).update(any(CustomerNumber.class), + argThat(a -> "12345 Hannover".equals(a.getCity().toString()))); + doThrow(new ValidationException("Meinten Sie eine von 19322 Wittenberge oder 19322 Rühstädt?")) + .when(deliveryAddressRepository).update(any(CustomerNumber.class), + argThat(a -> "19322 Oldenburg".equals(a.getCity().toString()))); } - @Test - public void getCustomerWithBillingAddress() { - JsonObject result = Json.createReader(new StringReader(ClientBuilder - .newClient() - .target(uri) - .path("customers") - .path("007") - .request(MediaType.APPLICATION_JSON) - .get() - .readEntity(String.class))) - .readObject(); - assertThat(result).is(sameAs(getClass().getResourceAsStream("james-with-addresses.json"))); - } - - @Test - public void getCustomerWithDeliveryAddress() { - JsonObject result = Json.createReader(new StringReader(ClientBuilder - .newClient() - .target(uri) - .path("customers") - .path("0816") - .request(MediaType.APPLICATION_JSON) - .get() - .readEntity(String.class))) - .readObject(); - assertThat(result).is(sameAs(getClass().getResourceAsStream("erika-with-addresses.json"))); - } - - @Test - public void setBillingAddressOfExistingCustomer() { - Response response = ClientBuilder - .newClient() - .target(uri) - .path("customers/0816/billing-address") - .request(MediaType.APPLICATION_JSON) - .put(entity(getClass().getResourceAsStream("sherlock-address.json"), MediaType.APPLICATION_JSON)); - assertThat(response.getStatus()).isEqualTo(Response.Status.NO_CONTENT.getStatusCode()); - } - - @Test - public void setBillingAddressOfNonExistingCustomerFails() { - Response response = ClientBuilder - .newClient() - .target(uri) - .path("customers/0817/billing-address") - .request(MediaType.APPLICATION_JSON) - .put(entity(getClass().getResourceAsStream("sherlock-address.json"), MediaType.APPLICATION_JSON)); - assertThat(response.getStatus()).isEqualTo(Response.Status.NOT_FOUND.getStatusCode()); - } - - - @Test - public void setDeliveryAddressOfExistingCustomer() { - Response response = ClientBuilder - .newClient() - .target(uri) - .path("customers/0816/delivery-address") - .request(MediaType.APPLICATION_JSON) - .put(entity(getClass().getResourceAsStream("sherlock-address.json"), MediaType.APPLICATION_JSON)); - assertThat(response.getStatus()).isEqualTo(Response.Status.NO_CONTENT.getStatusCode()); + @BeforeEach + public void setUp(PactVerificationContext verificationContext) { + ofNullable(verificationContext) + .ifPresent(context -> context.setTarget(new HttpTestTarget("localhost", config.getHttpPort(), "/"))); } - @Test - public void setDeliveryAddressOfNonExistingCustomerFails() { - Response response = ClientBuilder - .newClient() - .target(uri) - .path("customers/0817/delivery-address") - .request(MediaType.APPLICATION_JSON) - .put(entity(getClass().getResourceAsStream("sherlock-address.json"), MediaType.APPLICATION_JSON)); - assertThat(response.getStatus()).isEqualTo(Response.Status.NOT_FOUND.getStatusCode()); + @TestTemplate + @ExtendWith(PactVerificationInvocationContextProvider.class) + void pactVerificationTestTemplate(PactVerificationContext context) { + ofNullable(context).ifPresent(PactVerificationContext::verifyInteraction); } } diff --git a/customer-service/src/test/java/de/openknowledge/sample/customer/domain/TestCustomerRepository.java b/customer-service/src/test/java/de/openknowledge/sample/customer/domain/TestCustomerRepository.java new file mode 100644 index 00000000..0df16a6e --- /dev/null +++ b/customer-service/src/test/java/de/openknowledge/sample/customer/domain/TestCustomerRepository.java @@ -0,0 +1,25 @@ +/* + * Copyright 2019 open knowledge GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.openknowledge.sample.customer.domain; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.inject.Specializes; + +@Specializes +@RequestScoped +public class TestCustomerRepository extends CustomerRepository { + +} From 3e4c31992261e817f7b506c3104d1816f2e615b3 Mon Sep 17 00:00:00 2001 From: Arne Limburg Date: Mon, 23 Feb 2026 08:26:57 +0100 Subject: [PATCH 6/6] test: Add Pact mocking --- customer-client/test/customer-detail.spec.ts | 630 +++++++++++-------- customer-client/test/customer-list.spec.ts | 127 ++-- 2 files changed, 438 insertions(+), 319 deletions(-) diff --git a/customer-client/test/customer-detail.spec.ts b/customer-client/test/customer-detail.spec.ts index 7b56fdc0..a34497a5 100644 --- a/customer-client/test/customer-detail.spec.ts +++ b/customer-client/test/customer-detail.spec.ts @@ -14,8 +14,8 @@ * limitations under the License. */ import { test, expect } from '@playwright/test'; - -const API_URL = 'http://localhost:8181'; +import { PactV4 } from '@pact-foundation/pact'; +import { createProvider, setupApiProxy } from './pact-proxy'; const customer0815 = { number: '0815', @@ -32,319 +32,419 @@ const customer0815 = { }, }; +function addCustomer0815Interaction(provider: PactV4) { + return provider + .addInteraction() + .uponReceiving('a request to get customer 0815') + .withRequest('GET', '/customers/0815') + .willRespondWith(200, (builder) => { + builder.headers({ 'Content-Type': 'application/json' }).jsonBody(customer0815); + }); +} + test.describe('Adressen anlegen', () => { test('Rechnungsadresse anlegen', async ({ page }) => { - // Given - await page.route(`${API_URL}/customers/007`, async (route) => { - const customer = { number: '007', name: 'James Bond' }; - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify(customer), + const provider = createProvider(); + provider + .addInteraction() + .uponReceiving('a request to get customer 007') + .withRequest('GET', '/customers/007') + .willRespondWith(200, (builder) => { + builder + .headers({ 'Content-Type': 'application/json' }) + .jsonBody({ number: '007', name: 'James Bond' }); + }); + await provider + .addInteraction() + .uponReceiving('a request to create billing address for customer 007') + .withRequest('PUT', '/customers/007/billing-address', (builder) => { + builder.headers({ 'Content-Type': 'application/json' }).jsonBody({ + recipient: 'James Bond', + street: { name: 'Albert Embankment', number: '85' }, + city: 'SE1 7TP London', + }); + }) + .willRespondWith(204) + .executeTest(async (mockServer) => { + await setupApiProxy(page, mockServer.url); + await page.goto('/customers/007'); + await expect(page.getByText('007')).toBeVisible(); + await expect(page.getByText('Name: James Bond')).toBeVisible(); + + // When + const billingSection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Rechnungsadresse' }), + }); + await expect(billingSection.getByText('Keine Adresse hinterlegt')).toBeVisible(); + await billingSection.getByRole('button', { name: 'Hinzufügen' }).click(); + await billingSection.getByRole('textbox', { name: 'Empfänger *' }).fill('James Bond'); + await billingSection.getByRole('textbox', { name: 'Straße' }).fill('Albert Embankment'); + await billingSection.getByRole('textbox', { name: 'Hausnummer' }).fill('85'); + await billingSection.getByRole('textbox', { name: 'PLZ / Ort' }).fill('SE1 7TP London'); + await billingSection.getByRole('button', { name: 'Speichern' }).click(); + + // Then + await expect(billingSection.getByRole('button', { name: 'Speichern' })).not.toBeVisible(); + await expect(billingSection.getByRole('button', { name: 'Hinzufügen' })).not.toBeVisible(); }); - }); - - await page.route(`${API_URL}/customers/007/billing-address`, async (route) => { - await route.fulfill({ status: 204 }); - }); - - await page.goto('/customers/007'); - await expect(page.getByText('007')).toBeVisible(); - await expect(page.getByText('Name: James Bond')).toBeVisible(); - - // When - const billingSection = page.locator('.address-section', { - has: page.locator('h3', { hasText: 'Rechnungsadresse' }), - }); - await expect(billingSection.getByText('Keine Adresse hinterlegt')).toBeVisible(); - await billingSection.getByRole('button', { name: 'Hinzufügen' }).click(); - await billingSection.getByRole('textbox', { name: 'Empfänger *' }).fill('James Bond'); - await billingSection.getByRole('textbox', { name: 'Straße' }).fill('Albert Embankment'); - await billingSection.getByRole('textbox', { name: 'Hausnummer' }).fill('85'); - await billingSection.getByRole('textbox', { name: 'PLZ / Ort' }).fill('SE1 7TP London'); - await billingSection.getByRole('button', { name: 'Speichern' }).click(); - - // Then - await expect(billingSection.getByRole('button', { name: 'Speichern' })).not.toBeVisible(); - await expect(billingSection.getByRole('button', { name: 'Hinzufügen' })).not.toBeVisible(); }); test('Lieferadresse anlegen', async ({ page }) => { - // Given - await page.route(`${API_URL}/customers/007`, async (route) => { - const customer = { number: '007', name: 'James Bond' }; - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify(customer), + const provider = createProvider(); + provider + .addInteraction() + .uponReceiving('a request to get customer 007') + .withRequest('GET', '/customers/007') + .willRespondWith(200, (builder) => { + builder + .headers({ 'Content-Type': 'application/json' }) + .jsonBody({ number: '007', name: 'James Bond' }); + }); + await provider + .addInteraction() + .uponReceiving('a request to create delivery address for customer 007') + .withRequest('PUT', '/customers/007/delivery-address', (builder) => { + builder.headers({ 'Content-Type': 'application/json' }).jsonBody({ + recipient: 'James Bond', + street: { name: 'Chausseestraße', number: '96 - 99a' }, + city: '10115 Berlin Mitte', + }); + }) + .willRespondWith(204) + .executeTest(async (mockServer) => { + await setupApiProxy(page, mockServer.url); + await page.goto('/customers/007'); + await expect(page.getByText('007')).toBeVisible(); + await expect(page.getByText('Name: James Bond')).toBeVisible(); + + // When + const deliverySection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Lieferadresse' }), + }); + await expect(deliverySection.getByText('Keine Adresse hinterlegt')).toBeVisible(); + await deliverySection.getByRole('button', { name: 'Hinzufügen' }).click(); + await deliverySection.getByRole('textbox', { name: 'Empfänger *' }).fill('James Bond'); + await deliverySection.getByRole('textbox', { name: 'Straße' }).fill('Chausseestraße'); + await deliverySection.getByRole('textbox', { name: 'Hausnummer' }).fill('96 - 99a'); + await deliverySection.getByRole('textbox', { name: 'PLZ' }).fill('10115'); + await deliverySection.getByRole('textbox', { name: 'Stadt' }).fill('Berlin Mitte'); + await deliverySection.getByRole('button', { name: 'Speichern' }).click(); + + // Then + await expect(deliverySection.getByRole('button', { name: 'Speichern' })).not.toBeVisible(); + await expect(deliverySection.getByRole('button', { name: 'Hinzufügen' })).not.toBeVisible(); }); - }); - - await page.route(`${API_URL}/customers/007/delivery-address`, async (route) => { - await route.fulfill({ status: 204 }); - }); - - await page.goto('/customers/007'); - await expect(page.getByText('007')).toBeVisible(); - await expect(page.getByText('Name: James Bond')).toBeVisible(); - - // When - const deliverySection = page.locator('.address-section', { - has: page.locator('h3', { hasText: 'Lieferadresse' }), - }); - await expect(deliverySection.getByText('Keine Adresse hinterlegt')).toBeVisible(); - await deliverySection.getByRole('button', { name: 'Hinzufügen' }).click(); - await deliverySection.getByRole('textbox', { name: 'Empfänger *' }).fill('James Bond'); - await deliverySection.getByRole('textbox', { name: 'Straße' }).fill('Chausseestraße'); - await deliverySection.getByRole('textbox', { name: 'Hausnummer' }).fill('96 - 99a'); - await deliverySection.getByRole('textbox', { name: 'PLZ' }).fill('10115'); - await deliverySection.getByRole('textbox', { name: 'Stadt' }).fill('Berlin Mitte'); - await deliverySection.getByRole('button', { name: 'Speichern' }).click(); - - // Then - await expect(deliverySection.getByRole('button', { name: 'Speichern' })).not.toBeVisible(); - await expect(deliverySection.getByRole('button', { name: 'Hinzufügen' })).not.toBeVisible(); }); }); test.describe('Adressen bearbeiten', () => { - test.beforeEach(async ({ page }) => { - await page.route(`${API_URL}/customers/0815`, async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify(customer0815), - }); - }); - }); - test('Rechnungsadresse: Straße ohne Hausnummer zeigt Fehlermeldung', async ({ page }) => { - // Given - await page.goto('/customers/0815'); - const billingSection = page.locator('.address-section', { - has: page.locator('h3', { hasText: 'Rechnungsadresse' }), - }); - await billingSection.getByRole('button', { name: 'Bearbeiten' }).click(); + await addCustomer0815Interaction(createProvider()).executeTest(async (mockServer) => { + await setupApiProxy(page, mockServer.url); + await page.goto('/customers/0815'); + const billingSection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Rechnungsadresse' }), + }); + await billingSection.getByRole('button', { name: 'Bearbeiten' }).click(); - // When - await billingSection.getByRole('textbox', { name: 'Hausnummer' }).fill(''); + // When + await billingSection.getByRole('textbox', { name: 'Hausnummer' }).fill(''); - // Then - await expect( - billingSection.getByText('Hausnummer ist erforderlich wenn Straße angegeben'), - ).toBeVisible(); + // Then + await expect( + billingSection.getByText('Hausnummer ist erforderlich wenn Straße angegeben'), + ).toBeVisible(); + }); }); test('Rechnungsadresse: Hausnummer ohne Straße zeigt Fehlermeldung', async ({ page }) => { - // Given - await page.goto('/customers/0815'); - const billingSection = page.locator('.address-section', { - has: page.locator('h3', { hasText: 'Rechnungsadresse' }), - }); - await billingSection.getByRole('button', { name: 'Bearbeiten' }).click(); + await addCustomer0815Interaction(createProvider()).executeTest(async (mockServer) => { + await setupApiProxy(page, mockServer.url); + await page.goto('/customers/0815'); + const billingSection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Rechnungsadresse' }), + }); + await billingSection.getByRole('button', { name: 'Bearbeiten' }).click(); - // When - await billingSection.getByRole('textbox', { name: 'Straße' }).fill(''); + // When + await billingSection.getByRole('textbox', { name: 'Straße' }).fill(''); - // Then - await expect( - billingSection.getByText('Straße ist erforderlich wenn Hausnummer angegeben'), - ).toBeVisible(); + // Then + await expect( + billingSection.getByText('Straße ist erforderlich wenn Hausnummer angegeben'), + ).toBeVisible(); + }); }); test('Lieferadresse: Straße ohne Hausnummer zeigt Fehlermeldung', async ({ page }) => { - // Given - await page.goto('/customers/0815'); - const deliverySection = page.locator('.address-section', { - has: page.locator('h3', { hasText: 'Lieferadresse' }), - }); - await deliverySection.getByRole('button', { name: 'Bearbeiten' }).click(); + await addCustomer0815Interaction(createProvider()).executeTest(async (mockServer) => { + await setupApiProxy(page, mockServer.url); + await page.goto('/customers/0815'); + const deliverySection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Lieferadresse' }), + }); + await deliverySection.getByRole('button', { name: 'Bearbeiten' }).click(); - // When - await deliverySection.getByRole('textbox', { name: 'Hausnummer' }).fill(''); + // When + await deliverySection.getByRole('textbox', { name: 'Hausnummer' }).fill(''); - // Then - await expect( - deliverySection.getByText('Hausnummer ist erforderlich wenn Straße angegeben'), - ).toBeVisible(); + // Then + await expect( + deliverySection.getByText('Hausnummer ist erforderlich wenn Straße angegeben'), + ).toBeVisible(); + }); }); test('Lieferadresse: Hausnummer ohne Straße zeigt Fehlermeldung', async ({ page }) => { - // Given - await page.goto('/customers/0815'); - const deliverySection = page.locator('.address-section', { - has: page.locator('h3', { hasText: 'Lieferadresse' }), - }); - await deliverySection.getByRole('button', { name: 'Bearbeiten' }).click(); + await addCustomer0815Interaction(createProvider()).executeTest(async (mockServer) => { + await setupApiProxy(page, mockServer.url); + await page.goto('/customers/0815'); + const deliverySection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Lieferadresse' }), + }); + await deliverySection.getByRole('button', { name: 'Bearbeiten' }).click(); - // When - await deliverySection.getByRole('textbox', { name: 'Straße' }).fill(''); + // When + await deliverySection.getByRole('textbox', { name: 'Straße' }).fill(''); - // Then - await expect( - deliverySection.getByText('Straße ist erforderlich wenn Hausnummer angegeben'), - ).toBeVisible(); + // Then + await expect( + deliverySection.getByText('Straße ist erforderlich wenn Hausnummer angegeben'), + ).toBeVisible(); + }); }); test('Lieferadresse: PLZ existiert nicht zeigt Fehlermeldung', async ({ page }) => { - // Given - await page.route(`${API_URL}/customers/0815/delivery-address`, async (route) => { - await route.fulfill({ - status: 422, - contentType: 'application/problem+json', - body: JSON.stringify({ detail: 'Meinten Sie 26122 Oldenburg?' }), + const provider = createProvider(); + addCustomer0815Interaction(provider); + await provider + .addInteraction() + .uponReceiving('a request to update delivery address for customer 0815 with invalid zip code') + .withRequest('PUT', '/customers/0815/delivery-address', (builder) => { + builder.headers({ 'Content-Type': 'application/json' }).jsonBody({ + recipient: 'Max Mustermann', + street: { name: 'Poststr.', number: '1' }, + city: '12345 Oldenburg', + }); + }) + .willRespondWith(400, (builder) => { + builder + .headers({ 'Content-Type': 'application/problem+json' }) + .jsonBody({ detail: 'Meinten Sie 26122 Oldenburg?' }); + }) + .executeTest(async (mockServer) => { + await setupApiProxy(page, mockServer.url); + await page.goto('/customers/0815'); + const deliverySection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Lieferadresse' }), + }); + await deliverySection.getByRole('button', { name: 'Bearbeiten' }).click(); + + // When + await deliverySection.getByRole('textbox', { name: 'PLZ' }).fill('12345'); + await deliverySection.getByRole('button', { name: 'Speichern' }).click(); + + // Then + await expect(deliverySection.getByText('Meinten Sie 26122 Oldenburg?')).toBeVisible(); }); - }); - - await page.goto('/customers/0815'); - const deliverySection = page.locator('.address-section', { - has: page.locator('h3', { hasText: 'Lieferadresse' }), - }); - await deliverySection.getByRole('button', { name: 'Bearbeiten' }).click(); - - // When - await deliverySection.getByRole('textbox', { name: 'PLZ' }).fill('12345'); - await deliverySection.getByRole('button', { name: 'Speichern' }).click(); - - // Then - await expect(deliverySection.getByText('Meinten Sie 26122 Oldenburg?')).toBeVisible(); }); test('Lieferadresse: PLZ passt nicht zum Ort zeigt Fehlermeldung', async ({ page }) => { - // Given - await page.route(`${API_URL}/customers/0815/delivery-address`, async (route) => { - await route.fulfill({ - status: 422, - contentType: 'application/problem+json', - body: JSON.stringify({ detail: 'Meinten Sie 10115 Berlin Mitte?' }), + const provider = createProvider(); + addCustomer0815Interaction(provider); + await provider + .addInteraction() + .uponReceiving( + 'a request to update delivery address for customer 0815 with mismatched zip and city', + ) + .withRequest('PUT', '/customers/0815/delivery-address', (builder) => { + builder.headers({ 'Content-Type': 'application/json' }).jsonBody({ + recipient: 'Max Mustermann', + street: { name: 'Poststr.', number: '1' }, + city: '10115 Berlin', + }); + }) + .willRespondWith(400, (builder) => { + builder + .headers({ 'Content-Type': 'application/problem+json' }) + .jsonBody({ detail: 'Meinten Sie 10115 Berlin Mitte?' }); + }) + .executeTest(async (mockServer) => { + await setupApiProxy(page, mockServer.url); + await page.goto('/customers/0815'); + const deliverySection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Lieferadresse' }), + }); + await deliverySection.getByRole('button', { name: 'Bearbeiten' }).click(); + + // When + await deliverySection.getByRole('textbox', { name: 'PLZ' }).fill('10115'); + await deliverySection.getByRole('textbox', { name: 'Stadt' }).fill('Berlin'); + await deliverySection.getByRole('button', { name: 'Speichern' }).click(); + + // Then + await expect(deliverySection.getByText('Meinten Sie 10115 Berlin Mitte?')).toBeVisible(); }); - }); - - await page.goto('/customers/0815'); - const deliverySection = page.locator('.address-section', { - has: page.locator('h3', { hasText: 'Lieferadresse' }), - }); - await deliverySection.getByRole('button', { name: 'Bearbeiten' }).click(); - - // When - await deliverySection.getByRole('textbox', { name: 'PLZ' }).fill('10115'); - await deliverySection.getByRole('textbox', { name: 'Stadt' }).fill('Berlin'); - await deliverySection.getByRole('button', { name: 'Speichern' }).click(); - - // Then - await expect(deliverySection.getByText('Meinten Sie 10115 Berlin Mitte?')).toBeVisible(); }); test('Lieferadresse: Unbekannte PLZ mit bekannter Stadt zeigt mögliche PLZ', async ({ page }) => { - // Given - await page.route(`${API_URL}/customers/0815/delivery-address`, async (route) => { - await route.fulfill({ - status: 422, - contentType: 'application/problem+json', - body: JSON.stringify({ - detail: 'Meinten Sie eine von 30159 Hannover oder 30161 Hannover?', - }), + const provider = createProvider(); + addCustomer0815Interaction(provider); + await provider + .addInteraction() + .uponReceiving( + 'a request to update delivery address for customer 0815 with unknown zip and known city', + ) + .withRequest('PUT', '/customers/0815/delivery-address', (builder) => { + builder.headers({ 'Content-Type': 'application/json' }).jsonBody({ + recipient: 'Max Mustermann', + street: { name: 'Poststr.', number: '1' }, + city: '12345 Hannover', + }); + }) + .willRespondWith(400, (builder) => { + builder + .headers({ 'Content-Type': 'application/problem+json' }) + .jsonBody({ detail: 'Meinten Sie eine von 30159 Hannover oder 30161 Hannover?' }); + }) + .executeTest(async (mockServer) => { + await setupApiProxy(page, mockServer.url); + await page.goto('/customers/0815'); + const deliverySection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Lieferadresse' }), + }); + await deliverySection.getByRole('button', { name: 'Bearbeiten' }).click(); + + // When + await deliverySection.getByRole('textbox', { name: 'PLZ' }).fill('12345'); + await deliverySection.getByRole('textbox', { name: 'Stadt' }).fill('Hannover'); + await deliverySection.getByRole('button', { name: 'Speichern' }).click(); + + // Then + await expect(deliverySection.locator('.error-message')).toContainText( + 'Meinten Sie eine von', + ); + await expect(deliverySection.locator('.error-message')).toContainText('Hannover'); }); - }); - - await page.goto('/customers/0815'); - const deliverySection = page.locator('.address-section', { - has: page.locator('h3', { hasText: 'Lieferadresse' }), - }); - await deliverySection.getByRole('button', { name: 'Bearbeiten' }).click(); - - // When - await deliverySection.getByRole('textbox', { name: 'PLZ' }).fill('12345'); - await deliverySection.getByRole('textbox', { name: 'Stadt' }).fill('Hannover'); - await deliverySection.getByRole('button', { name: 'Speichern' }).click(); - - // Then - await expect(deliverySection.locator('.error-message')).toContainText('Meinten Sie eine von'); - await expect(deliverySection.locator('.error-message')).toContainText('Hannover'); }); test('Lieferadresse: PLZ mit mehreren möglichen Orten zeigt Fehlermeldung', async ({ page }) => { - // Given - await page.route(`${API_URL}/customers/0815/delivery-address`, async (route) => { - await route.fulfill({ - status: 422, - contentType: 'application/problem+json', - body: JSON.stringify({ - detail: 'Meinten Sie eine von 19322 Wittenberge oder 19322 Rühstädt?', - }), + const provider = createProvider(); + addCustomer0815Interaction(provider); + await provider + .addInteraction() + .uponReceiving( + 'a request to update delivery address for customer 0815 with zip matching multiple cities', + ) + .withRequest('PUT', '/customers/0815/delivery-address', (builder) => { + builder.headers({ 'Content-Type': 'application/json' }).jsonBody({ + recipient: 'Max Mustermann', + street: { name: 'Poststr.', number: '1' }, + city: '19322 Oldenburg', + }); + }) + .willRespondWith(400, (builder) => { + builder + .headers({ 'Content-Type': 'application/problem+json' }) + .jsonBody({ detail: 'Meinten Sie eine von 19322 Wittenberge oder 19322 Rühstädt?' }); + }) + .executeTest(async (mockServer) => { + await setupApiProxy(page, mockServer.url); + await page.goto('/customers/0815'); + const deliverySection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Lieferadresse' }), + }); + await deliverySection.getByRole('button', { name: 'Bearbeiten' }).click(); + + // When + await deliverySection.getByRole('textbox', { name: 'PLZ' }).fill('19322'); + await deliverySection.getByRole('button', { name: 'Speichern' }).click(); + + // Then + await expect(deliverySection.locator('.error-message')).toContainText( + 'Meinten Sie eine von', + ); + await expect(deliverySection.locator('.error-message')).toContainText('19322 Wittenberge'); + await expect(deliverySection.locator('.error-message')).toContainText('19322 Rühstädt'); }); - }); - - await page.goto('/customers/0815'); - const deliverySection = page.locator('.address-section', { - has: page.locator('h3', { hasText: 'Lieferadresse' }), - }); - await deliverySection.getByRole('button', { name: 'Bearbeiten' }).click(); - - // When - await deliverySection.getByRole('textbox', { name: 'PLZ' }).fill('19322'); - await deliverySection.getByRole('button', { name: 'Speichern' }).click(); - - // Then - await expect(deliverySection.locator('.error-message')).toContainText('Meinten Sie eine von'); - await expect(deliverySection.locator('.error-message')).toContainText('19322 Wittenberge'); - await expect(deliverySection.locator('.error-message')).toContainText('19322 Rühstädt'); }); test('Rechnungsadresse ändern', async ({ page }) => { - // Given - await page.route(`${API_URL}/customers/0815/billing-address`, async (route) => { - await route.fulfill({ status: 204 }); - }); - - await page.goto('/customers/0815'); - - // When - const billingSection = page.locator('.address-section', { - has: page.locator('h3', { hasText: 'Rechnungsadresse' }), - }); - await expect(billingSection.getByText('Empfänger: Max Mustermann')).toBeVisible(); - await expect(billingSection.getByText('Straße: Poststr. 1')).toBeVisible(); - await expect(billingSection.getByText('Ort: 26122 Oldenburg')).toBeVisible(); - await billingSection.getByRole('button', { name: 'Bearbeiten' }).click(); - await billingSection.getByRole('textbox', { name: 'Empfänger *' }).fill('James Bond'); - await billingSection.getByRole('textbox', { name: 'Straße' }).fill('Albert Embankment'); - await billingSection.getByRole('textbox', { name: 'Hausnummer' }).fill('85'); - await billingSection.getByRole('textbox', { name: 'PLZ / Ort' }).fill('SE1 7TP London'); - await billingSection.getByRole('button', { name: 'Speichern' }).click(); - - // Then - await expect(billingSection.getByRole('button', { name: 'Speichern' })).not.toBeVisible(); - await expect(billingSection.getByRole('button', { name: 'Hinzufügen' })).not.toBeVisible(); + const provider = createProvider(); + addCustomer0815Interaction(provider); + await provider + .addInteraction() + .uponReceiving('a request to update billing address for customer 0815') + .withRequest('PUT', '/customers/0815/billing-address', (builder) => { + builder.headers({ 'Content-Type': 'application/json' }).jsonBody({ + recipient: 'James Bond', + street: { name: 'Albert Embankment', number: '85' }, + city: 'SE1 7TP London', + }); + }) + .willRespondWith(204) + .executeTest(async (mockServer) => { + await setupApiProxy(page, mockServer.url); + await page.goto('/customers/0815'); + + // When + const billingSection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Rechnungsadresse' }), + }); + await expect(billingSection.getByText('Empfänger: Max Mustermann')).toBeVisible(); + await expect(billingSection.getByText('Straße: Poststr. 1')).toBeVisible(); + await expect(billingSection.getByText('Ort: 26122 Oldenburg')).toBeVisible(); + await billingSection.getByRole('button', { name: 'Bearbeiten' }).click(); + await billingSection.getByRole('textbox', { name: 'Empfänger *' }).fill('James Bond'); + await billingSection.getByRole('textbox', { name: 'Straße' }).fill('Albert Embankment'); + await billingSection.getByRole('textbox', { name: 'Hausnummer' }).fill('85'); + await billingSection.getByRole('textbox', { name: 'PLZ / Ort' }).fill('SE1 7TP London'); + await billingSection.getByRole('button', { name: 'Speichern' }).click(); + + // Then + await expect(billingSection.getByRole('button', { name: 'Speichern' })).not.toBeVisible(); + await expect(billingSection.getByRole('button', { name: 'Hinzufügen' })).not.toBeVisible(); + }); }); test('Lieferadresse ändern', async ({ page }) => { - // Given - await page.route(`${API_URL}/customers/0815/delivery-address`, async (route) => { - await route.fulfill({ status: 204 }); - }); - - await page.goto('/customers/0815'); - await expect(page.getByText('0815')).toBeVisible(); - await expect(page.getByText('Name: Max Mustermann')).toBeVisible(); - - // When - const deliverySection = page.locator('.address-section', { - has: page.locator('h3', { hasText: 'Lieferadresse' }), - }); - await expect(deliverySection.getByText('Empfänger: Max Mustermann')).toBeVisible(); - await expect(deliverySection.getByText('Straße: Poststr. 1')).toBeVisible(); - await expect(deliverySection.getByText('Ort: 26122 Oldenburg')).toBeVisible(); - await deliverySection.getByRole('button', { name: 'Bearbeiten' }).click(); - await deliverySection.getByRole('textbox', { name: 'Empfänger *' }).fill('James Bond'); - await deliverySection.getByRole('textbox', { name: 'Straße' }).fill('Chausseestraße'); - await deliverySection.getByRole('textbox', { name: 'Hausnummer' }).fill('96 - 99a'); - await deliverySection.getByRole('textbox', { name: 'PLZ' }).fill('10115'); - await deliverySection.getByRole('textbox', { name: 'Stadt' }).fill('Berlin Mitte'); - await deliverySection.getByRole('button', { name: 'Speichern' }).click(); - - // Then - await expect(deliverySection.getByRole('button', { name: 'Speichern' })).not.toBeVisible(); - await expect(deliverySection.getByRole('button', { name: 'Hinzufügen' })).not.toBeVisible(); + const provider = createProvider(); + addCustomer0815Interaction(provider); + await provider + .addInteraction() + .uponReceiving('a request to update delivery address for customer 0815') + .withRequest('PUT', '/customers/0815/delivery-address', (builder) => { + builder.headers({ 'Content-Type': 'application/json' }).jsonBody({ + recipient: 'James Bond', + street: { name: 'Chausseestraße', number: '96 - 99a' }, + city: '10115 Berlin Mitte', + }); + }) + .willRespondWith(204) + .executeTest(async (mockServer) => { + await setupApiProxy(page, mockServer.url); + await page.goto('/customers/0815'); + await expect(page.getByText('0815')).toBeVisible(); + await expect(page.getByText('Name: Max Mustermann')).toBeVisible(); + + // When + const deliverySection = page.locator('.address-section', { + has: page.locator('h3', { hasText: 'Lieferadresse' }), + }); + await expect(deliverySection.getByText('Empfänger: Max Mustermann')).toBeVisible(); + await expect(deliverySection.getByText('Straße: Poststr. 1')).toBeVisible(); + await expect(deliverySection.getByText('Ort: 26122 Oldenburg')).toBeVisible(); + await deliverySection.getByRole('button', { name: 'Bearbeiten' }).click(); + await deliverySection.getByRole('textbox', { name: 'Empfänger *' }).fill('James Bond'); + await deliverySection.getByRole('textbox', { name: 'Straße' }).fill('Chausseestraße'); + await deliverySection.getByRole('textbox', { name: 'Hausnummer' }).fill('96 - 99a'); + await deliverySection.getByRole('textbox', { name: 'PLZ' }).fill('10115'); + await deliverySection.getByRole('textbox', { name: 'Stadt' }).fill('Berlin Mitte'); + await deliverySection.getByRole('button', { name: 'Speichern' }).click(); + + // Then + await expect(deliverySection.getByRole('button', { name: 'Speichern' })).not.toBeVisible(); + await expect(deliverySection.getByRole('button', { name: 'Hinzufügen' })).not.toBeVisible(); + }); }); }); diff --git a/customer-client/test/customer-list.spec.ts b/customer-client/test/customer-list.spec.ts index f19b9615..bee21959 100644 --- a/customer-client/test/customer-list.spec.ts +++ b/customer-client/test/customer-list.spec.ts @@ -14,8 +14,8 @@ * limitations under the License. */ import { test, expect } from '@playwright/test'; - -const API_URL = 'http://localhost:8181'; +import { PactV4 } from '@pact-foundation/pact'; +import { createProvider, setupApiProxy } from './pact-proxy'; const customers = [ { number: '007', name: 'James Bond' }, @@ -23,73 +23,92 @@ const customers = [ { number: '0816', name: 'Erika Mustermann' }, ]; -test.beforeEach(async ({ page }) => { - await page.route(`${API_URL}/customers/`, async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify(customers), +function addCustomersListInteraction(provider: PactV4) { + return provider + .addInteraction() + .uponReceiving('a request to get all customers') + .withRequest('GET', '/customers/') + .willRespondWith(200, (builder) => { + builder.headers({ 'Content-Type': 'application/json' }).jsonBody(customers); }); - }); -}); +} test('zeigt Kunden mit Kundennummer und Name in der Tabelle', async ({ page }) => { - await page.goto('/'); - - await expect(page.getByRole('cell', { name: '0815' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'Max Mustermann' })).toBeVisible(); - await expect(page.getByRole('cell', { name: '0816' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'Erika Mustermann' })).toBeVisible(); - await expect(page.getByRole('cell', { name: '007' })).toBeVisible(); - await expect(page.getByRole('cell', { name: 'James Bond' })).toBeVisible(); + await addCustomersListInteraction(createProvider()).executeTest(async (mockServer) => { + await setupApiProxy(page, mockServer.url); + await page.goto('/'); + + await expect(page.getByRole('cell', { name: '0815' })).toBeVisible(); + await expect(page.getByRole('cell', { name: 'Max Mustermann' })).toBeVisible(); + await expect(page.getByRole('cell', { name: '0816' })).toBeVisible(); + await expect(page.getByRole('cell', { name: 'Erika Mustermann' })).toBeVisible(); + await expect(page.getByRole('cell', { name: '007' })).toBeVisible(); + await expect(page.getByRole('cell', { name: 'James Bond' })).toBeVisible(); + }); }); test('navigiert zu Kundendetails beim Klick auf eine Zeile', async ({ page }) => { - await page.route(`${API_URL}/customers/0815`, async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ number: '0815', name: 'Max Mustermann' }), + const provider = createProvider(); + addCustomersListInteraction(provider); + await provider + .addInteraction() + .uponReceiving('a request to get customer 0815') + .withRequest('GET', '/customers/0815') + .willRespondWith(200, (builder) => { + builder + .headers({ 'Content-Type': 'application/json' }) + .jsonBody({ number: '0815', name: 'Max Mustermann' }); + }) + .executeTest(async (mockServer) => { + await setupApiProxy(page, mockServer.url); + await page.goto('/'); + await page.getByRole('cell', { name: 'Max Mustermann' }).click(); + + await expect(page).toHaveURL('/customers/0815'); + await page.waitForLoadState('networkidle'); }); - }); - - await page.goto('/'); - await page.getByRole('cell', { name: 'Max Mustermann' }).click(); - - await expect(page).toHaveURL('/customers/0815'); }); test('navigiert zu Neuer Kunde beim Klick auf den Button', async ({ page }) => { - await page.goto('/'); - await page.getByRole('button', { name: 'Neuer Kunde' }).click(); + await addCustomersListInteraction(createProvider()).executeTest(async (mockServer) => { + await setupApiProxy(page, mockServer.url); + await page.goto('/'); + await page.getByRole('button', { name: 'Neuer Kunde' }).click(); - await expect(page).toHaveURL('/customers/new'); + await expect(page).toHaveURL('/customers/new'); + }); }); test('sortiert nach Kundennummer aufsteigend und absteigend', async ({ page }) => { - await page.goto('/'); - - await page.getByRole('columnheader', { name: 'Kundennummer' }).click(); - let numbers = await page.locator('tbody tr td:first-child').allTextContents(); - expect(numbers.indexOf('007')).toBeLessThan(numbers.indexOf('0815')); - expect(numbers.indexOf('0815')).toBeLessThan(numbers.indexOf('0816')); - - await page.getByRole('columnheader', { name: 'Kundennummer' }).click(); - numbers = await page.locator('tbody tr td:first-child').allTextContents(); - expect(numbers.indexOf('0816')).toBeLessThan(numbers.indexOf('0815')); - expect(numbers.indexOf('0815')).toBeLessThan(numbers.indexOf('007')); + await addCustomersListInteraction(createProvider()).executeTest(async (mockServer) => { + await setupApiProxy(page, mockServer.url); + await page.goto('/'); + + await page.getByRole('columnheader', { name: 'Kundennummer' }).click(); + let numbers = await page.locator('tbody tr td:first-child').allTextContents(); + expect(numbers.indexOf('007')).toBeLessThan(numbers.indexOf('0815')); + expect(numbers.indexOf('0815')).toBeLessThan(numbers.indexOf('0816')); + + await page.getByRole('columnheader', { name: 'Kundennummer' }).click(); + numbers = await page.locator('tbody tr td:first-child').allTextContents(); + expect(numbers.indexOf('0816')).toBeLessThan(numbers.indexOf('0815')); + expect(numbers.indexOf('0815')).toBeLessThan(numbers.indexOf('007')); + }); }); test('sortiert nach Name aufsteigend und absteigend', async ({ page }) => { - await page.goto('/'); - - await page.getByRole('columnheader', { name: 'Name' }).click(); - let names = await page.locator('tbody tr td:nth-child(2)').allTextContents(); - expect(names.indexOf('Erika Mustermann')).toBeLessThan(names.indexOf('James Bond')); - expect(names.indexOf('James Bond')).toBeLessThan(names.indexOf('Max Mustermann')); - - await page.getByRole('columnheader', { name: 'Name' }).click(); - names = await page.locator('tbody tr td:nth-child(2)').allTextContents(); - expect(names.indexOf('Max Mustermann')).toBeLessThan(names.indexOf('James Bond')); - expect(names.indexOf('James Bond')).toBeLessThan(names.indexOf('Erika Mustermann')); + await addCustomersListInteraction(createProvider()).executeTest(async (mockServer) => { + await setupApiProxy(page, mockServer.url); + await page.goto('/'); + + await page.getByRole('columnheader', { name: 'Name' }).click(); + let names = await page.locator('tbody tr td:nth-child(2)').allTextContents(); + expect(names.indexOf('Erika Mustermann')).toBeLessThan(names.indexOf('James Bond')); + expect(names.indexOf('James Bond')).toBeLessThan(names.indexOf('Max Mustermann')); + + await page.getByRole('columnheader', { name: 'Name' }).click(); + names = await page.locator('tbody tr td:nth-child(2)').allTextContents(); + expect(names.indexOf('Max Mustermann')).toBeLessThan(names.indexOf('James Bond')); + expect(names.indexOf('James Bond')).toBeLessThan(names.indexOf('Erika Mustermann')); + }); });