Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.threeriversbank.controller;

import com.threeriversbank.model.dto.ApplicationRequestDto;
import com.threeriversbank.model.dto.ApplicationResponseDto;
import com.threeriversbank.service.ApplicationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/api/applications")
@RequiredArgsConstructor
@Slf4j
@Tag(name = "Applications", description = "Three Rivers Bank Credit Card Application API")
public class ApplicationController {

private final ApplicationService applicationService;

@PostMapping
@Operation(summary = "Submit credit card application", description = "Submits a new business credit card application")
public ResponseEntity<?> submitApplication(
@Valid @RequestBody ApplicationRequestDto request,
HttpServletRequest httpRequest) {
log.info("POST /api/applications - card ID: {}", request.getCreditCardId());
String ipAddress = getClientIp(httpRequest);
try {
ApplicationResponseDto response = applicationService.submitApplication(request, ipAddress);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
} catch (IllegalStateException e) {
log.warn("Rate limit or business rule violation: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.body(Map.of("error", e.getMessage()));
} catch (IllegalArgumentException e) {
log.warn("Validation error: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of("error", e.getMessage()));
}
}
Comment on lines +27 to +46

private String getClientIp(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
Comment on lines +49 to +52
return request.getRemoteAddr();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package com.threeriversbank.model.dto;

import jakarta.validation.constraints.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApplicationRequestDto {

@NotNull(message = "Credit card ID is required")
private Long creditCardId;

// Business Information
@NotBlank(message = "Business legal name is required")
@Size(max = 200, message = "Business legal name must not exceed 200 characters")
private String businessLegalName;

@Size(max = 200, message = "DBA name must not exceed 200 characters")
private String dbaName;

@NotBlank(message = "Business structure is required")
private String businessStructure;

@NotBlank(message = "Tax ID is required")
@Pattern(regexp = "^\\d{9}$", message = "Tax ID must be 9 digits")
private String taxId;

@NotBlank(message = "Industry is required")
private String industry;

@NotNull(message = "Years in business is required")
@Min(value = 0, message = "Years in business must be non-negative")
private Integer yearsInBusiness;

@NotNull(message = "Number of employees is required")
@Min(value = 1, message = "Number of employees must be at least 1")
private Integer numberOfEmployees;

@NotBlank(message = "Annual revenue is required")
private String annualRevenue;

@NotBlank(message = "Business street address is required")
private String businessStreet;

@NotBlank(message = "Business city is required")
private String businessCity;

@NotBlank(message = "Business state is required")
@Size(min = 2, max = 2, message = "Business state must be 2 characters")
private String businessState;

@NotBlank(message = "Business ZIP is required")
@Pattern(regexp = "^\\d{5}(-\\d{4})?$", message = "Business ZIP must be a valid ZIP code")
private String businessZip;

@NotBlank(message = "Business phone is required")
@Pattern(regexp = "^[\\d\\-\\+\\(\\)\\s]{7,20}$", message = "Business phone must be a valid phone number")
private String businessPhone;

@Pattern(regexp = "^$|^https?://[\\w.-]+(?:\\.[\\w.-]+)+[/\\w.-]*$", message = "Business website must be a valid URL")
private String businessWebsite;

// Personal Information
@NotBlank(message = "First name is required")
@Size(max = 100, message = "First name must not exceed 100 characters")
private String ownerFirstName;

@NotBlank(message = "Last name is required")
@Size(max = 100, message = "Last name must not exceed 100 characters")
private String ownerLastName;

@NotBlank(message = "Date of birth is required")
private String dateOfBirth; // ISO format: YYYY-MM-DD, validated in service

@NotBlank(message = "SSN is required")
@Pattern(regexp = "^\\d{9}$", message = "SSN must be 9 digits")
private String ssn;

@NotBlank(message = "Email is required")
@Email(message = "Email must be a valid email address")
private String ownerEmail;

@NotBlank(message = "Home street address is required")
private String ownerStreet;

@NotBlank(message = "Home city is required")
private String ownerCity;

@NotBlank(message = "Home state is required")
@Size(min = 2, max = 2, message = "Home state must be 2 characters")
private String ownerState;

@NotBlank(message = "Home ZIP is required")
@Pattern(regexp = "^\\d{5}(-\\d{4})?$", message = "Home ZIP must be a valid ZIP code")
private String ownerZip;

@NotBlank(message = "Mobile phone is required")
@Pattern(regexp = "^[\\d\\-\\+\\(\\)\\s]{7,20}$", message = "Mobile phone must be a valid phone number")
private String ownerPhone;

@NotNull(message = "Ownership percentage is required")
@Min(value = 1, message = "Ownership percentage must be at least 1")
@Max(value = 100, message = "Ownership percentage must not exceed 100")
private Integer ownershipPercentage;

@NotBlank(message = "Title/Position is required")
@Size(max = 100, message = "Title must not exceed 100 characters")
private String ownerTitle;

@NotNull(message = "Annual personal income is required")
@DecimalMin(value = "0.00", message = "Annual income must be non-negative")
private BigDecimal annualPersonalIncome;

// Card Preferences
@NotBlank(message = "Requested credit limit is required")
private String requestedCreditLimit;

@Min(value = 0, message = "Number of employee cards must be non-negative")
@Max(value = 50, message = "Number of employee cards must not exceed 50")
private Integer numberOfEmployeeCards;

// Terms
@NotNull(message = "Agreement to terms is required")
@AssertTrue(message = "You must agree to the terms and conditions")
private Boolean agreedToTerms;

@NotNull(message = "Consent to credit check is required")
@AssertTrue(message = "You must consent to a credit check")
private Boolean consentToCreditCheck;

@NotBlank(message = "Electronic signature is required")
@Size(max = 200, message = "Electronic signature must not exceed 200 characters")
private String electronicSignature;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.threeriversbank.model.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApplicationResponseDto {

private String applicationId;
private Long id;
private String status;
private LocalDateTime submittedAt;
private String cardName;
private String ownerFirstName;
private String ownerLastName;
private String ownerEmail;
private String message;
private String expectedDecisionTimeline;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package com.threeriversbank.model.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;

@Entity
@Table(name = "business_credit_card_application")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BusinessCreditCardApplication {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true, length = 50)
private String applicationId;

// Card reference
@Column(nullable = false)
private Long creditCardId;

// Business Information
@Column(nullable = false, length = 200)
private String businessLegalName;

@Column(length = 200)
private String dbaName;

@Column(nullable = false, length = 50)
private String businessStructure;

@Column(nullable = false, length = 100)
private String taxId; // stored encrypted

@Column(nullable = false, length = 100)
private String industry;

@Column(nullable = false)
private Integer yearsInBusiness;

@Column(nullable = false)
private Integer numberOfEmployees;

@Column(nullable = false, length = 50)
private String annualRevenue;

@Column(nullable = false, length = 200)
private String businessStreet;

@Column(nullable = false, length = 100)
private String businessCity;

@Column(nullable = false, length = 2)
private String businessState;

@Column(nullable = false, length = 10)
private String businessZip;

@Column(nullable = false, length = 20)
private String businessPhone;

@Column(length = 255)
private String businessWebsite;

// Personal Information
@Column(nullable = false, length = 100)
private String ownerFirstName;

@Column(nullable = false, length = 100)
private String ownerLastName;

@Column(nullable = false)
private LocalDate dateOfBirth;

@Column(nullable = false, length = 100)
private String ssn; // stored encrypted

@Column(nullable = false, length = 255)
private String ownerEmail;

@Column(nullable = false, length = 200)
private String ownerStreet;

@Column(nullable = false, length = 100)
private String ownerCity;

@Column(nullable = false, length = 2)
private String ownerState;

@Column(nullable = false, length = 10)
private String ownerZip;

@Column(nullable = false, length = 20)
private String ownerPhone;

@Column(nullable = false)
private Integer ownershipPercentage;

@Column(nullable = false, length = 100)
private String ownerTitle;

@Column(nullable = false, precision = 15, scale = 2)
private BigDecimal annualPersonalIncome;

// Card Preferences
@Column(nullable = false, length = 20)
private String requestedCreditLimit;

@Column
private Integer numberOfEmployeeCards;

// Terms
@Column(nullable = false)
private Boolean agreedToTerms;

@Column(nullable = false)
private Boolean consentToCreditCheck;

@Column(nullable = false, length = 200)
private String electronicSignature;

// Status
@Column(nullable = false, length = 20)
private String status; // Pending, Under Review, Approved, Denied

@Column(nullable = false)
private LocalDateTime submittedAt;

@Column(nullable = false, length = 50)
private String ipAddress;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.threeriversbank.repository;

import com.threeriversbank.model.entity.BusinessCreditCardApplication;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

@Repository
public interface ApplicationRepository extends JpaRepository<BusinessCreditCardApplication, Long> {

Optional<BusinessCreditCardApplication> findByApplicationId(String applicationId);

@Query("SELECT COUNT(a) FROM BusinessCreditCardApplication a WHERE a.ipAddress = :ipAddress AND a.submittedAt >= :since")
long countByIpAddressSince(String ipAddress, LocalDateTime since);
Comment on lines +17 to +18

List<BusinessCreditCardApplication> findByCreditCardId(Long creditCardId);
}
Loading
Loading