initial implementation of loan approval service

This commit is contained in:
2025-12-05 10:22:10 +01:00
parent ef5b7072cf
commit 2fe315b7df
16 changed files with 784 additions and 2 deletions

View File

@ -12,7 +12,12 @@
"Bash(pip install:*)",
"Bash(python:*)",
"Bash(git init:*)",
"Bash(git add:*)"
"Bash(git add:*)",
"Bash(git commit -m \"$(cat <<''EOF''\nInitial commit: JAX-WS Hello World Service\n\n- Complete JAX-WS Hello World implementation\n- Docker and Docker Compose support for easy deployment\n- Python test scripts for service validation\n- Comprehensive README with setup instructions for Windows and Linux\n- Maven configuration with JAX-WS dependencies\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\nEOF\n)\")",
"Bash(git remote add:*)",
"Bash(git push:*)",
"Bash(git commit:*)",
"Bash(docker-compose ps:*)"
],
"deny": [],
"ask": []

View File

@ -63,6 +63,11 @@ The easiest way to test this application is using Docker. No need to install Jav
docker-compose up -d
```
**1. Start the application ( when you changed code ):**
```bash
docker-compose up -d --build
```
**2. Wait for the application to start (about 30-40 seconds), then access:**
- **WSDL**: http://localhost:8080/jaxws-hello-world/hello?wsdl
- **Tomcat Manager**: http://localhost:8080/manager (username: `admin`, password: `admin123`)

View File

@ -41,6 +41,13 @@
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- SQLite JDBC Driver -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.43.0.0</version>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,47 @@
package com.example.listener;
import com.example.util.DatabaseManager;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
/**
* DatabaseInitializationListener - Initializes database schema when application starts
*/
public class DatabaseInitializationListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("===========================================");
System.out.println("Starting Database Initialization...");
System.out.println("===========================================");
try {
DatabaseManager dbManager = DatabaseManager.getInstance();
// Test connection
if (dbManager.testConnection()) {
System.out.println("Database connection test: PASSED");
} else {
System.err.println("Database connection test: FAILED");
return;
}
// Initialize schema (create tables if they don't exist)
dbManager.initializeSchema();
System.out.println("Database initialization completed successfully");
System.out.println("===========================================");
} catch (Exception e) {
System.err.println("FATAL: Database initialization failed!");
e.printStackTrace();
throw new RuntimeException("Failed to initialize database", e);
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("Application shutdown - Database cleanup completed");
}
}

View File

@ -0,0 +1,42 @@
package com.example.model;
/**
* CustomerRegistrationRequest - Input model for customer registration
*/
public class CustomerRegistrationRequest {
private String customerName;
private boolean blacklisted;
public CustomerRegistrationRequest() {
}
public CustomerRegistrationRequest(String customerName, boolean blacklisted) {
this.customerName = customerName;
this.blacklisted = blacklisted;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
public boolean isBlacklisted() {
return blacklisted;
}
public void setBlacklisted(boolean blacklisted) {
this.blacklisted = blacklisted;
}
@Override
public String toString() {
return "CustomerRegistrationRequest{" +
"customerName='" + customerName + '\'' +
", blacklisted=" + blacklisted +
'}';
}
}

View File

@ -0,0 +1,53 @@
package com.example.model;
/**
* LoanRequest - Input model for loan application processing
*/
public class LoanRequest {
private String applicantName;
private int requestedAmount;
private int creditScore;
public LoanRequest() {
}
public LoanRequest(String applicantName, int requestedAmount, int creditScore) {
this.applicantName = applicantName;
this.requestedAmount = requestedAmount;
this.creditScore = creditScore;
}
public String getApplicantName() {
return applicantName;
}
public void setApplicantName(String applicantName) {
this.applicantName = applicantName;
}
public int getRequestedAmount() {
return requestedAmount;
}
public void setRequestedAmount(int requestedAmount) {
this.requestedAmount = requestedAmount;
}
public int getCreditScore() {
return creditScore;
}
public void setCreditScore(int creditScore) {
this.creditScore = creditScore;
}
@Override
public String toString() {
return "LoanRequest{" +
"applicantName='" + applicantName + '\'' +
", requestedAmount=" + requestedAmount +
", creditScore=" + creditScore +
'}';
}
}

View File

@ -0,0 +1,64 @@
package com.example.model;
/**
* LoanResponse - Output model for loan application result
*/
public class LoanResponse {
private boolean approved;
private double approvedRate;
private String rejectionReason;
private String message;
public LoanResponse() {
}
public LoanResponse(boolean approved, double approvedRate, String rejectionReason, String message) {
this.approved = approved;
this.approvedRate = approvedRate;
this.rejectionReason = rejectionReason;
this.message = message;
}
public boolean isApproved() {
return approved;
}
public void setApproved(boolean approved) {
this.approved = approved;
}
public double getApprovedRate() {
return approvedRate;
}
public void setApprovedRate(double approvedRate) {
this.approvedRate = approvedRate;
}
public String getRejectionReason() {
return rejectionReason;
}
public void setRejectionReason(String rejectionReason) {
this.rejectionReason = rejectionReason;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public String toString() {
return "LoanResponse{" +
"approved=" + approved +
", approvedRate=" + approvedRate +
", rejectionReason='" + rejectionReason + '\'' +
", message='" + message + '\'' +
'}';
}
}

View File

@ -0,0 +1,22 @@
package com.example.repository;
/**
* CustomerRepository - Interface for customer data access
*/
public interface CustomerRepository {
/**
* Checks if the customer is on the blacklist
* @param name Customer name
* @return true if blacklisted, false otherwise
*/
boolean isBlacklisted(String name);
/**
* Registers a new customer into the database
* @param name Customer name
* @param isBlacklisted Blacklist status
* @return true if registered successfully, false if customer already exists
*/
boolean registerCustomer(String name, boolean isBlacklisted);
}

View File

@ -0,0 +1,18 @@
package com.example.repository;
/**
* LoanHistoryRepository - Interface for loan history data access
*/
public interface LoanHistoryRepository {
/**
* Saves loan application history to the database
* @param applicantName Name of the applicant
* @param requestedAmount Requested loan amount
* @param approved Whether the loan was approved
* @param approvedRate Interest rate if approved
* @param rejectionReason Reason for rejection if declined
*/
void saveHistory(String applicantName, int requestedAmount, boolean approved,
double approvedRate, String rejectionReason);
}

View File

@ -0,0 +1,102 @@
package com.example.repository.impl;
import com.example.repository.CustomerRepository;
import com.example.util.DatabaseManager;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* CustomerRepositoryImpl - SQLite implementation of CustomerRepository
*/
public class CustomerRepositoryImpl implements CustomerRepository {
private final DatabaseManager dbManager;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public CustomerRepositoryImpl() {
this.dbManager = DatabaseManager.getInstance();
}
@Override
public boolean isBlacklisted(String name) {
String query = "SELECT is_blacklisted FROM customers WHERE customer_name = ?";
try (Connection conn = dbManager.getConnection();
PreparedStatement pstmt = conn.prepareStatement(query)) {
pstmt.setString(1, name);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
int blacklisted = rs.getInt("is_blacklisted");
return blacklisted == 1;
}
}
// Customer not found, not blacklisted by default
return false;
} catch (SQLException e) {
throw new RuntimeException("Error checking blacklist status for: " + name, e);
}
}
@Override
public boolean registerCustomer(String name, boolean isBlacklisted) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Customer name cannot be null or empty");
}
// Check if customer already exists
if (customerExists(name)) {
return false;
}
String insert = "INSERT INTO customers (customer_name, is_blacklisted, registered_at) VALUES (?, ?, ?)";
try (Connection conn = dbManager.getConnection();
PreparedStatement pstmt = conn.prepareStatement(insert)) {
pstmt.setString(1, name);
pstmt.setInt(2, isBlacklisted ? 1 : 0);
pstmt.setString(3, LocalDateTime.now().format(DATE_FORMATTER));
int rowsAffected = pstmt.executeUpdate();
return rowsAffected > 0;
} catch (SQLException e) {
throw new RuntimeException("Error registering customer: " + name, e);
}
}
/**
* Helper method to check if customer already exists
* @param name Customer name
* @return true if exists, false otherwise
*/
private boolean customerExists(String name) {
String query = "SELECT COUNT(*) as count FROM customers WHERE customer_name = ?";
try (Connection conn = dbManager.getConnection();
PreparedStatement pstmt = conn.prepareStatement(query)) {
pstmt.setString(1, name);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
return rs.getInt("count") > 0;
}
}
return false;
} catch (SQLException e) {
throw new RuntimeException("Error checking customer existence: " + name, e);
}
}
}

View File

@ -0,0 +1,48 @@
package com.example.repository.impl;
import com.example.repository.LoanHistoryRepository;
import com.example.util.DatabaseManager;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* LoanHistoryRepositoryImpl - SQLite implementation of LoanHistoryRepository
*/
public class LoanHistoryRepositoryImpl implements LoanHistoryRepository {
private final DatabaseManager dbManager;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public LoanHistoryRepositoryImpl() {
this.dbManager = DatabaseManager.getInstance();
}
@Override
public void saveHistory(String applicantName, int requestedAmount, boolean approved,
double approvedRate, String rejectionReason) {
String insert = "INSERT INTO loan_history " +
"(applicant_name, requested_amount, approved, approved_rate, rejection_reason, processed_at) " +
"VALUES (?, ?, ?, ?, ?, ?)";
try (Connection conn = dbManager.getConnection();
PreparedStatement pstmt = conn.prepareStatement(insert)) {
pstmt.setString(1, applicantName);
pstmt.setInt(2, requestedAmount);
pstmt.setInt(3, approved ? 1 : 0);
pstmt.setDouble(4, approvedRate);
pstmt.setString(5, rejectionReason);
pstmt.setString(6, LocalDateTime.now().format(DATE_FORMATTER));
pstmt.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException("Error saving loan history for: " + applicantName, e);
}
}
}

View File

@ -0,0 +1,15 @@
package com.example.service;
/**
* CreditScoreService - Interface for external credit score checking
* This interface is designed to be mocked in unit tests
*/
public interface CreditScoreService {
/**
* Get credit score for an applicant
* @param applicantName Name of the applicant
* @return Credit score (0-850)
*/
int getCreditScore(String applicantName);
}

View File

@ -0,0 +1,193 @@
package com.example.service;
import com.example.model.CustomerRegistrationRequest;
import com.example.model.LoanRequest;
import com.example.model.LoanResponse;
import com.example.repository.CustomerRepository;
import com.example.repository.LoanHistoryRepository;
import com.example.repository.impl.CustomerRepositoryImpl;
import com.example.repository.impl.LoanHistoryRepositoryImpl;
import javax.jws.WebMethod;
import javax.jws.WebService;
import javax.xml.ws.soap.SOAPFaultException;
import javax.xml.soap.SOAPFactory;
import javax.xml.soap.SOAPFault;
/**
* LoanApprovalService - Main JAX-WS service for loan processing and customer management
*/
@WebService(
serviceName = "LoanApprovalService",
portName = "LoanApprovalPort"
)
public class LoanApprovalService {
private CustomerRepository customerRepository;
private LoanHistoryRepository loanHistoryRepository;
private CreditScoreService creditScoreService;
/**
* Default constructor - initializes with real implementations
*/
public LoanApprovalService() {
this.customerRepository = new CustomerRepositoryImpl();
this.loanHistoryRepository = new LoanHistoryRepositoryImpl();
// Default credit score service (can be overridden for testing)
this.creditScoreService = new CreditScoreService() {
@Override
public int getCreditScore(String applicantName) {
// Simple mock implementation - returns a fixed score
// In production, this would call an external API
return 700;
}
};
}
/**
* Setter for CustomerRepository (for dependency injection in tests)
*/
@WebMethod(exclude = true)
public void setCustomerRepository(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
/**
* Setter for LoanHistoryRepository (for dependency injection in tests)
*/
@WebMethod(exclude = true)
public void setLoanHistoryRepository(LoanHistoryRepository loanHistoryRepository) {
this.loanHistoryRepository = loanHistoryRepository;
}
/**
* Setter for CreditScoreService (for dependency injection in tests)
*/
@WebMethod(exclude = true)
public void setCreditScoreService(CreditScoreService creditScoreService) {
this.creditScoreService = creditScoreService;
}
/**
* Register a new customer
* @param request Customer registration request
* @return Success or error message
*/
@WebMethod
public String registerNewCustomer(CustomerRegistrationRequest request) {
// Input validation
if (request == null || request.getCustomerName() == null ||
request.getCustomerName().trim().isEmpty()) {
throw createSOAPFault("Customer name cannot be null or empty");
}
// Register customer
boolean registered = customerRepository.registerCustomer(
request.getCustomerName(),
request.isBlacklisted()
);
if (registered) {
return "Registration Successful";
} else {
return "Error: Customer already exists";
}
}
/**
* Process loan application
* @param request Loan application request
* @return Loan decision response
*/
@WebMethod
public LoanResponse processLoanApplication(LoanRequest request) {
// Input validation
if (request == null || request.getApplicantName() == null ||
request.getApplicantName().trim().isEmpty()) {
throw createSOAPFault("Applicant name cannot be null or empty");
}
if (request.getRequestedAmount() <= 0) {
throw createSOAPFault("Requested amount must be greater than 0");
}
// Check blacklist
if (customerRepository.isBlacklisted(request.getApplicantName())) {
LoanResponse response = new LoanResponse(
false,
0.0,
"Applicant is blacklisted",
"Loan application rejected"
);
// Save history
loanHistoryRepository.saveHistory(
request.getApplicantName(),
request.getRequestedAmount(),
false,
0.0,
"Applicant is blacklisted"
);
return response;
}
// Get credit score
int creditScore = creditScoreService.getCreditScore(request.getApplicantName());
// Loan approval logic
boolean approved;
double approvedRate;
String rejectionReason = null;
String message;
if (creditScore >= 700) {
// Good credit - approved with low rate
approved = true;
approvedRate = 3.5;
message = "Loan approved with excellent rate";
} else if (creditScore >= 600) {
// Fair credit - approved with standard rate
approved = true;
approvedRate = 5.5;
message = "Loan approved with standard rate";
} else if (creditScore >= 500) {
// Poor credit - approved with high rate
approved = true;
approvedRate = 8.5;
message = "Loan approved with high risk rate";
} else {
// Very poor credit - rejected
approved = false;
approvedRate = 0.0;
rejectionReason = "Credit score too low";
message = "Loan application rejected";
}
// Save history
loanHistoryRepository.saveHistory(
request.getApplicantName(),
request.getRequestedAmount(),
approved,
approvedRate,
rejectionReason
);
// Create response
return new LoanResponse(approved, approvedRate, rejectionReason, message);
}
/**
* Helper method to create SOAP fault
*/
private SOAPFaultException createSOAPFault(String message) {
try {
SOAPFactory soapFactory = SOAPFactory.newInstance();
SOAPFault soapFault = soapFactory.createFault();
soapFault.setFaultString(message);
return new SOAPFaultException(soapFault);
} catch (Exception e) {
throw new RuntimeException("Error creating SOAP fault", e);
}
}
}

View File

@ -0,0 +1,137 @@
package com.example.util;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
/**
* DatabaseManager - Utility class for managing SQLite database connections and schema initialization.
* This class provides methods to:
* - Get database connections
* - Initialize database schema (create tables)
* - Reset database (drop and recreate tables)
*/
public class DatabaseManager {
private static final String DB_URL = "jdbc:sqlite:loan_app.db";
private static DatabaseManager instance;
// Private constructor for singleton pattern
private DatabaseManager() {
try {
// Load SQLite JDBC driver
Class.forName("org.sqlite.JDBC");
} catch (ClassNotFoundException e) {
throw new RuntimeException("Failed to load SQLite JDBC driver", e);
}
}
/**
* Get singleton instance of DatabaseManager
* @return DatabaseManager instance
*/
public static synchronized DatabaseManager getInstance() {
if (instance == null) {
instance = new DatabaseManager();
}
return instance;
}
/**
* Get a new database connection
* @return Connection object
* @throws SQLException if connection fails
*/
public Connection getConnection() throws SQLException {
return DriverManager.getConnection(DB_URL);
}
/**
* Initialize database schema - create tables if they don't exist
*/
public void initializeSchema() {
String createCustomersTable =
"CREATE TABLE IF NOT EXISTS customers (" +
" customer_name TEXT PRIMARY KEY," +
" is_blacklisted INTEGER DEFAULT 0," +
" registered_at TEXT" +
")";
String createLoanHistoryTable =
"CREATE TABLE IF NOT EXISTS loan_history (" +
" id INTEGER PRIMARY KEY AUTOINCREMENT," +
" applicant_name TEXT," +
" requested_amount INTEGER," +
" approved INTEGER," +
" approved_rate REAL," +
" rejection_reason TEXT," +
" processed_at TEXT" +
")";
try (Connection conn = getConnection();
Statement stmt = conn.createStatement()) {
// Create tables
stmt.execute(createCustomersTable);
stmt.execute(createLoanHistoryTable);
System.out.println("Database schema initialized successfully");
} catch (SQLException e) {
throw new RuntimeException("Failed to initialize database schema", e);
}
}
/**
* Reset database - drop all tables and recreate them
* Useful for testing scenarios
*/
public void resetDatabase() {
String dropCustomers = "DROP TABLE IF EXISTS customers";
String dropLoanHistory = "DROP TABLE IF EXISTS loan_history";
try (Connection conn = getConnection();
Statement stmt = conn.createStatement()) {
// Drop tables
stmt.execute(dropCustomers);
stmt.execute(dropLoanHistory);
System.out.println("Database tables dropped");
// Recreate tables
initializeSchema();
} catch (SQLException e) {
throw new RuntimeException("Failed to reset database", e);
}
}
/**
* Test database connection
* @return true if connection is successful
*/
public boolean testConnection() {
try (Connection conn = getConnection()) {
return conn != null && !conn.isClosed();
} catch (SQLException e) {
System.err.println("Database connection test failed: " + e.getMessage());
return false;
}
}
/**
* Close connection safely
* @param conn Connection to close
*/
public static void closeConnection(Connection conn) {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
System.err.println("Error closing connection: " + e.getMessage());
}
}
}
}

View File

@ -6,4 +6,9 @@
implementation="com.example.service.HelloWorldServiceImpl"
url-pattern="/hello"/>
<endpoint
name="LoanApprovalService"
implementation="com.example.service.LoanApprovalService"
url-pattern="/loan"/>
</endpoints>

View File

@ -5,12 +5,19 @@
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<display-name>JAX-WS Hello World Service</display-name>
<display-name>JAX-WS Loan Approval Service</display-name>
<!-- Database Initialization Listener -->
<listener>
<listener-class>com.example.listener.DatabaseInitializationListener</listener-class>
</listener>
<!-- JAX-WS Context Listener -->
<listener>
<listener-class>com.sun.xml.ws.transport.http.servlet.WSServletContextListener</listener-class>
</listener>
<!-- Hello World Service Servlet -->
<servlet>
<servlet-name>HelloWorldService</servlet-name>
<servlet-class>com.sun.xml.ws.transport.http.servlet.WSServlet</servlet-class>
@ -22,4 +29,16 @@
<url-pattern>/hello</url-pattern>
</servlet-mapping>
<!-- Loan Approval Service Servlet -->
<servlet>
<servlet-name>LoanApprovalService</servlet-name>
<servlet-class>com.sun.xml.ws.transport.http.servlet.WSServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>LoanApprovalService</servlet-name>
<url-pattern>/loan</url-pattern>
</servlet-mapping>
</web-app>