Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions services/carddemo-refdata/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target/
142 changes: 142 additions & 0 deletions services/carddemo-refdata/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# CardDemo Reference Data Service (`carddemo-refdata`)

Spring Boot 3.x microservice that replaces the COBOL/DB2 reference-data subsystem of the CardDemo mainframe application with a modern REST API backed by PostgreSQL.

## Legacy COBOL → Java Mapping

| COBOL Artifact | Purpose | Java Equivalent |
|---|---|---|
| **CVTRA03Y.cpy** — `TRAN-TYPE-RECORD` (RECLN=60) | Transaction type master | `TransactionType` entity |
| **CVTRA04Y.cpy** — `TRAN-CAT-RECORD` (RECLN=60) | Transaction category (composite key: type + cat) | `TransactionCategory` entity |
| **CVTRA02Y.cpy** — `DIS-GROUP-RECORD` (RECLN=50) | Disclosure group with interest rate | `DisclosureGroup` entity |
| **COTRTLIC.cbl** (2 098 LOC) | CICS list/delete transaction types & categories | `TransactionTypeController`, `TransactionCategoryController` (GET / DELETE) |
| **COTRTUPC.cbl** (1 702 LOC) | CICS add/edit transaction types & categories | Same controllers (POST / PUT) |
| **COBTUPDT.cbl** | Batch maintenance of transaction types | Superseded by REST CRUD |

### Field-Level Mapping

#### TransactionType — `CVTRA03Y.cpy`

| COBOL Field | PIC | Java Field | Type |
|---|---|---|---|
| `TRAN-TYPE` | `X(02)` | `typeCode` | `String(2)` — Primary Key |
| `TRAN-TYPE-DESC` | `X(50)` | `description` | `String(50)` |
| `FILLER` | `X(08)` | — | not mapped |

#### TransactionCategory — `CVTRA04Y.cpy`

| COBOL Field | PIC | Java Field | Type |
|---|---|---|---|
| `TRAN-TYPE-CD` | `X(02)` | `typeCode` | `String(2)` — Composite PK part 1 |
| `TRAN-CAT-CD` | `9(04)` | `categoryCode` | `int` (0–9999) — Composite PK part 2 |
| `TRAN-CAT-TYPE-DESC` | `X(50)` | `description` | `String(50)` |
| `FILLER` | `X(04)` | — | not mapped |

#### DisclosureGroup — `CVTRA02Y.cpy`

| COBOL Field | PIC | Java Field | Type |
|---|---|---|---|
| `DIS-ACCT-GROUP-ID` | `X(10)` | `accountGroupId` | `String(10)` — Composite PK part 1 |
| `DIS-TRAN-TYPE-CD` | `X(02)` | `transactionTypeCode` | `String(2)` — Composite PK part 2 |
| `DIS-TRAN-CAT-CD` | `9(04)` | `transactionCategoryCode` | `int` (0–9999) — Composite PK part 3 |
| `DIS-INT-RATE` | `S9(04)V99` | `interestRate` | `BigDecimal(6,2)` |
| `FILLER` | `X(28)` | — | not mapped |

## REST API

| Method | Path | Description |
|---|---|---|
| `GET` | `/reference/types` | List all transaction types |
| `POST` | `/reference/types` | Create a transaction type |
| `GET` | `/reference/types/{code}` | Get a single type |
| `PUT` | `/reference/types/{code}` | Update a type |
| `DELETE` | `/reference/types/{code}` | Delete a type |
| `GET` | `/reference/categories` | List all categories |
| `POST` | `/reference/categories` | Create a category |
| `GET` | `/reference/categories/{typeCode}/{catCode}` | Get a single category |
| `PUT` | `/reference/categories/{typeCode}/{catCode}` | Update a category |
| `DELETE` | `/reference/categories/{typeCode}/{catCode}` | Delete a category |
| `GET` | `/reference/disclosure-groups` | List all disclosure groups |
| `POST` | `/reference/disclosure-groups` | Create a disclosure group |
| `GET` | `/reference/disclosure-groups/{groupId}/{typeCode}/{catCode}` | Get a single group |
| `PUT` | `/reference/disclosure-groups/{groupId}/{typeCode}/{catCode}` | Update a group |
| `DELETE` | `/reference/disclosure-groups/{groupId}/{typeCode}/{catCode}` | Delete a group |

## Validation Rules

- **Type code**: 1–2 characters (`@Size(min=1, max=2)`, `@NotBlank`)
- **Category code**: 0–9999 (`@Min(0)`, `@Max(9999)`)
- **Account group ID**: 1–10 characters
- **Interest rate**: `BigDecimal` with up to 4 integer digits and 2 fractional digits (`@Digits(integer=4, fraction=2)`)

## Running Locally

### Prerequisites
- Java 17+
- Maven 3.6+
- PostgreSQL 14+ (or use the H2 test profile)

### Build & Test

```bash
cd services/carddemo-refdata
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 # adjust as needed
mvn clean test # runs all 63 tests against H2
mvn spring-boot:run # starts on port 8082 (requires PostgreSQL)
```

### Configuration

| Property | Default | Description |
|---|---|---|
| `spring.datasource.url` | `jdbc:postgresql://localhost:5432/carddemo` | JDBC URL |
| `spring.datasource.username` | `carddemo` | DB user |
| `spring.datasource.password` | `carddemo` | DB password |
| `server.port` | `8082` | HTTP port |

## Project Structure

```
services/carddemo-refdata/
├── pom.xml
├── README.md
└── src/
├── main/java/com/carddemo/refdata/
│ ├── RefDataApplication.java
│ ├── controller/
│ │ ├── DisclosureGroupController.java
│ │ ├── GlobalExceptionHandler.java
│ │ ├── TransactionCategoryController.java
│ │ └── TransactionTypeController.java
│ ├── entity/
│ │ ├── DisclosureGroup.java
│ │ ├── DisclosureGroupId.java
│ │ ├── TransactionCategory.java
│ │ ├── TransactionCategoryId.java
│ │ └── TransactionType.java
│ ├── repository/
│ │ ├── DisclosureGroupRepository.java
│ │ ├── TransactionCategoryRepository.java
│ │ └── TransactionTypeRepository.java
│ └── service/
│ ├── DisclosureGroupService.java
│ ├── TransactionCategoryService.java
│ └── TransactionTypeService.java
├── main/resources/
│ └── application.yml
└── test/
├── java/com/carddemo/refdata/
│ ├── RefDataApplicationTest.java
│ ├── controller/
│ │ ├── DisclosureGroupControllerTest.java
│ │ ├── TransactionCategoryControllerTest.java
│ │ └── TransactionTypeControllerTest.java
│ ├── repository/
│ │ └── TransactionTypeRepositoryTest.java
│ └── service/
│ ├── DisclosureGroupServiceTest.java
│ ├── TransactionCategoryServiceTest.java
│ └── TransactionTypeServiceTest.java
└── resources/
└── application-test.yml
```
70 changes: 70 additions & 0 deletions services/carddemo-refdata/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.6</version>
<relativePath/>
</parent>

<groupId>com.carddemo</groupId>
<artifactId>carddemo-refdata</artifactId>
<version>0.1.0-SNAPSHOT</version>
<name>CardDemo Reference Data Service</name>
<description>Spring Boot service for CardDemo transaction type, category, and disclosure group reference data (Phase 1B)</description>

<properties>
<java.version>17</java.version>
</properties>

<dependencies>
<!-- Spring Boot starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<!-- PostgreSQL driver (runtime) -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>

<!-- H2 for tests -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>

<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.carddemo.refdata;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RefDataApplication {

public static void main(String[] args) {
SpringApplication.run(RefDataApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.carddemo.refdata.controller;

import java.util.List;

import com.carddemo.refdata.entity.DisclosureGroup;
import com.carddemo.refdata.service.DisclosureGroupService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/reference/disclosure-groups")
public class DisclosureGroupController {

private final DisclosureGroupService service;

public DisclosureGroupController(DisclosureGroupService service) {
this.service = service;
}

@GetMapping
public List<DisclosureGroup> list() {
return service.findAll();
}

@GetMapping("/{groupId}/{typeCode}/{catCode}")
public ResponseEntity<DisclosureGroup> get(@PathVariable String groupId,
@PathVariable String typeCode,
@PathVariable int catCode) {
DisclosureGroup entity = service.findByKey(groupId, typeCode, catCode);
if (entity == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(entity);
}

@PostMapping
public ResponseEntity<DisclosureGroup> create(@Valid @RequestBody DisclosureGroup entity) {
DisclosureGroup created = service.create(entity);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}

@PutMapping("/{groupId}/{typeCode}/{catCode}")
public ResponseEntity<DisclosureGroup> update(@PathVariable String groupId,
@PathVariable String typeCode,
@PathVariable int catCode,
@Valid @RequestBody DisclosureGroup entity) {
DisclosureGroup updated = service.update(groupId, typeCode, catCode, entity);
if (updated == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(updated);
}

@DeleteMapping("/{groupId}/{typeCode}/{catCode}")
public ResponseEntity<Void> delete(@PathVariable String groupId,
@PathVariable String typeCode,
@PathVariable int catCode) {
if (!service.delete(groupId, typeCode, catCode)) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.carddemo.refdata.controller;

import java.util.HashMap;
import java.util.Map;

import com.carddemo.refdata.service.DuplicateEntityException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
for (FieldError fe : ex.getBindingResult().getFieldErrors()) {
errors.put(fe.getField(), fe.getDefaultMessage());
}
return ResponseEntity.badRequest().body(errors);
}

@ExceptionHandler(DuplicateEntityException.class)
public ResponseEntity<Map<String, String>> handleDuplicate(DuplicateEntityException ex) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(Map.of("error", ex.getMessage() != null ? ex.getMessage() : "Conflict"));
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
}
Loading