From d4ddf209aa690df76c5f3592e729b82999ec56c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:09:39 +0000 Subject: [PATCH 1/4] Initial plan From d3ca1c4aca8406cd8fe5c261cd54a3b563554973 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:11:58 +0000 Subject: [PATCH 2/4] Add Copilot Code Review instruction files for Java and ReactJS Co-authored-by: yortch <4576246+yortch@users.noreply.github.com> --- .github/instructions/java.instructions.md | 81 ++++++++++ .github/instructions/reactjs.instructions.md | 162 +++++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 .github/instructions/java.instructions.md create mode 100644 .github/instructions/reactjs.instructions.md diff --git a/.github/instructions/java.instructions.md b/.github/instructions/java.instructions.md new file mode 100644 index 0000000..7a4258e --- /dev/null +++ b/.github/instructions/java.instructions.md @@ -0,0 +1,81 @@ +--- +description: 'Guidelines for building Java base applications' +applyTo: '**/*.java' +--- + +# Java Development + +## General Instructions + +- First, prompt the user if they want to integrate static analysis tools (SonarQube, PMD, Checkstyle) into their project setup. + - If yes, document a recommended static-analysis setup. + - Prefer SonarQube/SonarCloud (SonarLint in IDE + `sonar-scanner` in CI). + - Create a Sonar project key. + - Store the scanner token in CI secrets. + - Provide a sample CI job that runs the scanner. + - If the team declines Sonar, note this in the project README and continue. + - If Sonar is bound to the project: + - Use Sonar as the primary source of actionable issues. + - Reference Sonar rule keys in remediation guidance. + - If Sonar is unavailable: + - Perform up to 3 troubleshooting checks: + 1. Verify project binding and token. + 2. Ensure SonarScanner runs in CI. + 3. Confirm SonarLint is installed and configured. + - If still failing after 3 attempts: + - Enable SpotBugs, PMD, or Checkstyle as CI fallbacks. + - Open a short tracker issue documenting the blocker and next steps. +- If the user declines static analysis tools or wants to proceed without them, continue with implementing the Best practices, bug patterns and code smell prevention guidelines outlined below. +- Address code smells proactively during development rather than accumulating technical debt. +- Focus on readability, maintainability, and performance when refactoring identified issues. +- Use IDE / Code editor reported warnings and suggestions to catch common patterns early in development. + +## Best practices + +- **Records**: For classes primarily intended to store data (e.g., DTOs, immutable data structures), **Java Records should be used instead of traditional classes**. +- **Pattern Matching**: Utilize pattern matching for `instanceof` and `switch` expression to simplify conditional logic and type casting. +- **Type Inference**: Use `var` for local variable declarations to improve readability, but only when the type is explicitly clear from the right-hand side of the expression. +- **Immutability**: Favor immutable objects. Make classes and fields `final` where possible. Use collections from `List.of()`/`Map.of()` for fixed data. Use `Stream.toList()` to create immutable lists. +- **Streams and Lambdas**: Use the Streams API and lambda expressions for collection processing. Employ method references (e.g., `stream.map(Foo::toBar)`). +- **Null Handling**: Avoid returning or accepting `null`. Use `Optional` for possibly-absent values and `Objects` utility methods like `equals()` and `requireNonNull()`. + +### Naming Conventions + +- Follow Google's Java style guide: + - `UpperCamelCase` for class and interface names. + - `lowerCamelCase` for method and variable names. + - `UPPER_SNAKE_CASE` for constants. + - `lowercase` for package names. +- Use nouns for classes (`UserService`) and verbs for methods (`getUserById`). +- Avoid abbreviations and Hungarian notation. + +### Common Bug Patterns + +Below are concise, human-readable rules you can apply regardless of which static analysis tool you use. If you run Sonar/SonarLint, the IDE will show the matching rule and location — direct Sonar connections are preferred and should override this ruleset. + +- Resource management — Always close resources (files, sockets, streams). Use try-with-resources where possible so resources are closed automatically. +- Equality checks — Compare object equality with `.equals()` or `Objects.equals(...)` rather than `==` for non-primitives; this avoids reference-equality bugs. +- Redundant casts — Remove unnecessary casts; prefer correct generic typing and let the compiler infer types where possible. +- Reachable conditions — Avoid conditional expressions that are always true or false; they indicate bugs or dead code and should be corrected. + +For contributors who *do* use Sonar or SonarLint: the IDE/scan will show the specific rule key (for example, S2095 for resource leaks) and the affected file/line. Use that information to navigate to the exact location, then apply the recommended remediation. + +### Common Code Smells + +These patterns are phrased for humans; they map cleanly to checks in Sonar, SpotBugs, PMD, or Checkstyle but do not require those tools to be useful. + +- Parameter count — Keep method parameter lists short. If a method needs many params, consider grouping into a value object or using the builder pattern. +- Method size — Keep methods focused and small. Extract helper methods to improve readability and testability. +- Cognitive complexity — Reduce nested conditionals and heavy branching by extracting methods, using polymorphism, or applying the Strategy pattern. +- Duplicated literals — Extract repeated strings and numbers into named constants or enums to reduce errors and ease changes. +- Dead code — Remove unused variables and assignments. They confuse readers and can hide bugs. +- Magic numbers — Replace numeric literals with named constants that explain intent (e.g., MAX_RETRIES). + +If you run a static analyzer like Sonar or SonarLint — direct Sonar connections are preferred and should override this ruleset. Sonar rule keys are useful for automation and suppression, but they are not required in day-to-day developer guidance. + +## Build and Verification + +- After adding or modifying code, verify the project continues to build successfully. +- If the project uses Maven, run `mvn clean install`. +- If the project uses Gradle, run `./gradlew build` (or `gradlew.bat build` on Windows). +- Ensure all tests pass as part of the build. diff --git a/.github/instructions/reactjs.instructions.md b/.github/instructions/reactjs.instructions.md new file mode 100644 index 0000000..79bd275 --- /dev/null +++ b/.github/instructions/reactjs.instructions.md @@ -0,0 +1,162 @@ +--- +description: 'ReactJS development standards and best practices' +applyTo: '**/*.jsx, **/*.tsx, **/*.js, **/*.ts, **/*.css, **/*.scss' +--- + +# ReactJS Development Instructions + +Instructions for building high-quality ReactJS applications with modern patterns, hooks, and best practices following the official React documentation at https://react.dev. + +## Project Context +- Latest React version (React 19+) +- TypeScript for type safety (when applicable) +- Functional components with hooks as default +- Follow React's official style guide and best practices +- Use modern build tools (Vite, Create React App, or custom Webpack setup) +- Implement proper component composition and reusability patterns + +## Development Standards + +### Architecture +- Use functional components with hooks as the primary pattern +- Implement component composition over inheritance +- Organize components by feature or domain for scalability +- Separate presentational and container components clearly +- Use custom hooks for reusable stateful logic +- Implement proper component hierarchies with clear data flow + +### TypeScript Integration +- Use TypeScript interfaces for props, state, and component definitions +- Define proper types for event handlers and refs +- Implement generic components where appropriate +- Use strict mode in `tsconfig.json` for type safety +- Leverage React's built-in types (`React.FC`, `React.ComponentProps`, etc.) +- Create union types for component variants and states + +### Component Design +- Follow the single responsibility principle for components +- Use descriptive and consistent naming conventions +- Implement proper prop validation with TypeScript or PropTypes +- Design components to be testable and reusable +- Keep components small and focused on a single concern +- Use composition patterns (render props, children as functions) + +### State Management +- Use `useState` for local component state +- Implement `useReducer` for complex state logic +- Leverage `useContext` for sharing state across component trees +- Consider external state management (Redux Toolkit, Zustand) for complex applications +- Implement proper state normalization and data structures +- Use React Query or SWR for server state management + +### Hooks and Effects +- Use `useEffect` with proper dependency arrays to avoid infinite loops +- Implement cleanup functions in effects to prevent memory leaks +- Use `useMemo` and `useCallback` for performance optimization when needed +- Create custom hooks for reusable stateful logic +- Follow the rules of hooks (only call at the top level) +- Use `useRef` for accessing DOM elements and storing mutable values + +### Styling +- Use CSS Modules, Styled Components, or modern CSS-in-JS solutions +- Implement responsive design with mobile-first approach +- Follow BEM methodology or similar naming conventions for CSS classes +- Use CSS custom properties (variables) for theming +- Implement consistent spacing, typography, and color systems +- Ensure accessibility with proper ARIA attributes and semantic HTML + +### Performance Optimization +- Use `React.memo` for component memoization when appropriate +- Implement code splitting with `React.lazy` and `Suspense` +- Optimize bundle size with tree shaking and dynamic imports +- Use `useMemo` and `useCallback` judiciously to prevent unnecessary re-renders +- Implement virtual scrolling for large lists +- Profile components with React DevTools to identify performance bottlenecks + +### Data Fetching +- Use modern data fetching libraries (React Query, SWR, Apollo Client) +- Implement proper loading, error, and success states +- Handle race conditions and request cancellation +- Use optimistic updates for better user experience +- Implement proper caching strategies +- Handle offline scenarios and network errors gracefully + +### Error Handling +- Implement Error Boundaries for component-level error handling +- Use proper error states in data fetching +- Implement fallback UI for error scenarios +- Log errors appropriately for debugging +- Handle async errors in effects and event handlers +- Provide meaningful error messages to users + +### Forms and Validation +- Use controlled components for form inputs +- Implement proper form validation with libraries like Formik, React Hook Form +- Handle form submission and error states appropriately +- Implement accessibility features for forms (labels, ARIA attributes) +- Use debounced validation for better user experience +- Handle file uploads and complex form scenarios + +### Routing +- Use React Router for client-side routing +- Implement nested routes and route protection +- Handle route parameters and query strings properly +- Implement lazy loading for route-based code splitting +- Use proper navigation patterns and back button handling +- Implement breadcrumbs and navigation state management + +### Testing +- Write unit tests for components using React Testing Library +- Test component behavior, not implementation details +- Use Jest for test runner and assertion library +- Implement integration tests for complex component interactions +- Mock external dependencies and API calls appropriately +- Test accessibility features and keyboard navigation + +### Security +- Sanitize user inputs to prevent XSS attacks +- Validate and escape data before rendering +- Use HTTPS for all external API calls +- Implement proper authentication and authorization patterns +- Avoid storing sensitive data in localStorage or sessionStorage +- Use Content Security Policy (CSP) headers + +### Accessibility +- Use semantic HTML elements appropriately +- Implement proper ARIA attributes and roles +- Ensure keyboard navigation works for all interactive elements +- Provide alt text for images and descriptive text for icons +- Implement proper color contrast ratios +- Test with screen readers and accessibility tools + +## Implementation Process +1. Plan component architecture and data flow +2. Set up project structure with proper folder organization +3. Define TypeScript interfaces and types +4. Implement core components with proper styling +5. Add state management and data fetching logic +6. Implement routing and navigation +7. Add form handling and validation +8. Implement error handling and loading states +9. Add testing coverage for components and functionality +10. Optimize performance and bundle size +11. Ensure accessibility compliance +12. Add documentation and code comments + +## Additional Guidelines +- Follow React's naming conventions (PascalCase for components, camelCase for functions) +- Use meaningful commit messages and maintain clean git history +- Implement proper code splitting and lazy loading strategies +- Document complex components and custom hooks with JSDoc +- Use ESLint and Prettier for consistent code formatting +- Keep dependencies up to date and audit for security vulnerabilities +- Implement proper environment configuration for different deployment stages +- Use React Developer Tools for debugging and performance analysis + +## Common Patterns +- Higher-Order Components (HOCs) for cross-cutting concerns +- Render props pattern for component composition +- Compound components for related functionality +- Provider pattern for context-based state sharing +- Container/Presentational component separation +- Custom hooks for reusable logic extraction From 27983fed04d5a0dcc4896f0dbb8c570b5f1493bf Mon Sep 17 00:00:00 2001 From: Jorge Balderas Date: Thu, 26 Feb 2026 16:50:11 -0500 Subject: [PATCH 3/4] added wildcard import rule --- .github/instructions/java.instructions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/instructions/java.instructions.md b/.github/instructions/java.instructions.md index 7a4258e..9a90b22 100644 --- a/.github/instructions/java.instructions.md +++ b/.github/instructions/java.instructions.md @@ -32,6 +32,7 @@ applyTo: '**/*.java' ## Best practices +- **Imports**: **MUST never use Java wildcard imports** (e.g., `import java.util.*`). Always use explicit imports for each class (e.g., `import java.util.List`). This improves readability, avoids naming conflicts, and makes dependencies explicit. - **Records**: For classes primarily intended to store data (e.g., DTOs, immutable data structures), **Java Records should be used instead of traditional classes**. - **Pattern Matching**: Utilize pattern matching for `instanceof` and `switch` expression to simplify conditional logic and type casting. - **Type Inference**: Use `var` for local variable declarations to improve readability, but only when the type is explicitly clear from the right-hand side of the expression. From 577fbc6ff688fac768d3f450b186358d69931449 Mon Sep 17 00:00:00 2001 From: Jorge Balderas Date: Thu, 26 Feb 2026 17:01:28 -0500 Subject: [PATCH 4/4] feat: Implement credit card application feature with validation and error handling - Added GlobalExceptionHandler for centralized exception handling. - Created CreditCardApplicationController for handling application submissions and retrievals. - Developed CreditCardApplicationRequest and CreditCardApplicationResponse DTOs for data transfer. - Implemented CreditCardApplication entity with JPA annotations for persistence. - Created CreditCardApplicationRepository for database interactions. - Developed CreditCardApplicationService for business logic related to applications. - Added tests for CreditCardApplicationController and CreditCardApplicationRepository. - Implemented CardApplicationPage in the frontend for user interaction with the application process. --- .../config/GlobalExceptionHandler.java | 40 ++ .../CreditCardApplicationController.java | 55 ++ .../dto/CreditCardApplicationRequest.java | 63 +++ .../dto/CreditCardApplicationResponse.java | 27 + .../model/entity/CreditCardApplication.java | 78 +++ .../CreditCardApplicationRepository.java | 20 + .../service/CreditCardApplicationService.java | 84 +++ .../CreditCardApplicationControllerTest.java | 135 +++++ .../CreditCardApplicationRepositoryTest.java | 106 ++++ frontend/src/pages/CardApplicationPage.jsx | 518 ++++++++++++++++++ 10 files changed, 1126 insertions(+) create mode 100644 backend/src/main/java/com/threeriversbank/config/GlobalExceptionHandler.java create mode 100644 backend/src/main/java/com/threeriversbank/controller/CreditCardApplicationController.java create mode 100644 backend/src/main/java/com/threeriversbank/model/dto/CreditCardApplicationRequest.java create mode 100644 backend/src/main/java/com/threeriversbank/model/dto/CreditCardApplicationResponse.java create mode 100644 backend/src/main/java/com/threeriversbank/model/entity/CreditCardApplication.java create mode 100644 backend/src/main/java/com/threeriversbank/repository/CreditCardApplicationRepository.java create mode 100644 backend/src/main/java/com/threeriversbank/service/CreditCardApplicationService.java create mode 100644 backend/src/test/java/com/threeriversbank/controller/CreditCardApplicationControllerTest.java create mode 100644 backend/src/test/java/com/threeriversbank/repository/CreditCardApplicationRepositoryTest.java create mode 100644 frontend/src/pages/CardApplicationPage.jsx diff --git a/backend/src/main/java/com/threeriversbank/config/GlobalExceptionHandler.java b/backend/src/main/java/com/threeriversbank/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..02c0ba8 --- /dev/null +++ b/backend/src/main/java/com/threeriversbank/config/GlobalExceptionHandler.java @@ -0,0 +1,40 @@ +package com.threeriversbank.config; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.HashMap; +import java.util.Map; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationErrors(MethodArgumentNotValidException ex) { + Map fieldErrors = new HashMap<>(); + ex.getBindingResult().getFieldErrors().forEach(error -> + fieldErrors.put(error.getField(), error.getDefaultMessage())); + + Map response = new HashMap<>(); + response.put("error", "Validation failed"); + response.put("fieldErrors", fieldErrors); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleIllegalArgumentException(IllegalArgumentException ex) { + Map response = new HashMap<>(); + response.put("error", ex.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); + } + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity> handleRuntimeException(RuntimeException ex) { + Map response = new HashMap<>(); + response.put("error", ex.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } +} diff --git a/backend/src/main/java/com/threeriversbank/controller/CreditCardApplicationController.java b/backend/src/main/java/com/threeriversbank/controller/CreditCardApplicationController.java new file mode 100644 index 0000000..3829609 --- /dev/null +++ b/backend/src/main/java/com/threeriversbank/controller/CreditCardApplicationController.java @@ -0,0 +1,55 @@ +package com.threeriversbank.controller; + +import com.threeriversbank.model.dto.CreditCardApplicationRequest; +import com.threeriversbank.model.dto.CreditCardApplicationResponse; +import com.threeriversbank.service.CreditCardApplicationService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +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.List; + +@RestController +@RequestMapping("/api/applications") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "Credit Card Applications", description = "Apply for Three Rivers Bank Business Credit Cards") +public class CreditCardApplicationController { + + private final CreditCardApplicationService applicationService; + + @PostMapping + @Operation(summary = "Submit a credit card application", + description = "Submit a new business credit card application") + public ResponseEntity submitApplication( + @Valid @RequestBody CreditCardApplicationRequest request) { + log.info("POST /api/applications - Submitting application for card id: {}", request.getCardId()); + CreditCardApplicationResponse response = applicationService.submitApplication(request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + @GetMapping("/reference/{referenceNumber}") + @Operation(summary = "Get application by reference number", + description = "Look up an application status by its reference number") + public ResponseEntity getApplicationByReference( + @PathVariable String referenceNumber) { + log.info("GET /api/applications/reference/{}", referenceNumber); + CreditCardApplicationResponse response = applicationService.getApplicationByReference(referenceNumber); + return ResponseEntity.ok(response); + } + + @GetMapping + @Operation(summary = "Get applications by email", + description = "Look up all applications for a given email address") + public ResponseEntity> getApplicationsByEmail( + @RequestParam String email) { + log.info("GET /api/applications?email={}", email); + List responses = applicationService.getApplicationsByEmail(email); + return ResponseEntity.ok(responses); + } +} diff --git a/backend/src/main/java/com/threeriversbank/model/dto/CreditCardApplicationRequest.java b/backend/src/main/java/com/threeriversbank/model/dto/CreditCardApplicationRequest.java new file mode 100644 index 0000000..d825c23 --- /dev/null +++ b/backend/src/main/java/com/threeriversbank/model/dto/CreditCardApplicationRequest.java @@ -0,0 +1,63 @@ +package com.threeriversbank.model.dto; + +import jakarta.validation.constraints.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CreditCardApplicationRequest { + + @NotNull(message = "Card ID is required") + private Long cardId; + + @NotBlank(message = "First name is required") + @Size(max = 100) + private String firstName; + + @NotBlank(message = "Last name is required") + @Size(max = 100) + private String lastName; + + @NotBlank(message = "Email is required") + @Email(message = "Please provide a valid email") + private String email; + + @NotBlank(message = "Phone number is required") + @Pattern(regexp = "^\\(?\\d{3}\\)?[-.\\s]?\\d{3}[-.\\s]?\\d{4}$", + message = "Please provide a valid phone number") + private String phone; + + @NotBlank(message = "Address is required") + @Size(max = 200) + private String address; + + @NotBlank(message = "City is required") + @Size(max = 100) + private String city; + + @NotBlank(message = "State is required") + @Size(min = 2, max = 2, message = "State must be a 2-letter code") + private String state; + + @NotBlank(message = "ZIP code is required") + @Pattern(regexp = "^\\d{5}(-\\d{4})?$", message = "Please provide a valid ZIP code") + private String zipCode; + + @NotBlank(message = "Business name is required") + @Size(max = 200) + private String businessName; + + @NotNull(message = "Annual revenue is required") + @DecimalMin(value = "0.0", message = "Annual revenue must be positive") + private BigDecimal annualRevenue; + + @NotBlank(message = "Years in business is required") + private String yearsInBusiness; +} diff --git a/backend/src/main/java/com/threeriversbank/model/dto/CreditCardApplicationResponse.java b/backend/src/main/java/com/threeriversbank/model/dto/CreditCardApplicationResponse.java new file mode 100644 index 0000000..324c5c6 --- /dev/null +++ b/backend/src/main/java/com/threeriversbank/model/dto/CreditCardApplicationResponse.java @@ -0,0 +1,27 @@ +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 CreditCardApplicationResponse { + + private Long id; + private Long cardId; + private String cardName; + private String firstName; + private String lastName; + private String email; + private String phone; + private String businessName; + private String status; + private String referenceNumber; + private LocalDateTime submittedAt; +} diff --git a/backend/src/main/java/com/threeriversbank/model/entity/CreditCardApplication.java b/backend/src/main/java/com/threeriversbank/model/entity/CreditCardApplication.java new file mode 100644 index 0000000..ac92413 --- /dev/null +++ b/backend/src/main/java/com/threeriversbank/model/entity/CreditCardApplication.java @@ -0,0 +1,78 @@ +package com.threeriversbank.model.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Entity +@Table(name = "credit_card_application") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CreditCardApplication { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "card_id", nullable = false) + private CreditCard creditCard; + + @Column(nullable = false, length = 100) + private String firstName; + + @Column(nullable = false, length = 100) + private String lastName; + + @Column(nullable = false, length = 200) + private String email; + + @Column(nullable = false, length = 20) + private String phone; + + @Column(nullable = false, length = 200) + private String address; + + @Column(nullable = false, length = 100) + private String city; + + @Column(nullable = false, length = 2) + private String state; + + @Column(nullable = false, length = 10) + private String zipCode; + + @Column(nullable = false, length = 200) + private String businessName; + + @Column(precision = 15, scale = 2) + private BigDecimal annualRevenue; + + @Column(nullable = false, length = 50) + private String yearsInBusiness; + + @Column(nullable = false, length = 20) + private String status; + + @Column(length = 36) + private String referenceNumber; + + @Column(nullable = false) + private LocalDateTime submittedAt; + + @PrePersist + protected void onCreate() { + submittedAt = LocalDateTime.now(); + if (status == null) { + status = "PENDING"; + } + if (referenceNumber == null) { + referenceNumber = "TRB-" + System.currentTimeMillis(); + } + } +} diff --git a/backend/src/main/java/com/threeriversbank/repository/CreditCardApplicationRepository.java b/backend/src/main/java/com/threeriversbank/repository/CreditCardApplicationRepository.java new file mode 100644 index 0000000..63ae8fe --- /dev/null +++ b/backend/src/main/java/com/threeriversbank/repository/CreditCardApplicationRepository.java @@ -0,0 +1,20 @@ +package com.threeriversbank.repository; + +import com.threeriversbank.model.entity.CreditCardApplication; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface CreditCardApplicationRepository extends JpaRepository { + + List findByEmail(String email); + + Optional findByReferenceNumber(String referenceNumber); + + List findByCreditCardId(Long cardId); + + List findByStatus(String status); +} diff --git a/backend/src/main/java/com/threeriversbank/service/CreditCardApplicationService.java b/backend/src/main/java/com/threeriversbank/service/CreditCardApplicationService.java new file mode 100644 index 0000000..1fe064d --- /dev/null +++ b/backend/src/main/java/com/threeriversbank/service/CreditCardApplicationService.java @@ -0,0 +1,84 @@ +package com.threeriversbank.service; + +import com.threeriversbank.model.dto.CreditCardApplicationRequest; +import com.threeriversbank.model.dto.CreditCardApplicationResponse; +import com.threeriversbank.model.entity.CreditCard; +import com.threeriversbank.model.entity.CreditCardApplication; +import com.threeriversbank.repository.CreditCardApplicationRepository; +import com.threeriversbank.repository.CreditCardRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Slf4j +public class CreditCardApplicationService { + + private final CreditCardApplicationRepository applicationRepository; + private final CreditCardRepository creditCardRepository; + + @Transactional + public CreditCardApplicationResponse submitApplication(CreditCardApplicationRequest request) { + log.info("Submitting credit card application for card id: {} by {}", + request.getCardId(), request.getEmail()); + + CreditCard card = creditCardRepository.findById(request.getCardId()) + .orElseThrow(() -> new RuntimeException("Credit card not found with id: " + request.getCardId())); + + CreditCardApplication application = new CreditCardApplication(); + application.setCreditCard(card); + application.setFirstName(request.getFirstName()); + application.setLastName(request.getLastName()); + application.setEmail(request.getEmail()); + application.setPhone(request.getPhone()); + application.setAddress(request.getAddress()); + application.setCity(request.getCity()); + application.setState(request.getState()); + application.setZipCode(request.getZipCode()); + application.setBusinessName(request.getBusinessName()); + application.setAnnualRevenue(request.getAnnualRevenue()); + application.setYearsInBusiness(request.getYearsInBusiness()); + + CreditCardApplication saved = applicationRepository.save(application); + log.info("Application submitted successfully with reference: {}", saved.getReferenceNumber()); + + return convertToResponse(saved); + } + + @Transactional(readOnly = true) + public CreditCardApplicationResponse getApplicationByReference(String referenceNumber) { + log.info("Fetching application by reference: {}", referenceNumber); + CreditCardApplication application = applicationRepository.findByReferenceNumber(referenceNumber) + .orElseThrow(() -> new RuntimeException("Application not found with reference: " + referenceNumber)); + return convertToResponse(application); + } + + @Transactional(readOnly = true) + public List getApplicationsByEmail(String email) { + log.info("Fetching applications for email: {}", email); + return applicationRepository.findByEmail(email).stream() + .map(this::convertToResponse) + .collect(Collectors.toList()); + } + + private CreditCardApplicationResponse convertToResponse(CreditCardApplication application) { + return CreditCardApplicationResponse.builder() + .id(application.getId()) + .cardId(application.getCreditCard().getId()) + .cardName(application.getCreditCard().getName()) + .firstName(application.getFirstName()) + .lastName(application.getLastName()) + .email(application.getEmail()) + .phone(application.getPhone()) + .businessName(application.getBusinessName()) + .status(application.getStatus()) + .referenceNumber(application.getReferenceNumber()) + .submittedAt(application.getSubmittedAt()) + .build(); + } +} diff --git a/backend/src/test/java/com/threeriversbank/controller/CreditCardApplicationControllerTest.java b/backend/src/test/java/com/threeriversbank/controller/CreditCardApplicationControllerTest.java new file mode 100644 index 0000000..52b5a22 --- /dev/null +++ b/backend/src/test/java/com/threeriversbank/controller/CreditCardApplicationControllerTest.java @@ -0,0 +1,135 @@ +package com.threeriversbank.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.threeriversbank.model.dto.CreditCardApplicationRequest; +import com.threeriversbank.model.dto.CreditCardApplicationResponse; +import com.threeriversbank.service.CreditCardApplicationService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Collections; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; +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.result.MockMvcResultMatchers.*; + +@WebMvcTest(CreditCardApplicationController.class) +class CreditCardApplicationControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private CreditCardApplicationService applicationService; + + @Autowired + private ObjectMapper objectMapper; + + private CreditCardApplicationRequest buildValidRequest() { + return CreditCardApplicationRequest.builder() + .cardId(1L) + .firstName("John") + .lastName("Doe") + .email("john.doe@example.com") + .phone("412-555-1234") + .address("123 Main Street") + .city("Pittsburgh") + .state("PA") + .zipCode("15201") + .businessName("Doe Enterprises") + .annualRevenue(new BigDecimal("500000")) + .yearsInBusiness("3-5 years") + .build(); + } + + private CreditCardApplicationResponse buildResponse() { + return CreditCardApplicationResponse.builder() + .id(1L) + .cardId(1L) + .cardName("Business Cash Rewards") + .firstName("John") + .lastName("Doe") + .email("john.doe@example.com") + .phone("412-555-1234") + .businessName("Doe Enterprises") + .status("PENDING") + .referenceNumber("TRB-1234567890") + .submittedAt(LocalDateTime.now()) + .build(); + } + + @Test + void submitApplication_ShouldReturnCreated() throws Exception { + CreditCardApplicationRequest request = buildValidRequest(); + CreditCardApplicationResponse response = buildResponse(); + + when(applicationService.submitApplication(any(CreditCardApplicationRequest.class))) + .thenReturn(response); + + mockMvc.perform(post("/api/applications") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.referenceNumber").value("TRB-1234567890")) + .andExpect(jsonPath("$.status").value("PENDING")) + .andExpect(jsonPath("$.cardName").value("Business Cash Rewards")) + .andExpect(jsonPath("$.firstName").value("John")); + } + + @Test + void submitApplication_WithMissingFields_ShouldReturnBadRequest() throws Exception { + CreditCardApplicationRequest request = CreditCardApplicationRequest.builder() + .cardId(1L) + .build(); + + mockMvc.perform(post("/api/applications") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + void submitApplication_WithInvalidEmail_ShouldReturnBadRequest() throws Exception { + CreditCardApplicationRequest request = buildValidRequest(); + request.setEmail("not-an-email"); + + mockMvc.perform(post("/api/applications") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } + + @Test + void getApplicationByReference_ShouldReturnApplication() throws Exception { + CreditCardApplicationResponse response = buildResponse(); + when(applicationService.getApplicationByReference(anyString())).thenReturn(response); + + mockMvc.perform(get("/api/applications/reference/TRB-1234567890")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.referenceNumber").value("TRB-1234567890")) + .andExpect(jsonPath("$.firstName").value("John")); + } + + @Test + void getApplicationsByEmail_ShouldReturnList() throws Exception { + CreditCardApplicationResponse response = buildResponse(); + when(applicationService.getApplicationsByEmail(anyString())) + .thenReturn(Collections.singletonList(response)); + + mockMvc.perform(get("/api/applications") + .param("email", "john.doe@example.com")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$.length()").value(1)) + .andExpect(jsonPath("$[0].email").value("john.doe@example.com")); + } +} diff --git a/backend/src/test/java/com/threeriversbank/repository/CreditCardApplicationRepositoryTest.java b/backend/src/test/java/com/threeriversbank/repository/CreditCardApplicationRepositoryTest.java new file mode 100644 index 0000000..a5d63bf --- /dev/null +++ b/backend/src/test/java/com/threeriversbank/repository/CreditCardApplicationRepositoryTest.java @@ -0,0 +1,106 @@ +package com.threeriversbank.repository; + +import com.threeriversbank.model.entity.CreditCard; +import com.threeriversbank.model.entity.CreditCardApplication; +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.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +class CreditCardApplicationRepositoryTest { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private CreditCardApplicationRepository applicationRepository; + + private CreditCard testCard; + + @BeforeEach + void setUp() { + // The seed data creates cards with IDs 1-5, use the existing one + testCard = entityManager.find(CreditCard.class, 1L); + } + + @Test + void saveApplication_ShouldPersistAndGenerateId() { + CreditCardApplication application = createTestApplication(); + CreditCardApplication saved = applicationRepository.save(application); + + assertThat(saved.getId()).isNotNull(); + assertThat(saved.getFirstName()).isEqualTo("Jane"); + assertThat(saved.getStatus()).isEqualTo("PENDING"); + assertThat(saved.getReferenceNumber()).startsWith("TRB-"); + assertThat(saved.getSubmittedAt()).isNotNull(); + } + + @Test + void findByEmail_ShouldReturnMatchingApplications() { + CreditCardApplication app = createTestApplication(); + applicationRepository.save(app); + + List results = applicationRepository.findByEmail("jane.doe@example.com"); + assertThat(results).hasSize(1); + assertThat(results.get(0).getEmail()).isEqualTo("jane.doe@example.com"); + } + + @Test + void findByReferenceNumber_ShouldReturnApplication() { + CreditCardApplication app = createTestApplication(); + app.setReferenceNumber("TRB-TEST123"); + CreditCardApplication saved = applicationRepository.save(app); + + Optional result = applicationRepository.findByReferenceNumber("TRB-TEST123"); + assertThat(result).isPresent(); + assertThat(result.get().getId()).isEqualTo(saved.getId()); + } + + @Test + void findByCreditCardId_ShouldReturnApplicationsForCard() { + CreditCardApplication app = createTestApplication(); + applicationRepository.save(app); + + List results = applicationRepository.findByCreditCardId(1L); + assertThat(results).isNotEmpty(); + } + + @Test + void findByStatus_ShouldReturnApplicationsWithStatus() { + CreditCardApplication app = createTestApplication(); + applicationRepository.save(app); + + List results = applicationRepository.findByStatus("PENDING"); + assertThat(results).isNotEmpty(); + assertThat(results).allMatch(a -> "PENDING".equals(a.getStatus())); + } + + private CreditCardApplication createTestApplication() { + CreditCardApplication app = new CreditCardApplication(); + app.setCreditCard(testCard); + app.setFirstName("Jane"); + app.setLastName("Doe"); + app.setEmail("jane.doe@example.com"); + app.setPhone("412-555-9876"); + app.setAddress("456 Oak Avenue"); + app.setCity("Pittsburgh"); + app.setState("PA"); + app.setZipCode("15213"); + app.setBusinessName("Doe Corp"); + app.setAnnualRevenue(new BigDecimal("250000")); + app.setYearsInBusiness("1-2 years"); + app.setStatus("PENDING"); + app.setReferenceNumber("TRB-" + System.currentTimeMillis()); + app.setSubmittedAt(LocalDateTime.now()); + return app; + } +} diff --git a/frontend/src/pages/CardApplicationPage.jsx b/frontend/src/pages/CardApplicationPage.jsx new file mode 100644 index 0000000..63566a1 --- /dev/null +++ b/frontend/src/pages/CardApplicationPage.jsx @@ -0,0 +1,518 @@ +import React, { useState } from 'react'; +import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; +import { useQuery, useMutation } from '@tanstack/react-query'; +import { + Container, + Typography, + Box, + Grid, + Card, + CardContent, + Button, + TextField, + MenuItem, + CircularProgress, + Alert, + Stepper, + Step, + StepLabel, + Divider, + InputAdornment, + Chip, +} from '@mui/material'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; +import SendIcon from '@mui/icons-material/Send'; +import { creditCardService, applicationService } from '../services/api'; + +const US_STATES = [ + 'AL','AK','AZ','AR','CA','CO','CT','DE','FL','GA', + 'HI','ID','IL','IN','IA','KS','KY','LA','ME','MD', + 'MA','MI','MN','MS','MO','MT','NE','NV','NH','NJ', + 'NM','NY','NC','ND','OH','OK','OR','PA','RI','SC', + 'SD','TN','TX','UT','VT','VA','WA','WV','WI','WY', +]; + +const YEARS_IN_BUSINESS_OPTIONS = [ + 'Less than 1 year', + '1-2 years', + '3-5 years', + '6-10 years', + 'More than 10 years', +]; + +const steps = ['Card Selection', 'Business Information', 'Personal Information', 'Review & Submit']; + +const CardApplicationPage = () => { + const { id: cardIdParam } = useParams(); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + + const cardId = cardIdParam || searchParams.get('cardId'); + + const [activeStep, setActiveStep] = useState(cardId ? 1 : 0); + const [selectedCardId, setSelectedCardId] = useState(cardId || ''); + const [formData, setFormData] = useState({ + firstName: '', + lastName: '', + email: '', + phone: '', + address: '', + city: '', + state: '', + zipCode: '', + businessName: '', + annualRevenue: '', + yearsInBusiness: '', + }); + const [fieldErrors, setFieldErrors] = useState({}); + const [submissionResult, setSubmissionResult] = useState(null); + + const { data: cards, isLoading: cardsLoading } = useQuery({ + queryKey: ['creditCards'], + queryFn: () => creditCardService.getAllCards(), + }); + + const { data: selectedCard } = useQuery({ + queryKey: ['creditCard', selectedCardId], + queryFn: () => creditCardService.getCardById(selectedCardId), + enabled: !!selectedCardId, + }); + + const submitMutation = useMutation({ + mutationFn: (data) => applicationService.submitApplication(data), + onSuccess: (response) => { + setSubmissionResult(response); + setActiveStep(4); + }, + onError: (error) => { + if (error.response?.data?.fieldErrors) { + setFieldErrors(error.response.data.fieldErrors); + } + }, + }); + + const handleInputChange = (e) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + if (fieldErrors[name]) { + setFieldErrors((prev) => ({ ...prev, [name]: '' })); + } + }; + + const validateStep = (step) => { + const errors = {}; + if (step === 1) { + if (!formData.businessName) errors.businessName = 'Business name is required'; + if (!formData.annualRevenue) errors.annualRevenue = 'Annual revenue is required'; + if (formData.annualRevenue && isNaN(formData.annualRevenue)) errors.annualRevenue = 'Must be a number'; + if (!formData.yearsInBusiness) errors.yearsInBusiness = 'Years in business is required'; + } + if (step === 2) { + if (!formData.firstName) errors.firstName = 'First name is required'; + if (!formData.lastName) errors.lastName = 'Last name is required'; + if (!formData.email) errors.email = 'Email is required'; + else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) errors.email = 'Invalid email'; + if (!formData.phone) errors.phone = 'Phone number is required'; + else if (!/^\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/.test(formData.phone)) errors.phone = 'Invalid phone number'; + if (!formData.address) errors.address = 'Address is required'; + if (!formData.city) errors.city = 'City is required'; + if (!formData.state) errors.state = 'State is required'; + if (!formData.zipCode) errors.zipCode = 'ZIP code is required'; + else if (!/^\d{5}(-\d{4})?$/.test(formData.zipCode)) errors.zipCode = 'Invalid ZIP code'; + } + setFieldErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleNext = () => { + if (activeStep === 0) { + if (!selectedCardId) return; + setActiveStep(1); + } else if (validateStep(activeStep)) { + setActiveStep((prev) => prev + 1); + } + }; + + const handleBack = () => { + setActiveStep((prev) => prev - 1); + setFieldErrors({}); + }; + + const handleSubmit = () => { + const payload = { + cardId: Number(selectedCardId), + ...formData, + annualRevenue: parseFloat(formData.annualRevenue), + }; + submitMutation.mutate(payload); + }; + + // Success screen + if (submissionResult) { + return ( + + + + + Application Submitted! + + + Thank you, {submissionResult.firstName}. Your application for the{' '} + {submissionResult.cardName} has been received. + + + + + Reference Number + + + {submissionResult.referenceNumber} + + + Status: + + + + + A confirmation email will be sent to {submissionResult.email}. + You can use your reference number to check your application status. + + + + + + + + ); + } + + return ( + + + + + Apply for a Business Credit Card + + + Complete the application form below. All fields are required. + + + + {steps.map((label) => ( + + {label} + + ))} + + + {submitMutation.isError && !submitMutation.error?.response?.data?.fieldErrors && ( + + An error occurred while submitting your application. Please try again. + + )} + + {/* Step 0: Card Selection */} + {activeStep === 0 && ( + + + Select a Credit Card + + {cardsLoading ? ( + + + + ) : ( + + {cards?.map((card) => ( + + setSelectedCardId(card.id)} + > + + + {card.name} + + + + Annual Fee: ${card.annualFee} | Rewards: {card.rewardsRate > 0 ? `${card.rewardsRate}%` : 'N/A'} + + + + + ))} + + )} + + )} + + {/* Step 1: Business Information */} + {activeStep === 1 && ( + + + Business Information + + {selectedCard && ( + + Applying for: {selectedCard.name} ({selectedCard.cardType}) + + )} + + + + + + $, + }} + /> + + + + {YEARS_IN_BUSINESS_OPTIONS.map((option) => ( + {option} + ))} + + + + + )} + + {/* Step 2: Personal Information */} + {activeStep === 2 && ( + + + Personal Information + + + + + + + + + + + + + + + + + + + + + + + {US_STATES.map((st) => ( + {st} + ))} + + + + + + + + )} + + {/* Step 3: Review & Submit */} + {activeStep === 3 && ( + + + Review Your Application + + {selectedCard && ( + + + + {selectedCard.name} + + + {selectedCard.cardType} | Annual Fee: ${selectedCard.annualFee} | Rewards: {selectedCard.rewardsRate > 0 ? `${selectedCard.rewardsRate}%` : 'N/A'} + + + + )} + + + + Business Information + + Business Name: {formData.businessName} + Annual Revenue: ${Number(formData.annualRevenue).toLocaleString()} + Years in Business: {formData.yearsInBusiness} + + + Personal Information + + Name: {formData.firstName} {formData.lastName} + Email: {formData.email} + Phone: {formData.phone} + Address: {formData.address} + {formData.city}, {formData.state} {formData.zipCode} + + + + + By submitting this application, you agree to allow Three Rivers Bank to review your + business credit history. This is a demo application — no real credit check will be performed. + + + )} + + {/* Navigation Buttons */} + + + + {activeStep < 3 ? ( + + ) : ( + + )} + + + + ); +}; + +export default CardApplicationPage;