delete(@PathVariable String code) {
+ if (!service.delete(code)) {
+ return ResponseEntity.notFound().build();
+ }
+ return ResponseEntity.noContent().build();
+ }
+}
diff --git a/services/carddemo-refdata/src/main/java/com/carddemo/refdata/entity/DisclosureGroup.java b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/entity/DisclosureGroup.java
new file mode 100644
index 00000000..de1b523e
--- /dev/null
+++ b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/entity/DisclosureGroup.java
@@ -0,0 +1,99 @@
+package com.carddemo.refdata.entity;
+
+import java.math.BigDecimal;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.IdClass;
+import jakarta.persistence.Table;
+import jakarta.validation.constraints.Digits;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+
+/**
+ * Maps to COBOL copybook CVTRA02Y.cpy — DIS-GROUP-RECORD (RECLN=50).
+ *
+ *
+ * 05 DIS-GROUP-KEY
+ * 10 DIS-ACCT-GROUP-ID PIC X(10) -> accountGroupId CHAR(10), PK part 1
+ * 10 DIS-TRAN-TYPE-CD PIC X(02) -> transactionTypeCode CHAR(2), PK part 2
+ * 10 DIS-TRAN-CAT-CD PIC 9(04) -> transactionCategoryCode INT, PK part 3
+ * 05 DIS-INT-RATE PIC S9(04)V99 -> interestRate DECIMAL(6,2)
+ * 05 FILLER PIC X(28) -- padding, not mapped
+ *
+ */
+@Entity
+@Table(name = "disclosure_group")
+@IdClass(DisclosureGroupId.class)
+public class DisclosureGroup {
+
+ @Id
+ @Column(name = "account_group_id", length = 10, nullable = false)
+ @NotBlank
+ @Size(min = 1, max = 10)
+ private String accountGroupId;
+
+ @Id
+ @Column(name = "transaction_type_code", length = 2, nullable = false)
+ @NotBlank
+ @Size(min = 1, max = 2)
+ private String transactionTypeCode;
+
+ @Id
+ @Column(name = "transaction_category_code", nullable = false)
+ @Min(0)
+ @Max(9999)
+ private int transactionCategoryCode;
+
+ @Column(name = "interest_rate", precision = 6, scale = 2)
+ @NotNull
+ @Digits(integer = 4, fraction = 2)
+ private BigDecimal interestRate;
+
+ public DisclosureGroup() {
+ }
+
+ public DisclosureGroup(String accountGroupId, String transactionTypeCode,
+ int transactionCategoryCode, BigDecimal interestRate) {
+ this.accountGroupId = accountGroupId;
+ this.transactionTypeCode = transactionTypeCode;
+ this.transactionCategoryCode = transactionCategoryCode;
+ this.interestRate = interestRate;
+ }
+
+ public String getAccountGroupId() {
+ return accountGroupId;
+ }
+
+ public void setAccountGroupId(String accountGroupId) {
+ this.accountGroupId = accountGroupId;
+ }
+
+ public String getTransactionTypeCode() {
+ return transactionTypeCode;
+ }
+
+ public void setTransactionTypeCode(String transactionTypeCode) {
+ this.transactionTypeCode = transactionTypeCode;
+ }
+
+ public int getTransactionCategoryCode() {
+ return transactionCategoryCode;
+ }
+
+ public void setTransactionCategoryCode(int transactionCategoryCode) {
+ this.transactionCategoryCode = transactionCategoryCode;
+ }
+
+ public BigDecimal getInterestRate() {
+ return interestRate;
+ }
+
+ public void setInterestRate(BigDecimal interestRate) {
+ this.interestRate = interestRate;
+ }
+}
diff --git a/services/carddemo-refdata/src/main/java/com/carddemo/refdata/entity/DisclosureGroupId.java b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/entity/DisclosureGroupId.java
new file mode 100644
index 00000000..31dc9829
--- /dev/null
+++ b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/entity/DisclosureGroupId.java
@@ -0,0 +1,63 @@
+package com.carddemo.refdata.entity;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * Composite primary key for DisclosureGroup, matching the COBOL
+ * DIS-GROUP-KEY in CVTRA02Y.cpy (DIS-ACCT-GROUP-ID + DIS-TRAN-TYPE-CD + DIS-TRAN-CAT-CD).
+ */
+public class DisclosureGroupId implements Serializable {
+
+ private String accountGroupId;
+ private String transactionTypeCode;
+ private int transactionCategoryCode;
+
+ public DisclosureGroupId() {
+ }
+
+ public DisclosureGroupId(String accountGroupId, String transactionTypeCode, int transactionCategoryCode) {
+ this.accountGroupId = accountGroupId;
+ this.transactionTypeCode = transactionTypeCode;
+ this.transactionCategoryCode = transactionCategoryCode;
+ }
+
+ public String getAccountGroupId() {
+ return accountGroupId;
+ }
+
+ public void setAccountGroupId(String accountGroupId) {
+ this.accountGroupId = accountGroupId;
+ }
+
+ public String getTransactionTypeCode() {
+ return transactionTypeCode;
+ }
+
+ public void setTransactionTypeCode(String transactionTypeCode) {
+ this.transactionTypeCode = transactionTypeCode;
+ }
+
+ public int getTransactionCategoryCode() {
+ return transactionCategoryCode;
+ }
+
+ public void setTransactionCategoryCode(int transactionCategoryCode) {
+ this.transactionCategoryCode = transactionCategoryCode;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ DisclosureGroupId that = (DisclosureGroupId) o;
+ return transactionCategoryCode == that.transactionCategoryCode
+ && Objects.equals(accountGroupId, that.accountGroupId)
+ && Objects.equals(transactionTypeCode, that.transactionTypeCode);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(accountGroupId, transactionTypeCode, transactionCategoryCode);
+ }
+}
diff --git a/services/carddemo-refdata/src/main/java/com/carddemo/refdata/entity/TransactionCategory.java b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/entity/TransactionCategory.java
new file mode 100644
index 00000000..95b6b2cc
--- /dev/null
+++ b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/entity/TransactionCategory.java
@@ -0,0 +1,77 @@
+package com.carddemo.refdata.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.IdClass;
+import jakarta.persistence.Table;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+
+/**
+ * Maps to COBOL copybook CVTRA04Y.cpy — TRAN-CAT-RECORD (RECLN=60).
+ *
+ *
+ * 05 TRAN-CAT-KEY
+ * 10 TRAN-TYPE-CD PIC X(02) -> typeCode CHAR(2), composite PK part 1
+ * 10 TRAN-CAT-CD PIC 9(04) -> categoryCode INT, composite PK part 2
+ * 05 TRAN-CAT-TYPE-DESC PIC X(50) -> description VARCHAR(50)
+ * 05 FILLER PIC X(04) -- padding, not mapped
+ *
+ */
+@Entity
+@Table(name = "transaction_category")
+@IdClass(TransactionCategoryId.class)
+public class TransactionCategory {
+
+ @Id
+ @Column(name = "type_code", length = 2, nullable = false)
+ @NotBlank
+ @Size(min = 1, max = 2)
+ private String typeCode;
+
+ @Id
+ @Column(name = "category_code", nullable = false)
+ @Min(0)
+ @Max(9999)
+ private int categoryCode;
+
+ @Column(name = "description", length = 50)
+ @Size(max = 50)
+ private String description;
+
+ public TransactionCategory() {
+ }
+
+ public TransactionCategory(String typeCode, int categoryCode, String description) {
+ this.typeCode = typeCode;
+ this.categoryCode = categoryCode;
+ this.description = description;
+ }
+
+ public String getTypeCode() {
+ return typeCode;
+ }
+
+ public void setTypeCode(String typeCode) {
+ this.typeCode = typeCode;
+ }
+
+ public int getCategoryCode() {
+ return categoryCode;
+ }
+
+ public void setCategoryCode(int categoryCode) {
+ this.categoryCode = categoryCode;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+}
diff --git a/services/carddemo-refdata/src/main/java/com/carddemo/refdata/entity/TransactionCategoryId.java b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/entity/TransactionCategoryId.java
new file mode 100644
index 00000000..a194e2a8
--- /dev/null
+++ b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/entity/TransactionCategoryId.java
@@ -0,0 +1,51 @@
+package com.carddemo.refdata.entity;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * Composite primary key for TransactionCategory, matching the COBOL
+ * TRAN-CAT-KEY group in CVTRA04Y.cpy (TRAN-TYPE-CD + TRAN-CAT-CD).
+ */
+public class TransactionCategoryId implements Serializable {
+
+ private String typeCode;
+ private int categoryCode;
+
+ public TransactionCategoryId() {
+ }
+
+ public TransactionCategoryId(String typeCode, int categoryCode) {
+ this.typeCode = typeCode;
+ this.categoryCode = categoryCode;
+ }
+
+ public String getTypeCode() {
+ return typeCode;
+ }
+
+ public void setTypeCode(String typeCode) {
+ this.typeCode = typeCode;
+ }
+
+ public int getCategoryCode() {
+ return categoryCode;
+ }
+
+ public void setCategoryCode(int categoryCode) {
+ this.categoryCode = categoryCode;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TransactionCategoryId that = (TransactionCategoryId) o;
+ return categoryCode == that.categoryCode && Objects.equals(typeCode, that.typeCode);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(typeCode, categoryCode);
+ }
+}
diff --git a/services/carddemo-refdata/src/main/java/com/carddemo/refdata/entity/TransactionType.java b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/entity/TransactionType.java
new file mode 100644
index 00000000..6a041e00
--- /dev/null
+++ b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/entity/TransactionType.java
@@ -0,0 +1,56 @@
+package com.carddemo.refdata.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+
+/**
+ * Maps to COBOL copybook CVTRA03Y.cpy — TRAN-TYPE-RECORD (RECLN=60).
+ *
+ *
+ * 05 TRAN-TYPE PIC X(02) -> typeCode CHAR(2), PK
+ * 05 TRAN-TYPE-DESC PIC X(50) -> description VARCHAR(50)
+ * 05 FILLER PIC X(08) -- padding, not mapped
+ *
+ */
+@Entity
+@Table(name = "transaction_type")
+public class TransactionType {
+
+ @Id
+ @Column(name = "type_code", length = 2, nullable = false)
+ @NotBlank
+ @Size(min = 1, max = 2)
+ private String typeCode;
+
+ @Column(name = "description", length = 50)
+ @Size(max = 50)
+ private String description;
+
+ public TransactionType() {
+ }
+
+ public TransactionType(String typeCode, String description) {
+ this.typeCode = typeCode;
+ this.description = description;
+ }
+
+ public String getTypeCode() {
+ return typeCode;
+ }
+
+ public void setTypeCode(String typeCode) {
+ this.typeCode = typeCode;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+}
diff --git a/services/carddemo-refdata/src/main/java/com/carddemo/refdata/repository/DisclosureGroupRepository.java b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/repository/DisclosureGroupRepository.java
new file mode 100644
index 00000000..155445ce
--- /dev/null
+++ b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/repository/DisclosureGroupRepository.java
@@ -0,0 +1,14 @@
+package com.carddemo.refdata.repository;
+
+import java.util.List;
+
+import com.carddemo.refdata.entity.DisclosureGroup;
+import com.carddemo.refdata.entity.DisclosureGroupId;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface DisclosureGroupRepository extends JpaRepository {
+
+ List findByAccountGroupId(String accountGroupId);
+
+ List findByTransactionTypeCode(String transactionTypeCode);
+}
diff --git a/services/carddemo-refdata/src/main/java/com/carddemo/refdata/repository/TransactionCategoryRepository.java b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/repository/TransactionCategoryRepository.java
new file mode 100644
index 00000000..69b2fbb7
--- /dev/null
+++ b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/repository/TransactionCategoryRepository.java
@@ -0,0 +1,12 @@
+package com.carddemo.refdata.repository;
+
+import java.util.List;
+
+import com.carddemo.refdata.entity.TransactionCategory;
+import com.carddemo.refdata.entity.TransactionCategoryId;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface TransactionCategoryRepository extends JpaRepository {
+
+ List findByTypeCode(String typeCode);
+}
diff --git a/services/carddemo-refdata/src/main/java/com/carddemo/refdata/repository/TransactionTypeRepository.java b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/repository/TransactionTypeRepository.java
new file mode 100644
index 00000000..29f63e34
--- /dev/null
+++ b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/repository/TransactionTypeRepository.java
@@ -0,0 +1,7 @@
+package com.carddemo.refdata.repository;
+
+import com.carddemo.refdata.entity.TransactionType;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface TransactionTypeRepository extends JpaRepository {
+}
diff --git a/services/carddemo-refdata/src/main/java/com/carddemo/refdata/service/DisclosureGroupService.java b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/service/DisclosureGroupService.java
new file mode 100644
index 00000000..35807ddb
--- /dev/null
+++ b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/service/DisclosureGroupService.java
@@ -0,0 +1,60 @@
+package com.carddemo.refdata.service;
+
+import java.util.List;
+
+import com.carddemo.refdata.entity.DisclosureGroup;
+import com.carddemo.refdata.entity.DisclosureGroupId;
+import com.carddemo.refdata.repository.DisclosureGroupRepository;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional
+public class DisclosureGroupService {
+
+ private final DisclosureGroupRepository repository;
+
+ public DisclosureGroupService(DisclosureGroupRepository repository) {
+ this.repository = repository;
+ }
+
+ public List findAll() {
+ return repository.findAll();
+ }
+
+ public DisclosureGroup findByKey(String groupId, String typeCode, int catCode) {
+ return repository.findById(new DisclosureGroupId(groupId, typeCode, catCode)).orElse(null);
+ }
+
+ public DisclosureGroup create(DisclosureGroup entity) {
+ DisclosureGroupId id = new DisclosureGroupId(
+ entity.getAccountGroupId(),
+ entity.getTransactionTypeCode(),
+ entity.getTransactionCategoryCode());
+ if (repository.existsById(id)) {
+ throw new DuplicateEntityException("Disclosure group already exists: "
+ + entity.getAccountGroupId() + "/" + entity.getTransactionTypeCode()
+ + "/" + entity.getTransactionCategoryCode());
+ }
+ return repository.save(entity);
+ }
+
+ public DisclosureGroup update(String groupId, String typeCode, int catCode, DisclosureGroup entity) {
+ DisclosureGroupId id = new DisclosureGroupId(groupId, typeCode, catCode);
+ DisclosureGroup existing = repository.findById(id).orElse(null);
+ if (existing == null) {
+ return null;
+ }
+ existing.setInterestRate(entity.getInterestRate());
+ return repository.save(existing);
+ }
+
+ public boolean delete(String groupId, String typeCode, int catCode) {
+ DisclosureGroupId id = new DisclosureGroupId(groupId, typeCode, catCode);
+ if (!repository.existsById(id)) {
+ return false;
+ }
+ repository.deleteById(id);
+ return true;
+ }
+}
diff --git a/services/carddemo-refdata/src/main/java/com/carddemo/refdata/service/DuplicateEntityException.java b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/service/DuplicateEntityException.java
new file mode 100644
index 00000000..b5e9d1b1
--- /dev/null
+++ b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/service/DuplicateEntityException.java
@@ -0,0 +1,8 @@
+package com.carddemo.refdata.service;
+
+public class DuplicateEntityException extends RuntimeException {
+
+ public DuplicateEntityException(String message) {
+ super(message);
+ }
+}
diff --git a/services/carddemo-refdata/src/main/java/com/carddemo/refdata/service/TransactionCategoryService.java b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/service/TransactionCategoryService.java
new file mode 100644
index 00000000..b153d450
--- /dev/null
+++ b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/service/TransactionCategoryService.java
@@ -0,0 +1,56 @@
+package com.carddemo.refdata.service;
+
+import java.util.List;
+
+import com.carddemo.refdata.entity.TransactionCategory;
+import com.carddemo.refdata.entity.TransactionCategoryId;
+import com.carddemo.refdata.repository.TransactionCategoryRepository;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional
+public class TransactionCategoryService {
+
+ private final TransactionCategoryRepository repository;
+
+ public TransactionCategoryService(TransactionCategoryRepository repository) {
+ this.repository = repository;
+ }
+
+ public List findAll() {
+ return repository.findAll();
+ }
+
+ public TransactionCategory findByKey(String typeCode, int categoryCode) {
+ return repository.findById(new TransactionCategoryId(typeCode, categoryCode)).orElse(null);
+ }
+
+ public TransactionCategory create(TransactionCategory entity) {
+ TransactionCategoryId id = new TransactionCategoryId(entity.getTypeCode(), entity.getCategoryCode());
+ if (repository.existsById(id)) {
+ throw new DuplicateEntityException(
+ "Transaction category already exists: " + entity.getTypeCode() + "/" + entity.getCategoryCode());
+ }
+ return repository.save(entity);
+ }
+
+ public TransactionCategory update(String typeCode, int categoryCode, TransactionCategory entity) {
+ TransactionCategoryId id = new TransactionCategoryId(typeCode, categoryCode);
+ TransactionCategory existing = repository.findById(id).orElse(null);
+ if (existing == null) {
+ return null;
+ }
+ existing.setDescription(entity.getDescription());
+ return repository.save(existing);
+ }
+
+ public boolean delete(String typeCode, int categoryCode) {
+ TransactionCategoryId id = new TransactionCategoryId(typeCode, categoryCode);
+ if (!repository.existsById(id)) {
+ return false;
+ }
+ repository.deleteById(id);
+ return true;
+ }
+}
diff --git a/services/carddemo-refdata/src/main/java/com/carddemo/refdata/service/TransactionTypeService.java b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/service/TransactionTypeService.java
new file mode 100644
index 00000000..e3459c0a
--- /dev/null
+++ b/services/carddemo-refdata/src/main/java/com/carddemo/refdata/service/TransactionTypeService.java
@@ -0,0 +1,51 @@
+package com.carddemo.refdata.service;
+
+import java.util.List;
+
+import com.carddemo.refdata.entity.TransactionType;
+import com.carddemo.refdata.repository.TransactionTypeRepository;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional
+public class TransactionTypeService {
+
+ private final TransactionTypeRepository repository;
+
+ public TransactionTypeService(TransactionTypeRepository repository) {
+ this.repository = repository;
+ }
+
+ public List findAll() {
+ return repository.findAll();
+ }
+
+ public TransactionType findByCode(String code) {
+ return repository.findById(code).orElse(null);
+ }
+
+ public TransactionType create(TransactionType entity) {
+ if (repository.existsById(entity.getTypeCode())) {
+ throw new DuplicateEntityException("Transaction type already exists: " + entity.getTypeCode());
+ }
+ return repository.save(entity);
+ }
+
+ public TransactionType update(String code, TransactionType entity) {
+ TransactionType existing = repository.findById(code).orElse(null);
+ if (existing == null) {
+ return null;
+ }
+ existing.setDescription(entity.getDescription());
+ return repository.save(existing);
+ }
+
+ public boolean delete(String code) {
+ if (!repository.existsById(code)) {
+ return false;
+ }
+ repository.deleteById(code);
+ return true;
+ }
+}
diff --git a/services/carddemo-refdata/src/main/resources/application.yml b/services/carddemo-refdata/src/main/resources/application.yml
new file mode 100644
index 00000000..db6cc13c
--- /dev/null
+++ b/services/carddemo-refdata/src/main/resources/application.yml
@@ -0,0 +1,22 @@
+spring:
+ application:
+ name: carddemo-refdata
+
+ datasource:
+ url: jdbc:postgresql://localhost:5432/carddemo
+ username: carddemo
+ password: carddemo
+ driver-class-name: org.postgresql.Driver
+
+ jpa:
+ hibernate:
+ ddl-auto: validate
+ open-in-view: false
+ properties:
+ hibernate:
+ dialect: org.hibernate.dialect.PostgreSQLDialect
+
+server:
+ port: 8082
+ servlet:
+ context-path: /
diff --git a/services/carddemo-refdata/src/test/java/com/carddemo/refdata/RefDataApplicationTest.java b/services/carddemo-refdata/src/test/java/com/carddemo/refdata/RefDataApplicationTest.java
new file mode 100644
index 00000000..4a706544
--- /dev/null
+++ b/services/carddemo-refdata/src/test/java/com/carddemo/refdata/RefDataApplicationTest.java
@@ -0,0 +1,14 @@
+package com.carddemo.refdata;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+
+@SpringBootTest
+@ActiveProfiles("test")
+class RefDataApplicationTest {
+
+ @Test
+ void contextLoads() {
+ }
+}
diff --git a/services/carddemo-refdata/src/test/java/com/carddemo/refdata/controller/DisclosureGroupControllerTest.java b/services/carddemo-refdata/src/test/java/com/carddemo/refdata/controller/DisclosureGroupControllerTest.java
new file mode 100644
index 00000000..68274e81
--- /dev/null
+++ b/services/carddemo-refdata/src/test/java/com/carddemo/refdata/controller/DisclosureGroupControllerTest.java
@@ -0,0 +1,148 @@
+package com.carddemo.refdata.controller;
+
+import java.math.BigDecimal;
+
+import com.carddemo.refdata.entity.DisclosureGroup;
+import com.carddemo.refdata.repository.DisclosureGroupRepository;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+@ActiveProfiles("test")
+class DisclosureGroupControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private DisclosureGroupRepository repository;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @BeforeEach
+ void setUp() {
+ repository.deleteAll();
+ }
+
+ @Test
+ void listGroups_empty() throws Exception {
+ mockMvc.perform(get("/reference/disclosure-groups"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$", hasSize(0)));
+ }
+
+ @Test
+ void createAndGetGroup() throws Exception {
+ DisclosureGroup dg = new DisclosureGroup("GRP0000001", "SA", 1001, new BigDecimal("12.50"));
+ mockMvc.perform(post("/reference/disclosure-groups")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(dg)))
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.accountGroupId", is("GRP0000001")))
+ .andExpect(jsonPath("$.transactionTypeCode", is("SA")))
+ .andExpect(jsonPath("$.transactionCategoryCode", is(1001)))
+ .andExpect(jsonPath("$.interestRate", is(12.50)));
+
+ mockMvc.perform(get("/reference/disclosure-groups/GRP0000001/SA/1001"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.interestRate", is(12.50)));
+ }
+
+ @Test
+ void createDuplicate_returns409() throws Exception {
+ repository.save(new DisclosureGroup("GRP0000001", "SA", 1001, new BigDecimal("12.50")));
+ DisclosureGroup dup = new DisclosureGroup("GRP0000001", "SA", 1001, new BigDecimal("15.00"));
+ mockMvc.perform(post("/reference/disclosure-groups")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(dup)))
+ .andExpect(status().isConflict());
+ }
+
+ @Test
+ void getNotFound() throws Exception {
+ mockMvc.perform(get("/reference/disclosure-groups/NOGROUP/ZZ/0"))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ void updateGroup() throws Exception {
+ repository.save(new DisclosureGroup("GRP0000001", "SA", 1001, new BigDecimal("12.50")));
+ DisclosureGroup update = new DisclosureGroup("GRP0000001", "SA", 1001, new BigDecimal("18.75"));
+ mockMvc.perform(put("/reference/disclosure-groups/GRP0000001/SA/1001")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(update)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.interestRate", is(18.75)));
+ }
+
+ @Test
+ void updateNotFound() throws Exception {
+ DisclosureGroup update = new DisclosureGroup("NOGROUP", "ZZ", 0, new BigDecimal("1.00"));
+ mockMvc.perform(put("/reference/disclosure-groups/NOGROUP/ZZ/0")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(update)))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ void deleteGroup() throws Exception {
+ repository.save(new DisclosureGroup("GRP0000001", "SA", 1001, new BigDecimal("12.50")));
+ mockMvc.perform(delete("/reference/disclosure-groups/GRP0000001/SA/1001"))
+ .andExpect(status().isNoContent());
+
+ mockMvc.perform(get("/reference/disclosure-groups/GRP0000001/SA/1001"))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ void deleteNotFound() throws Exception {
+ mockMvc.perform(delete("/reference/disclosure-groups/NOGROUP/ZZ/0"))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ void createInvalid_nullRate() throws Exception {
+ String json = "{\"accountGroupId\":\"GRP0000001\",\"transactionTypeCode\":\"SA\","
+ + "\"transactionCategoryCode\":1001}";
+ mockMvc.perform(post("/reference/disclosure-groups")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(json))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void createInvalid_groupIdTooLong() throws Exception {
+ DisclosureGroup dg = new DisclosureGroup("TOOLONGGROUP", "SA", 1001, new BigDecimal("12.50"));
+ mockMvc.perform(post("/reference/disclosure-groups")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(dg)))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void createInvalid_rateTooManyDecimals() throws Exception {
+ DisclosureGroup dg = new DisclosureGroup("GRP0000001", "SA", 1001, new BigDecimal("12.501"));
+ mockMvc.perform(post("/reference/disclosure-groups")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(dg)))
+ .andExpect(status().isBadRequest());
+ }
+}
diff --git a/services/carddemo-refdata/src/test/java/com/carddemo/refdata/controller/TransactionCategoryControllerTest.java b/services/carddemo-refdata/src/test/java/com/carddemo/refdata/controller/TransactionCategoryControllerTest.java
new file mode 100644
index 00000000..7cdb0448
--- /dev/null
+++ b/services/carddemo-refdata/src/test/java/com/carddemo/refdata/controller/TransactionCategoryControllerTest.java
@@ -0,0 +1,135 @@
+package com.carddemo.refdata.controller;
+
+import com.carddemo.refdata.entity.TransactionCategory;
+import com.carddemo.refdata.repository.TransactionCategoryRepository;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+@ActiveProfiles("test")
+class TransactionCategoryControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private TransactionCategoryRepository repository;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @BeforeEach
+ void setUp() {
+ repository.deleteAll();
+ }
+
+ @Test
+ void listCategories_empty() throws Exception {
+ mockMvc.perform(get("/reference/categories"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$", hasSize(0)));
+ }
+
+ @Test
+ void createAndGetCategory() throws Exception {
+ TransactionCategory cat = new TransactionCategory("SA", 1001, "Online Sale");
+ mockMvc.perform(post("/reference/categories")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(cat)))
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.typeCode", is("SA")))
+ .andExpect(jsonPath("$.categoryCode", is(1001)))
+ .andExpect(jsonPath("$.description", is("Online Sale")));
+
+ mockMvc.perform(get("/reference/categories/SA/1001"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.description", is("Online Sale")));
+ }
+
+ @Test
+ void createDuplicate_returns409() throws Exception {
+ repository.save(new TransactionCategory("SA", 1001, "Online Sale"));
+ TransactionCategory dup = new TransactionCategory("SA", 1001, "Duplicate");
+ mockMvc.perform(post("/reference/categories")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(dup)))
+ .andExpect(status().isConflict());
+ }
+
+ @Test
+ void getNotFound() throws Exception {
+ mockMvc.perform(get("/reference/categories/ZZ/9999"))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ void updateCategory() throws Exception {
+ repository.save(new TransactionCategory("SA", 1001, "Online Sale"));
+ TransactionCategory update = new TransactionCategory("SA", 1001, "Updated Sale");
+ mockMvc.perform(put("/reference/categories/SA/1001")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(update)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.description", is("Updated Sale")));
+ }
+
+ @Test
+ void updateNotFound() throws Exception {
+ TransactionCategory update = new TransactionCategory("ZZ", 9999, "Nope");
+ mockMvc.perform(put("/reference/categories/ZZ/9999")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(update)))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ void deleteCategory() throws Exception {
+ repository.save(new TransactionCategory("SA", 1001, "Online Sale"));
+ mockMvc.perform(delete("/reference/categories/SA/1001"))
+ .andExpect(status().isNoContent());
+
+ mockMvc.perform(get("/reference/categories/SA/1001"))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ void deleteNotFound() throws Exception {
+ mockMvc.perform(delete("/reference/categories/ZZ/9999"))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ void createInvalid_codeOverflow() throws Exception {
+ TransactionCategory cat = new TransactionCategory("SA", 10000, "Overflow");
+ mockMvc.perform(post("/reference/categories")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(cat)))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void createInvalid_blankTypeCode() throws Exception {
+ TransactionCategory cat = new TransactionCategory("", 1001, "No type");
+ mockMvc.perform(post("/reference/categories")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(cat)))
+ .andExpect(status().isBadRequest());
+ }
+}
diff --git a/services/carddemo-refdata/src/test/java/com/carddemo/refdata/controller/TransactionTypeControllerTest.java b/services/carddemo-refdata/src/test/java/com/carddemo/refdata/controller/TransactionTypeControllerTest.java
new file mode 100644
index 00000000..2722de70
--- /dev/null
+++ b/services/carddemo-refdata/src/test/java/com/carddemo/refdata/controller/TransactionTypeControllerTest.java
@@ -0,0 +1,143 @@
+package com.carddemo.refdata.controller;
+
+import com.carddemo.refdata.entity.TransactionType;
+import com.carddemo.refdata.repository.TransactionTypeRepository;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+@ActiveProfiles("test")
+class TransactionTypeControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private TransactionTypeRepository repository;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @BeforeEach
+ void setUp() {
+ repository.deleteAll();
+ }
+
+ @Test
+ void listTypes_empty() throws Exception {
+ mockMvc.perform(get("/reference/types"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$", hasSize(0)));
+ }
+
+ @Test
+ void createAndGetType() throws Exception {
+ TransactionType tt = new TransactionType("SA", "Sale");
+ mockMvc.perform(post("/reference/types")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(tt)))
+ .andExpect(status().isCreated())
+ .andExpect(jsonPath("$.typeCode", is("SA")))
+ .andExpect(jsonPath("$.description", is("Sale")));
+
+ mockMvc.perform(get("/reference/types/SA"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.typeCode", is("SA")));
+ }
+
+ @Test
+ void createDuplicate_returns409() throws Exception {
+ repository.save(new TransactionType("SA", "Sale"));
+ TransactionType dup = new TransactionType("SA", "Sale Again");
+ mockMvc.perform(post("/reference/types")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(dup)))
+ .andExpect(status().isConflict());
+ }
+
+ @Test
+ void getNotFound() throws Exception {
+ mockMvc.perform(get("/reference/types/ZZ"))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ void updateType() throws Exception {
+ repository.save(new TransactionType("SA", "Sale"));
+ TransactionType update = new TransactionType("SA", "Updated Sale");
+ mockMvc.perform(put("/reference/types/SA")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(update)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.description", is("Updated Sale")));
+ }
+
+ @Test
+ void updateNotFound() throws Exception {
+ TransactionType update = new TransactionType("ZZ", "Nope");
+ mockMvc.perform(put("/reference/types/ZZ")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(update)))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ void deleteType() throws Exception {
+ repository.save(new TransactionType("SA", "Sale"));
+ mockMvc.perform(delete("/reference/types/SA"))
+ .andExpect(status().isNoContent());
+
+ mockMvc.perform(get("/reference/types/SA"))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ void deleteNotFound() throws Exception {
+ mockMvc.perform(delete("/reference/types/ZZ"))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ void createInvalid_blankCode() throws Exception {
+ TransactionType tt = new TransactionType("", "No Code");
+ mockMvc.perform(post("/reference/types")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(tt)))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void createInvalid_codeToLong() throws Exception {
+ TransactionType tt = new TransactionType("ABC", "Too long code");
+ mockMvc.perform(post("/reference/types")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(tt)))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void listTypes_returnsAll() throws Exception {
+ repository.save(new TransactionType("SA", "Sale"));
+ repository.save(new TransactionType("RE", "Return"));
+ mockMvc.perform(get("/reference/types"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$", hasSize(2)));
+ }
+}
diff --git a/services/carddemo-refdata/src/test/java/com/carddemo/refdata/repository/TransactionTypeRepositoryTest.java b/services/carddemo-refdata/src/test/java/com/carddemo/refdata/repository/TransactionTypeRepositoryTest.java
new file mode 100644
index 00000000..1fef5853
--- /dev/null
+++ b/services/carddemo-refdata/src/test/java/com/carddemo/refdata/repository/TransactionTypeRepositoryTest.java
@@ -0,0 +1,42 @@
+package com.carddemo.refdata.repository;
+
+import java.util.Optional;
+
+import com.carddemo.refdata.entity.TransactionType;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.test.context.ActiveProfiles;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@DataJpaTest
+@ActiveProfiles("test")
+class TransactionTypeRepositoryTest {
+
+ @Autowired
+ private TransactionTypeRepository repository;
+
+ @Test
+ void saveAndFind() {
+ repository.save(new TransactionType("SA", "Sale"));
+ Optional found = repository.findById("SA");
+ assertTrue(found.isPresent());
+ assertEquals("Sale", found.get().getDescription());
+ }
+
+ @Test
+ void findAll_afterMultipleSaves() {
+ repository.save(new TransactionType("SA", "Sale"));
+ repository.save(new TransactionType("RE", "Return"));
+ assertEquals(2, repository.findAll().size());
+ }
+
+ @Test
+ void deleteById() {
+ repository.save(new TransactionType("SA", "Sale"));
+ repository.deleteById("SA");
+ assertTrue(repository.findById("SA").isEmpty());
+ }
+}
diff --git a/services/carddemo-refdata/src/test/java/com/carddemo/refdata/service/DisclosureGroupServiceTest.java b/services/carddemo-refdata/src/test/java/com/carddemo/refdata/service/DisclosureGroupServiceTest.java
new file mode 100644
index 00000000..65389b8a
--- /dev/null
+++ b/services/carddemo-refdata/src/test/java/com/carddemo/refdata/service/DisclosureGroupServiceTest.java
@@ -0,0 +1,106 @@
+package com.carddemo.refdata.service;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Optional;
+
+import com.carddemo.refdata.entity.DisclosureGroup;
+import com.carddemo.refdata.entity.DisclosureGroupId;
+import com.carddemo.refdata.repository.DisclosureGroupRepository;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class DisclosureGroupServiceTest {
+
+ @Mock
+ private DisclosureGroupRepository repository;
+
+ @InjectMocks
+ private DisclosureGroupService service;
+
+ @Test
+ void findAll_returnsList() {
+ when(repository.findAll()).thenReturn(List.of(
+ new DisclosureGroup("GRP001", "SA", 1001, new BigDecimal("12.50"))));
+ assertEquals(1, service.findAll().size());
+ }
+
+ @Test
+ void findByKey_found() {
+ DisclosureGroupId id = new DisclosureGroupId("GRP001", "SA", 1001);
+ when(repository.findById(id)).thenReturn(
+ Optional.of(new DisclosureGroup("GRP001", "SA", 1001, new BigDecimal("12.50"))));
+ DisclosureGroup result = service.findByKey("GRP001", "SA", 1001);
+ assertNotNull(result);
+ assertEquals(new BigDecimal("12.50"), result.getInterestRate());
+ }
+
+ @Test
+ void findByKey_notFound() {
+ when(repository.findById(any())).thenReturn(Optional.empty());
+ assertNull(service.findByKey("X", "ZZ", 0));
+ }
+
+ @Test
+ void create_success() {
+ DisclosureGroup dg = new DisclosureGroup("GRP001", "SA", 1001, new BigDecimal("12.50"));
+ when(repository.existsById(any())).thenReturn(false);
+ when(repository.save(dg)).thenReturn(dg);
+ DisclosureGroup result = service.create(dg);
+ assertEquals("GRP001", result.getAccountGroupId());
+ }
+
+ @Test
+ void create_duplicate_throws() {
+ DisclosureGroup dg = new DisclosureGroup("GRP001", "SA", 1001, new BigDecimal("12.50"));
+ when(repository.existsById(any())).thenReturn(true);
+ assertThrows(DuplicateEntityException.class, () -> service.create(dg));
+ verify(repository, never()).save(any());
+ }
+
+ @Test
+ void update_found() {
+ DisclosureGroup existing = new DisclosureGroup("GRP001", "SA", 1001, new BigDecimal("12.50"));
+ when(repository.findById(any())).thenReturn(Optional.of(existing));
+ when(repository.save(existing)).thenReturn(existing);
+
+ DisclosureGroup update = new DisclosureGroup("GRP001", "SA", 1001, new BigDecimal("18.75"));
+ DisclosureGroup result = service.update("GRP001", "SA", 1001, update);
+ assertNotNull(result);
+ assertEquals(new BigDecimal("18.75"), result.getInterestRate());
+ }
+
+ @Test
+ void update_notFound() {
+ when(repository.findById(any())).thenReturn(Optional.empty());
+ assertNull(service.update("X", "ZZ", 0,
+ new DisclosureGroup("X", "ZZ", 0, BigDecimal.ONE)));
+ }
+
+ @Test
+ void delete_found() {
+ when(repository.existsById(any())).thenReturn(true);
+ assertTrue(service.delete("GRP001", "SA", 1001));
+ }
+
+ @Test
+ void delete_notFound() {
+ when(repository.existsById(any())).thenReturn(false);
+ assertFalse(service.delete("X", "ZZ", 0));
+ }
+}
diff --git a/services/carddemo-refdata/src/test/java/com/carddemo/refdata/service/TransactionCategoryServiceTest.java b/services/carddemo-refdata/src/test/java/com/carddemo/refdata/service/TransactionCategoryServiceTest.java
new file mode 100644
index 00000000..f5a5125c
--- /dev/null
+++ b/services/carddemo-refdata/src/test/java/com/carddemo/refdata/service/TransactionCategoryServiceTest.java
@@ -0,0 +1,104 @@
+package com.carddemo.refdata.service;
+
+import java.util.List;
+import java.util.Optional;
+
+import com.carddemo.refdata.entity.TransactionCategory;
+import com.carddemo.refdata.entity.TransactionCategoryId;
+import com.carddemo.refdata.repository.TransactionCategoryRepository;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class TransactionCategoryServiceTest {
+
+ @Mock
+ private TransactionCategoryRepository repository;
+
+ @InjectMocks
+ private TransactionCategoryService service;
+
+ @Test
+ void findAll_returnsList() {
+ when(repository.findAll()).thenReturn(List.of(
+ new TransactionCategory("SA", 1001, "Online Sale")));
+ assertEquals(1, service.findAll().size());
+ }
+
+ @Test
+ void findByKey_found() {
+ TransactionCategoryId id = new TransactionCategoryId("SA", 1001);
+ when(repository.findById(id)).thenReturn(
+ Optional.of(new TransactionCategory("SA", 1001, "Online Sale")));
+ TransactionCategory result = service.findByKey("SA", 1001);
+ assertNotNull(result);
+ assertEquals("Online Sale", result.getDescription());
+ }
+
+ @Test
+ void findByKey_notFound() {
+ when(repository.findById(any())).thenReturn(Optional.empty());
+ assertNull(service.findByKey("ZZ", 9999));
+ }
+
+ @Test
+ void create_success() {
+ TransactionCategory cat = new TransactionCategory("SA", 1001, "Online Sale");
+ when(repository.existsById(any())).thenReturn(false);
+ when(repository.save(cat)).thenReturn(cat);
+ TransactionCategory result = service.create(cat);
+ assertEquals(1001, result.getCategoryCode());
+ }
+
+ @Test
+ void create_duplicate_throws() {
+ TransactionCategory cat = new TransactionCategory("SA", 1001, "Online Sale");
+ when(repository.existsById(any())).thenReturn(true);
+ assertThrows(DuplicateEntityException.class, () -> service.create(cat));
+ verify(repository, never()).save(any());
+ }
+
+ @Test
+ void update_found() {
+ TransactionCategory existing = new TransactionCategory("SA", 1001, "Online Sale");
+ when(repository.findById(any())).thenReturn(Optional.of(existing));
+ when(repository.save(existing)).thenReturn(existing);
+
+ TransactionCategory update = new TransactionCategory("SA", 1001, "Updated");
+ TransactionCategory result = service.update("SA", 1001, update);
+ assertNotNull(result);
+ assertEquals("Updated", result.getDescription());
+ }
+
+ @Test
+ void update_notFound() {
+ when(repository.findById(any())).thenReturn(Optional.empty());
+ assertNull(service.update("ZZ", 9999, new TransactionCategory("ZZ", 9999, "Nope")));
+ }
+
+ @Test
+ void delete_found() {
+ when(repository.existsById(any())).thenReturn(true);
+ assertTrue(service.delete("SA", 1001));
+ }
+
+ @Test
+ void delete_notFound() {
+ when(repository.existsById(any())).thenReturn(false);
+ assertFalse(service.delete("ZZ", 9999));
+ }
+}
diff --git a/services/carddemo-refdata/src/test/java/com/carddemo/refdata/service/TransactionTypeServiceTest.java b/services/carddemo-refdata/src/test/java/com/carddemo/refdata/service/TransactionTypeServiceTest.java
new file mode 100644
index 00000000..247abff5
--- /dev/null
+++ b/services/carddemo-refdata/src/test/java/com/carddemo/refdata/service/TransactionTypeServiceTest.java
@@ -0,0 +1,105 @@
+package com.carddemo.refdata.service;
+
+import java.util.List;
+import java.util.Optional;
+
+import com.carddemo.refdata.entity.TransactionType;
+import com.carddemo.refdata.repository.TransactionTypeRepository;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class TransactionTypeServiceTest {
+
+ @Mock
+ private TransactionTypeRepository repository;
+
+ @InjectMocks
+ private TransactionTypeService service;
+
+ @Test
+ void findAll_returnsList() {
+ when(repository.findAll()).thenReturn(List.of(
+ new TransactionType("SA", "Sale"),
+ new TransactionType("RE", "Return")));
+ List result = service.findAll();
+ assertEquals(2, result.size());
+ }
+
+ @Test
+ void findByCode_found() {
+ when(repository.findById("SA")).thenReturn(Optional.of(new TransactionType("SA", "Sale")));
+ TransactionType result = service.findByCode("SA");
+ assertNotNull(result);
+ assertEquals("Sale", result.getDescription());
+ }
+
+ @Test
+ void findByCode_notFound() {
+ when(repository.findById("ZZ")).thenReturn(Optional.empty());
+ assertNull(service.findByCode("ZZ"));
+ }
+
+ @Test
+ void create_success() {
+ TransactionType tt = new TransactionType("SA", "Sale");
+ when(repository.existsById("SA")).thenReturn(false);
+ when(repository.save(tt)).thenReturn(tt);
+ TransactionType result = service.create(tt);
+ assertEquals("SA", result.getTypeCode());
+ }
+
+ @Test
+ void create_duplicate_throws() {
+ TransactionType tt = new TransactionType("SA", "Sale");
+ when(repository.existsById("SA")).thenReturn(true);
+ assertThrows(DuplicateEntityException.class, () -> service.create(tt));
+ verify(repository, never()).save(any());
+ }
+
+ @Test
+ void update_found() {
+ TransactionType existing = new TransactionType("SA", "Sale");
+ when(repository.findById("SA")).thenReturn(Optional.of(existing));
+ when(repository.save(existing)).thenReturn(existing);
+
+ TransactionType update = new TransactionType("SA", "Updated");
+ TransactionType result = service.update("SA", update);
+ assertNotNull(result);
+ assertEquals("Updated", result.getDescription());
+ }
+
+ @Test
+ void update_notFound() {
+ when(repository.findById("ZZ")).thenReturn(Optional.empty());
+ assertNull(service.update("ZZ", new TransactionType("ZZ", "Nope")));
+ }
+
+ @Test
+ void delete_found() {
+ when(repository.existsById("SA")).thenReturn(true);
+ assertTrue(service.delete("SA"));
+ verify(repository).deleteById("SA");
+ }
+
+ @Test
+ void delete_notFound() {
+ when(repository.existsById("ZZ")).thenReturn(false);
+ assertFalse(service.delete("ZZ"));
+ }
+}
diff --git a/services/carddemo-refdata/src/test/resources/application-test.yml b/services/carddemo-refdata/src/test/resources/application-test.yml
new file mode 100644
index 00000000..ce3a76e4
--- /dev/null
+++ b/services/carddemo-refdata/src/test/resources/application-test.yml
@@ -0,0 +1,13 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=PostgreSQL
+ username: sa
+ password:
+ driver-class-name: org.h2.Driver
+
+ jpa:
+ hibernate:
+ ddl-auto: create-drop
+ properties:
+ hibernate:
+ dialect: org.hibernate.dialect.H2Dialect