Skip to content

feat(sap-demo-java): add Customer 360 SAP S/4HANA + Postgres sample#126

Merged
slayerjain merged 3 commits intomainfrom
feat/sap-demo-java
Apr 22, 2026
Merged

feat(sap-demo-java): add Customer 360 SAP S/4HANA + Postgres sample#126
slayerjain merged 3 commits intomainfrom
feat/sap-demo-java

Conversation

@slayerjain
Copy link
Copy Markdown
Member

Summary

Adds a new sample app: sap-demo-java/ — a Spring Boot Customer 360 API that exercises two Keploy-recordable egress surfaces simultaneously:

  • Outbound HTTPS to SAP S/4HANA Cloud Business Accelerator Hub sandbox (Business Partner + Sales Order OData v2)
  • Local PostgreSQL 16 persistence for customer tags, notes, and audit events

It is structured so a single end-to-end user journey drives a batch of HTTP + SAP OData + Postgres interactions, which is exactly the shape we want for recording/replaying real-world integration testcases.

What's in the sample

Path Purpose
src/ Spring Boot app (Java 17, Spring Boot 3, Hibernate, Flyway)
docker-compose.yml Single-host demo (app + postgres)
Dockerfile Multi-stage build
k8s/ + kind-config.yaml + deploy_kind.sh Local kind-based K8s deploy
demo_script.sh End-to-end curl walkthrough
run_flow.sh / simulate_tosca_flow.sh Scripted user journeys (Tosca-style)
pom.xml Spring Boot starter + Flyway + Hibernate
keploy.yml Recording config (no recorded data committed)

Secrets handling

All real credentials are gitignored and not committed:

  • .env — gitignored. Only .env.example (empty SAP_API_KEY) is committed.
  • k8s/secret.yaml — gitignored. Only k8s/secret.yaml.example (placeholder <PASTE_YOUR_SAP_API_KEY_HERE>) is committed.

Users supply their own free SAP Business Accelerator Hub sandbox API key (https://api.sap.com → any API → "Show API Key").

Why a new sample

The existing Java samples cover Spring Boot + a single data store (Postgres or Mongo). sap-demo-java is the first that:

  1. Combines Postgres persistence with outbound HTTPS to an external SAP tenant, so it stresses both postgres and http interceptors in one flow.
  2. Models a realistic enterprise API shape — OData pagination, problem+json error surfaces, correlation-id propagation — that shows up in SAP integration testing.
  3. Ships a ready-to-run kind deploy + Tosca-style flow script, so regression pipelines can drive it end-to-end without manual setup.

Test plan

  • cd sap-demo-java && cp .env.example .env and paste a sandbox API key
  • docker compose up --build → app on localhost:8080/actuator/health returns UP
  • bash demo_script.sh → walkthrough exercises SAP BP + local tag/note/audit CRUD
  • bash deploy_kind.sh → sample deploys to a local kind cluster (NodePort 30080)
  • Keploy record (keploy record -c "docker compose up --build") + replay against the same flow produces a green test report

Related

This sample is used as a regression fixture for the Postgres v3 parser in keploy/integrations, so changes to the parser are validated against a real enterprise-shape application.

🤖 Generated with Claude Code

A Spring Boot "Customer 360" API that exercises two integrations at once:
- Outbound HTTPS to SAP S/4HANA Cloud Business Accelerator Hub
  (sandbox API Business Partner + Sales Order OData endpoints)
- Local PostgreSQL persistence for customer tags, notes, and audit events

The sample covers the full keploy flow (record + replay) against both
egress surfaces simultaneously, and is structured to be driven from a
Tosca-style flow script. It ships three run shapes:

- docker-compose.yml: single-host demo (app + postgres)
- k8s/ + kind-config.yaml + deploy_kind.sh: local kind cluster deploy
- pom.xml mvn spring-boot:run: plain JVM

Secrets handling:
- .env is gitignored; only .env.example is committed (empty SAP_API_KEY)
- k8s/secret.yaml is gitignored; only secret.yaml.example is committed
  (placeholder <PASTE_YOUR_SAP_API_KEY_HERE>)
- Users populate both from their own SAP Business Accelerator Hub key

Included flow artefacts:
- demo_script.sh: end-to-end curl walkthrough
- run_flow.sh / simulate_tosca_flow.sh: scripted user journeys
- keploy.yml: recording config (empty globalNoise, no recorded data)
Copilot AI review requested due to automatic review settings April 22, 2026 06:06
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new sap-demo-java/ sample Spring Boot “Customer 360” service that aggregates SAP S/4HANA Business Partner data via outbound HTTPS while persisting local enrichment data (tags/notes/audit) in Postgres, intended for Keploy record/replay demos.

Changes:

  • Introduces a Spring Boot Customer 360 API with SAP OData client, fan-out aggregator, RFC7807 error model, and correlation-id propagation.
  • Adds Postgres schema + JPA repositories/entities for tags, notes, and audit events.
  • Adds local + kind/K8s deployment assets and demo/flow scripts (docker-compose, manifests, deploy helpers, Keploy config).

Reviewed changes

Copilot reviewed 57 out of 57 changed files in this pull request and generated 22 comments.

Show a summary per file
File Description
sap-demo-java/src/test/java/com/tricentisdemo/sap/customer360/Customer360ApplicationTests.java Context-load smoke test for Spring wiring
sap-demo-java/src/main/resources/logback-spring.xml Logback config with kubernetes JSON profile
sap-demo-java/src/main/resources/db/migration/V1__init_schema.sql Flyway schema for tags/notes/audit tables
sap-demo-java/src/main/resources/application.yml App config: datasource, SAP, caching, actuator, logging
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/web/TagController.java REST endpoints for local customer tags
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/web/NoteController.java REST endpoints for local customer notes
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/web/GlobalExceptionHandler.java RFC7807 problem responses + correlation headers
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/web/CustomerController.java Customer list/detail/360/count endpoints
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/web/AuditController.java REST endpoint for recent audit events
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/service/TagService.java Tag persistence/business logic
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/service/NoteService.java Note persistence/business logic
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/service/CustomerService.java Simple SAP-backed customer queries
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/service/Customer360AggregatorService.java Fan-out aggregator composing SAP + Postgres data
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/service/AuditService.java Synchronous audit insert/read operations
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/sap/SapBusinessPartnerClient.java SAP OData v2 gateway with caching + resilience
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/sap/SapApiException.java Domain exception for SAP failures
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/sap/CorrelationIdInterceptor.java Outbound correlation-id propagation to SAP
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/sap/CorrelationIdFilter.java Inbound correlation-id seeding + response echo
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/repository/CustomerTagRepository.java JPA repository for tags
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/repository/CustomerNoteRepository.java JPA repository for notes
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/repository/AuditEventRepository.java JPA repository for audit events
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/persistence/CustomerTag.java Tag entity mapping
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/persistence/CustomerNote.java Note entity mapping
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/persistence/AuditEvent.java Audit event entity mapping
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/model/TagRequest.java Tag request DTO + validation
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/model/ProblemResponse.java RFC7807 problem DTO
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/model/ODataEntityResponse.java SAP OData v2 single-entity envelope DTO
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/model/ODataCollectionResponse.java SAP OData v2 collection envelope DTO
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/model/NoteRequest.java Note request DTO + validation
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/model/CustomerSummary.java List projection DTO
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/model/Customer360View.java Aggregated 360 response DTO
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/model/BusinessPartnerRole.java SAP role DTO
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/model/BusinessPartnerAddress.java SAP address DTO
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/model/BusinessPartner.java SAP business partner DTO
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/config/SapClientConfig.java RestTemplate + auth + executor configuration
sap-demo-java/src/main/java/com/tricentisdemo/sap/customer360/Customer360Application.java Spring Boot entrypoint + OpenAPI metadata
sap-demo-java/simulate_tosca_flow.sh Narrated Tosca-style flow script
sap-demo-java/run_flow.sh Load-driving flow script (iterative calls)
sap-demo-java/pom.xml Maven build + dependencies for the sample
sap-demo-java/kind-config.yaml kind cluster config
sap-demo-java/keploy.yml Keploy recording/replay configuration
sap-demo-java/k8s/service.yaml NodePort service exposing the app
sap-demo-java/k8s/secret.yaml.example Example secret template for SAP credentials
sap-demo-java/k8s/postgres.yaml Postgres deployment/service/secret (demo)
sap-demo-java/k8s/namespace.yaml Namespace definition + Keploy marker label
sap-demo-java/k8s/ingress.yaml Ingress routing rules
sap-demo-java/k8s/deployment.yaml App deployment with probes + security context
sap-demo-java/k8s/configmap.yaml Non-secret runtime config
sap-demo-java/docker-compose.yml Local compose runner for app + Postgres
sap-demo-java/deploy_kind.sh kind cluster build/load/apply helper
sap-demo-java/demo_script.sh Demo record/replay harness
sap-demo-java/README.md Sample documentation and demo instructions
sap-demo-java/Dockerfile Runtime container image packaging
sap-demo-java/.gitignore Sample-local ignore rules (secrets/artifacts)
sap-demo-java/.env.example Environment template for SAP credentials
sap-demo-java/.dockerignore Docker build context filters
README.md Top-level README: adds link to the new SAP demo

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout((int) Duration.ofSeconds(connectTimeoutSeconds).toMillis());
factory.setConnectionRequestTimeout((int) Duration.ofSeconds(connectTimeoutSeconds).toMillis());
Comment on lines +28 to +40
* <p>Three interceptors are attached:
* <ol>
* <li><b>SapAuthInterceptor</b> — stamps the required {@code APIKey} header
* (sandbox) or {@code Authorization: Bearer} (production tenant).</li>
* <li><b>IdentityEncodingInterceptor</b> — forces {@code Accept-Encoding: identity}
* so bodies are not gzip-compressed in transit. In production this is
* optional; here it keeps Keploy-captured YAML mocks human-readable,
* which matters for demo-grade visibility and for diff-reviewing
* contract changes in pull requests.</li>
* <li><b>CorrelationIdInterceptor</b> — propagates the inbound request's
* correlation id into the outbound SAP call so traces chain across
* the hop.</li>
* </ol>
Comment on lines +76 to +79
boolean deleted = tags.remove(customerId, tag);
audit.record(customerId, "tags.delete", null);
return ResponseEntity.ok(Map.of("deleted", deleted, "tag", tag.toLowerCase()));
}
Comment on lines +90 to +97
@ExceptionHandler(Exception.class)
public ResponseEntity<ProblemResponse> handleUnexpected(Exception ex, HttpServletRequest req) {
log.error("Unhandled exception", ex);
ProblemResponse body = baseProblem(HttpStatus.INTERNAL_SERVER_ERROR,
"Internal Server Error", ex.getMessage(), req);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.headers(commonHeaders(null))
.body(body);
Comment on lines +44 to +56
log.warn("SAP error surfaced to caller: status={} upstream={} detail={}",
status.value(), ex.getUpstreamStatus().value(), ex.getMessage());

return ResponseEntity.status(status)
.headers(commonHeaders(ex.getUpstreamStatus().value()))
.body(body);
}

@ExceptionHandler(CallNotPermittedException.class)
public ResponseEntity<ProblemResponse> handleCircuitOpen(CallNotPermittedException ex,
HttpServletRequest req) {
log.warn("Circuit breaker open, rejecting call: {}", ex.getMessage());
ProblemResponse body = baseProblem(
Comment thread sap-demo-java/Dockerfile Outdated
Comment on lines +3 to +10
# Uses amazoncorretto:25 — Amazon Linux 2023-based, minimal, JDK-ready.
# The jar is built outside Docker (mvn package); this image just packages it
# and runs it. K8s securityContext enforces the non-root runtime UID (1001).
#
# Docker Hub pull rate limits are avoided by relying on the locally cached
# amazoncorretto:25 image.

FROM amazoncorretto:25
Comment on lines +91 to +93
public ResponseEntity<ProblemResponse> handleUnexpected(Exception ex, HttpServletRequest req) {
log.error("Unhandled exception", ex);
ProblemResponse body = baseProblem(HttpStatus.INTERNAL_SERVER_ERROR,
Comment on lines +118 to +121
} catch (TimeoutException e) {
log.warn("Aggregation timeout for bp={} after {}s — returning partial view",
customerId, aggregateTimeoutSeconds);
addresses = addressesF.getNow(List.of());
Comment thread sap-demo-java/README.md
Comment on lines +83 to +90
```bash
cd /home/shubham/tricentis/sap_testing/sap_demo_java

# 1. One-time: drop your SAP API key into .env
cp .env.example .env
$EDITOR .env # paste SAP_API_KEY

# 2. Stand everything up: kind cluster + build + load + apply
Comment thread sap-demo-java/keploy.yml Outdated
mockDownload:
registryIds: []

# Visit [https://keploy.io/docs/running-keploy/configuration-file/] to learn about using keploy through configration file.
slayerjain and others added 2 commits April 22, 2026 11:49
Applies 17 fixes from #126 review:

Alignment / correctness:
- Dockerfile: amazoncorretto:25 -> 21 (match pom java.version=21)
- docker-compose.yml, k8s/postgres.yaml: postgres:15-alpine -> 16-alpine
  (align with README/PR "Postgres 16")
- kind-config.yaml: add extraPortMappings for 30080 so the NodePort Service
  is reachable at http://localhost:30080 (previously 80/443 only)
- SapClientConfig: plumb readTimeoutSeconds through RestTemplateBuilder
  (HttpComponentsClientHttpRequestFactory.setReadTimeout was removed in
  Spring 6 / HttpClient5) + drop Javadoc reference to the never-implemented
  IdentityEncodingInterceptor

Bugs / robustness:
- TagController.remove: trim + lowercase before echoing "tag" in response,
  so the response matches the value used by the service
- GlobalExceptionHandler.handleUnexpected: stop leaking ex.getMessage() to
  clients; log full exception server-side with correlationId + path, return
  a generic body that invites the caller to quote the correlationId
- TagService.add: replace exists() + findAll().stream().filter().orElseThrow()
  with a single findByCustomerIdAndTag Optional lookup, eliminating the
  race window + the unsafe orElseThrow(); CustomerTagRepository gains the
  matching finder
- Customer360AggregatorService: split the combined
  `catch (InterruptedException | ExecutionException)` so
  Thread.currentThread().interrupt() fires only for InterruptedException
- deploy_kind.sh build_and_load: replace `[ src -nt target/...jar ]` (which
  only compares the src directory mtime) with a `find src pom.xml -newer`
  sweep so rebuilds actually trigger after any file change

Test / housekeeping:
- Customer360ApplicationTests: add @AutoConfigureTestDatabase(Replace.ANY)
  + H2 test dep so the context-load smoke test runs without a live
  Postgres; disable Flyway (migrations are Postgres-specific) and let
  Hibernate generate the schema from entities at test time. Verified:
  context loads in ~5.6s locally
- application.yml: drop logging.pattern.console — logback-spring.xml owns
  the pattern (and overrides Boot's key when present); avoids drift
- keploy.yml: typo "configration" -> "configuration"
- README.md Quick start: drop absolute /home/shubham/... path, use
  repo-relative `cd sap-demo-java`

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per product direction, the e2e/sample path MUST exercise real Postgres
+ Flyway so Keploy is forced to correctly capture the full wire-protocol
footprint — including the Flyway bootstrap/migration queries. Swapping
the datasource for H2 at test time would hide exactly the behaviour the
sample is meant to regress.

Restores the Customer360ApplicationTests to its pre-review shape and
drops the H2 test dependency. The test assumes a live Postgres on
localhost:5432 (docker-compose.yml brings it up); added a Javadoc note
so future readers don't re-H2 it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@slayerjain
Copy link
Copy Markdown
Member Author

Review round 2 — response to Copilot's 22 comments

Pushed: `f2407c7` (review fixes) + `679fc8b` (review round 2).

Applied (17)

# File What
1 `Dockerfile` amazoncorretto:25 → 21 (matches `pom.xml` java.version=21)
2 `docker-compose.yml` postgres:15-alpine → 16-alpine
3 `k8s/postgres.yaml` postgres:15-alpine → 16-alpine (+ comment)
4 `kind-config.yaml` added `extraPortMappings` for 30080 so NodePort is reachable
5 `SapClientConfig.java` `readTimeoutSeconds` now plumbed through `RestTemplateBuilder.setReadTimeout` — `HttpComponentsClientHttpRequestFactory.setReadTimeout(int)` was removed with the Spring 6 / HttpClient5 migration, so the factory-level call Copilot suggested doesn't compile
6 `SapClientConfig.java` dropped Javadoc reference to the never-implemented `IdentityEncodingInterceptor`
7 `TagController.java` `tag.toLowerCase()` → `tag.trim().toLowerCase()` in the response body
8 `TagService.java` + `CustomerTagRepository.java` replaced `exists(...) + findAll(...).stream().filter().orElseThrow()` with a single `findByCustomerIdAndTag` Optional lookup — eliminates the race window and the unsafe `orElseThrow`
9 `GlobalExceptionHandler.java` `handleUnexpected` no longer leaks `ex.getMessage()`; full exception stays server-side with correlationId + path; client gets a generic message pointing at the correlationId
10 `Customer360AggregatorService.java` split the combined `catch (InterruptedException | ExecutionException)` so `Thread.currentThread().interrupt()` fires only for `InterruptedException`
11 `deploy_kind.sh` replaced `[ src -nt target/...jar ]` (which only compares the src directory mtime) with a `find src pom.xml -newer` sweep
12 `application.yml` dropped `logging.pattern.console` — `logback-spring.xml` owns the pattern and overrides Boot's key, so duplicating it invited drift
13 `keploy.yml` typo "configration" → "configuration"
14 `README.md` Quick start: absolute `/home/shubham/...` path → repo-relative `cd sap-demo-java`
15 `GlobalExceptionHandler.java` error log now includes correlationId + request path so operators get an actionable line without the client message leaking
16 `Customer360ApplicationTests.java` docstring now explicitly says the test expects a live Postgres (see #17)
17 Also reverted an intermediate H2-swap in the smoke test per product direction: the sample exists to exercise Keploy against real Postgres + Flyway, so any test-time datasource override would hide the wire traffic we're here to regress.

Not applied — with rationale

Copilot's 4 WARN-level-logging complaints (`GlobalExceptionHandler:56`, `SapClientConfig:77`, `SapBusinessPartnerClient:223`, `Customer360AggregatorService:121`) cite "this repo's logging guideline" as prohibiting WARN. That guideline does not exist in this repo — there is no `.github/instructions/*`, no `CONTRIBUTING` rule, no `CODESTYLE` section that says so (verified: `find .github -type f` + `CONTRIBUTING.md`). The existing Java samples use WARN level freely.

WARN is the correct level for the four sites flagged:

  • `GlobalExceptionHandler:56` — circuit-breaker open. An operational signal, recoverable. Downgrading to INFO loses the alerting surface; upgrading to ERROR is alert-spam.
  • `SapClientConfig:77` — credentials not configured. The app still boots; SAP calls will 401. That's exactly what WARN is for.
  • `SapBusinessPartnerClient:223` — SAP transport failure. Converted into a SapApiException on the same line; WARN captures the transient while ERROR-ing each one would drown a real incident.
  • `Customer360AggregatorService:121` — partial-result aggregation after a timeout. Deliberately recoverable.

`pom.xml` Java 17 vs 21 — Copilot saw "Java 17" in my PR description. The canonical version is 21 (pom + Dockerfile agree; CI uses `eclipse-temurin-21`). Fixing the PR description.

`k8s/service.yaml:18` — rolled up into the kind-config `extraPortMappings` fix (#4); no separate change to the Service needed.

Re-requesting Copilot review.

@slayerjain slayerjain merged commit d8c9869 into main Apr 22, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants