This document provides comprehensive guidance for building the Spring Boot backend to work with this React frontend.
- Database Schema
- API Endpoints
- Authentication & Authorization
- Request/Response Formats
- Role-Based Access Control
- Error Handling
- Spring Boot Implementation Guide
-- Users table for authentication
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP NULL,
INDEX idx_users_email (email)
);
-- User roles table (separate for security)
CREATE TABLE user_roles (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
role ENUM('ADMINISTRATOR', 'BROKER_MANAGER', 'BROKER') NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE KEY unique_user_role (user_id, role)
);
-- Clients (with CNP - Romanian Personal Numeric Code)
CREATE TABLE clients (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
broker_id BIGINT NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
cnp CHAR(13) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL,
phone VARCHAR(20),
address TEXT,
date_of_birth DATE,
total_policies INT DEFAULT 0,
active_policies INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (broker_id) REFERENCES brokers(id),
INDEX idx_clients_email (email),
INDEX idx_clients_cnp (cnp),
INDEX idx_clients_broker (broker_id),
INDEX idx_clients_name (last_name, first_name)
);
-- Insurers
CREATE TABLE insurers (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
code VARCHAR(50) UNIQUE NOT NULL,
contact_email VARCHAR(255),
contact_phone VARCHAR(20),
address TEXT,
active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_insurers_code (code)
);
-- Insurance Products
CREATE TABLE insurance_products (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
code VARCHAR(50) UNIQUE NOT NULL,
description TEXT,
category ENUM('LIFE', 'HEALTH', 'AUTO', 'HOME', 'TRAVEL', 'BUSINESS') NOT NULL,
insurer_id BIGINT NOT NULL,
base_rate DECIMAL(8, 6) NOT NULL,
active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (insurer_id) REFERENCES insurers(id),
INDEX idx_products_category (category),
INDEX idx_products_insurer (insurer_id)
);
-- Product Custom Fields (for dynamic premium calculation)
CREATE TABLE product_custom_fields (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
product_id BIGINT NOT NULL,
name VARCHAR(100) NOT NULL,
label VARCHAR(255) NOT NULL,
type ENUM('text', 'number', 'select', 'date', 'checkbox') NOT NULL,
required BOOLEAN DEFAULT FALSE,
options JSON NULL, -- For select type: ["Option1", "Option2"]
placeholder VARCHAR(255) NULL,
factor_multiplier DECIMAL(5, 2) NULL, -- Premium factor multiplier
factor_condition JSON NULL, -- Condition for applying factor: {"operator": "eq", "value": "wood"}
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (product_id) REFERENCES insurance_products(id) ON DELETE CASCADE,
INDEX idx_custom_fields_product (product_id)
);
-- Brokers (includes Administrators as brokers with elevated privileges)
CREATE TABLE brokers (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNIQUE, -- Links to users table for authentication
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
phone VARCHAR(20),
license_number VARCHAR(100) UNIQUE NOT NULL,
commission_rate DECIMAL(5, 4) NOT NULL, -- e.g., 0.0500 for 5%
hire_date DATE NOT NULL,
role ENUM('BROKER', 'BROKER_MANAGER', 'ADMINISTRATOR') NOT NULL DEFAULT 'BROKER',
active BOOLEAN DEFAULT TRUE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_brokers_license (license_number),
INDEX idx_brokers_role (role)
);
-- Offers (pre-policy stage)
CREATE TABLE offers (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
offer_number VARCHAR(50) UNIQUE NOT NULL,
client_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
broker_id BIGINT NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
premium DECIMAL(10, 2) NOT NULL,
sum_insured DECIMAL(15, 2) NOT NULL,
status ENUM('DRAFT', 'PENDING', 'ACCEPTED', 'EXPIRED', 'DECLINED') NOT NULL DEFAULT 'PENDING',
expires_at DATE NOT NULL,
gdpr_consent BOOLEAN DEFAULT FALSE,
gdpr_consent_date DATE NULL,
custom_field_values JSON NULL, -- Stores custom field responses
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (client_id) REFERENCES clients(id),
FOREIGN KEY (product_id) REFERENCES insurance_products(id),
FOREIGN KEY (broker_id) REFERENCES brokers(id),
INDEX idx_offers_number (offer_number),
INDEX idx_offers_status (status),
INDEX idx_offers_broker (broker_id),
INDEX idx_offers_expires (expires_at)
);
-- Insurance Policies (created from offers after checkout)
CREATE TABLE insurance_policies (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
policy_number VARCHAR(50) UNIQUE NOT NULL,
offer_id BIGINT NULL, -- Link to original offer
client_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
broker_id BIGINT NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
premium DECIMAL(10, 2) NOT NULL,
sum_insured DECIMAL(15, 2) NOT NULL,
status ENUM('ACTIVE', 'EXPIRED', 'CANCELLED', 'PENDING', 'SUSPENDED', 'AWAITING_PAYMENT') NOT NULL DEFAULT 'PENDING',
gdpr_consent BOOLEAN DEFAULT FALSE,
gdpr_consent_date DATE NULL,
payment_method ENUM('CASH', 'POS', 'CARD_ONLINE', 'BANK_TRANSFER', 'BROKER_PAYMENT') NULL,
payment_status ENUM('PENDING', 'VALIDATED', 'REJECTED') DEFAULT 'PENDING',
proof_of_payment VARCHAR(255) NULL, -- File path to uploaded document
custom_field_values JSON NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (offer_id) REFERENCES offers(id),
FOREIGN KEY (client_id) REFERENCES clients(id),
FOREIGN KEY (product_id) REFERENCES insurance_products(id),
FOREIGN KEY (broker_id) REFERENCES brokers(id),
INDEX idx_policies_number (policy_number),
INDEX idx_policies_status (status),
INDEX idx_policies_broker (broker_id),
INDEX idx_policies_dates (start_date, end_date)
);
-- Commissions
CREATE TABLE commissions (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
policy_id BIGINT NOT NULL,
broker_id BIGINT NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
rate DECIMAL(5, 4) NOT NULL,
status ENUM('PENDING', 'PAID', 'CANCELLED') NOT NULL DEFAULT 'PENDING',
payment_date DATE NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (policy_id) REFERENCES insurance_policies(id),
FOREIGN KEY (broker_id) REFERENCES brokers(id),
INDEX idx_commissions_broker (broker_id),
INDEX idx_commissions_status (status)
);
-- Policy Renewals
CREATE TABLE renewals (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
original_policy_id BIGINT NOT NULL,
new_policy_id BIGINT NULL, -- NULL until renewal is completed
broker_id BIGINT NOT NULL,
renewal_date DATE NOT NULL,
previous_premium DECIMAL(10, 2) NOT NULL,
new_premium DECIMAL(10, 2) NOT NULL,
status ENUM('PENDING', 'COMPLETED', 'DECLINED') NOT NULL DEFAULT 'PENDING',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (original_policy_id) REFERENCES insurance_policies(id),
FOREIGN KEY (new_policy_id) REFERENCES insurance_policies(id),
FOREIGN KEY (broker_id) REFERENCES brokers(id),
INDEX idx_renewals_status (status),
INDEX idx_renewals_date (renewal_date)
);
-- Activity Logs (audit trail)
CREATE TABLE activity_logs (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
entity_type ENUM('CLIENT', 'POLICY', 'OFFER', 'COMMISSION', 'RENEWAL') NOT NULL,
entity_id BIGINT NOT NULL,
activity_type VARCHAR(100) NOT NULL,
description TEXT NOT NULL,
performed_by BIGINT NOT NULL,
performed_by_name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (performed_by) REFERENCES users(id),
INDEX idx_activity_entity (entity_type, entity_id),
INDEX idx_activity_user (performed_by),
INDEX idx_activity_date (created_at)
);
-- Default admin user (password: admin123 - hash with BCrypt)
INSERT INTO users (email, password_hash, first_name, last_name, active)
VALUES ('admin@insurebroker.com', '$2a$10$YourBCryptHashHere', 'Admin', 'User', TRUE);
INSERT INTO user_roles (user_id, role)
VALUES (1, 'ADMINISTRATOR');
-- Create corresponding broker entry for admin
INSERT INTO brokers (user_id, first_name, last_name, email, license_number, commission_rate, hire_date, role, active)
VALUES (1, 'Admin', 'User', 'admin@insurebroker.com', 'LIC-ADMIN-001', 0.0500, CURDATE(), 'ADMINISTRATOR', TRUE);http://localhost:8080/api
| Method | Endpoint | Description | Access |
|---|---|---|---|
| POST | /auth/login |
Login and get JWT token | Public |
| POST | /auth/logout |
Invalidate token | Authenticated |
| GET | /auth/me |
Get current user info | Authenticated |
| POST | /auth/refresh |
Refresh JWT token | Authenticated |
| Method | Endpoint | Description | Access |
|---|---|---|---|
| GET | /clients |
List all clients | BROKER, BROKER_MANAGER, ADMINISTRATOR |
| GET | /clients/{id} |
Get client by ID | BROKER, BROKER_MANAGER, ADMINISTRATOR |
| POST | /clients |
Create new client | BROKER, BROKER_MANAGER, ADMINISTRATOR |
| PUT | /clients/{id} |
Update client | BROKER, BROKER_MANAGER, ADMINISTRATOR |
| DELETE | /clients/{id} |
Delete client | BROKER_MANAGER, ADMINISTRATOR only |
IMPORTANT: Policies cannot be deleted. Use status change to CANCELLED instead (soft delete).
| Method | Endpoint | Description | Access |
|---|---|---|---|
| GET | /policies |
List policies | ALL (filtered by broker for BROKER role) |
| GET | /policies/{id} |
Get policy by ID | ALL (must be own policy for BROKER) |
| POST | /policies |
Create new policy from offer | BROKER, BROKER_MANAGER, ADMINISTRATOR |
| PUT | /policies/{id} |
Update policy | BROKER_MANAGER, ADMINISTRATOR |
| PATCH | /policies/{id}/cancel |
Cancel policy (soft delete) | BROKER_MANAGER, ADMINISTRATOR only |
| PATCH | /policies/{id}/suspend |
Suspend policy | BROKER_MANAGER, ADMINISTRATOR only |
| PATCH | /policies/{id}/validate-payment |
Validate payment | BROKER_MANAGER, ADMINISTRATOR |
| Method | Endpoint | Description | Access |
|---|---|---|---|
| GET | /offers |
List offers | ALL (filtered by broker for BROKER role) |
| GET | /offers/{id} |
Get offer by ID | ALL |
| POST | /offers |
Create new offer | BROKER, BROKER_MANAGER, ADMINISTRATOR |
| PUT | /offers/{id} |
Update offer | BROKER, BROKER_MANAGER, ADMINISTRATOR |
| DELETE | /offers/{id} |
Delete offer | BROKER_MANAGER, ADMINISTRATOR only |
| POST | /offers/{id}/checkout |
Convert offer to policy | BROKER, BROKER_MANAGER, ADMINISTRATOR |
| PATCH | /offers/{id}/reject |
Reject offer | BROKER_MANAGER, ADMINISTRATOR |
| -------- | ---------- | ------------- | -------- |
| GET | /offers |
List offers | ALL (filtered by broker for BROKER role) |
| GET | /offers/{id} |
Get offer by ID | ALL |
| POST | /offers |
Create new offer | BROKER, BROKER_MANAGER, ADMINISTRATOR |
| PUT | /offers/{id} |
Update offer | BROKER, BROKER_MANAGER, ADMINISTRATOR |
| POST | /offers/{id}/checkout |
Convert offer to policy | BROKER, BROKER_MANAGER, ADMINISTRATOR |
| Method | Endpoint | Description | Access |
|---|---|---|---|
| GET | /products |
List all products | ALL |
| GET | /products/{id} |
Get product by ID | ALL |
| POST | /products |
Create new product | ADMINISTRATOR |
| PUT | /products/{id} |
Update product | ADMINISTRATOR |
| DELETE | /products/{id} |
Delete product | ADMINISTRATOR |
| GET | /products/{id}/custom-fields |
Get product custom fields | ALL |
| POST | /products/{id}/custom-fields |
Add custom field | ADMINISTRATOR |
| PUT | /products/{id}/custom-fields/{fieldId} |
Update custom field | ADMINISTRATOR |
| DELETE | /products/{id}/custom-fields/{fieldId} |
Delete custom field | ADMINISTRATOR |
| Method | Endpoint | Description | Access |
|---|---|---|---|
| POST | /premium/calculate |
Calculate premium for a product | BROKER, BROKER_MANAGER, ADMINISTRATOR |
| Method | Endpoint | Description | Access |
|---|---|---|---|
| GET | /insurers |
List all insurers | ALL |
| GET | /insurers/{id} |
Get insurer by ID | ALL |
| POST | /insurers |
Create new insurer | ADMINISTRATOR |
| PUT | /insurers/{id} |
Update insurer | ADMINISTRATOR |
| DELETE | /insurers/{id} |
Delete insurer | ADMINISTRATOR |
| Method | Endpoint | Description | Access |
|---|---|---|---|
| GET | /brokers |
List all brokers | BROKER_MANAGER, ADMINISTRATOR |
| GET | /brokers/{id} |
Get broker by ID | BROKER_MANAGER, ADMINISTRATOR |
| POST | /brokers |
Create new broker | ADMINISTRATOR |
| PUT | /brokers/{id} |
Update broker | BROKER_MANAGER, ADMINISTRATOR |
| DELETE | /brokers/{id} |
Delete broker | ADMINISTRATOR |
| Method | Endpoint | Description | Access |
|---|---|---|---|
| GET | /commissions |
List commissions | ALL (filtered by broker for BROKER role) |
| GET | /commissions/{id} |
Get commission by ID | ALL (must be own for BROKER) |
| PATCH | /commissions/{id}/pay |
Mark as paid | BROKER_MANAGER, ADMINISTRATOR |
| Method | Endpoint | Description | Access |
|---|---|---|---|
| GET | /renewals |
List renewals | ALL (filtered by broker for BROKER role) |
| GET | /renewals/{id} |
Get renewal by ID | ALL |
| POST | /renewals |
Create renewal | BROKER, BROKER_MANAGER, ADMINISTRATOR |
| PATCH | /renewals/{id}/complete |
Complete renewal | BROKER_MANAGER, ADMINISTRATOR |
| PATCH | /renewals/{id}/decline |
Decline renewal | BROKER_MANAGER, ADMINISTRATOR |
| Method | Endpoint | Description | Access |
|---|---|---|---|
| GET | /dashboard/stats |
Get dashboard statistics | ALL (filtered by broker for BROKER role) |
{
"sub": "1", // User ID
"email": "admin@insurebroker.com",
"role": "ADMINISTRATOR",
"brokerId": 1, // Broker ID (all users including admins have this)
"iat": 1704067200,
"exp": 1704153600
}POST /api/auth/login
Content-Type: application/json
{
"email": "admin@insurebroker.com",
"password": "admin123"
}{
"user": {
"id": 1,
"email": "admin@insurebroker.com",
"firstName": "Admin",
"lastName": "User",
"role": "ADMINISTRATOR",
"brokerId": 1,
"active": true,
"createdAt": "2024-01-01T00:00:00Z",
"lastLogin": "2024-01-15T10:30:00Z"
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 86400
}All authenticated requests must include the token in the Authorization header:
GET /api/clients
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Request (Create/Update):
{
"firstName": "Ion",
"lastName": "Popescu",
"cnp": "1850315123456",
"email": "ion.popescu@example.com",
"phone": "+40722123456",
"address": "Str. Victoriei 123, București",
"dateOfBirth": "1985-03-15"
}Response:
{
"id": 1,
"brokerId": 1,
"brokerName": "Admin User",
"firstName": "Ion",
"lastName": "Popescu",
"cnp": "1850315123456",
"email": "ion.popescu@example.com",
"phone": "+40722123456",
"address": "Str. Victoriei 123, București",
"dateOfBirth": "1985-03-15",
"totalPolicies": 2,
"activePolicies": 1,
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}Request:
{
"productId": 1,
"sumInsured": 100000,
"customFieldValues": {
"building_year": "1990",
"construction_material": "Brick",
"has_alarm_system": true
},
"clientData": {
"cnp": "1850315123456",
"dateOfBirth": "1985-03-15",
"address": "București"
}
}Response:
{
"premium": 2450.00,
"breakdown": {
"basePremium": 2000.00,
"factors": [
{
"name": "Building Age",
"multiplier": 1.15,
"reason": "Building older than 30 years"
},
{
"name": "Security System",
"multiplier": 0.90,
"reason": "Has alarm system"
}
]
}
}Request (Create):
{
"clientId": 1,
"productId": 1,
"brokerId": 1,
"startDate": "2024-02-01",
"endDate": "2025-02-01",
"premium": 2450.00,
"sumInsured": 100000.00,
"gdprConsent": true,
"customFieldValues": {
"building_year": "1990",
"construction_material": "Brick"
}
}Response:
{
"id": 1,
"offerNumber": "OFR-2024-00001",
"clientId": 1,
"clientName": "Ion Popescu",
"productId": 1,
"productName": "Home Secure Basic",
"insurerName": "ABC Insurance",
"brokerId": 1,
"brokerName": "Admin User",
"startDate": "2024-02-01",
"endDate": "2025-02-01",
"premium": 2450.00,
"sumInsured": 100000.00,
"status": "PENDING",
"expiresAt": "2024-03-01",
"gdprConsent": true,
"gdprConsentDate": "2024-01-15",
"customFieldValues": {...},
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}Request:
{
"paymentMethod": "CARD_ONLINE",
"proofOfPayment": null
}Response:
{
"policy": {
"id": 1,
"policyNumber": "POL-2024-00001",
"offerId": 1,
"clientId": 1,
"clientName": "Ion Popescu",
"productId": 1,
"productName": "Home Secure Basic",
"insurerName": "ABC Insurance",
"brokerId": 1,
"brokerName": "Admin User",
"startDate": "2024-02-01",
"endDate": "2025-02-01",
"premium": 2450.00,
"sumInsured": 100000.00,
"status": "ACTIVE",
"paymentMethod": "CARD_ONLINE",
"paymentStatus": "VALIDATED",
"gdprConsent": true,
"gdprConsentDate": "2024-01-15",
"createdAt": "2024-01-15T10:30:00Z"
},
"message": "Policy created successfully"
}Response:
{
"id": 1,
"name": "Home Secure Basic",
"code": "HOME-BASIC",
"description": "Basic home insurance coverage",
"category": "HOME",
"insurerId": 1,
"insurerName": "ABC Insurance",
"baseRate": 0.02,
"active": true,
"customFields": [
{
"id": 1,
"name": "building_year",
"label": "Year of Construction",
"type": "number",
"required": true,
"placeholder": "e.g., 1990",
"factorMultiplier": 1.15,
"factorCondition": {"operator": "lt", "value": 1990}
},
{
"id": 2,
"name": "construction_material",
"label": "Construction Material",
"type": "select",
"required": true,
"options": ["Brick", "Concrete", "Wood", "Steel"],
"factorMultiplier": 1.20,
"factorCondition": {"operator": "eq", "value": "Wood"}
}
]
}Response:
{
"id": 1,
"policyId": 1,
"policyNumber": "POL-2024-0001",
"brokerId": 1,
"brokerName": "Admin User",
"amount": 122.50,
"rate": 0.05,
"status": "PENDING",
"paymentDate": null,
"createdAt": "2024-01-15T10:30:00Z"
}Response:
{
"totalClients": 150,
"activePolicies": 89,
"totalPremium": 125000.00,
"pendingRenewals": 12,
"monthlyCommissions": 8500.00,
"expiringThisMonth": 8,
"pendingOffers": 5,
"draftOffers": 3
}{
"content": [...],
"page": 0,
"size": 10,
"totalElements": 150,
"totalPages": 15,
"last": false
}ADMINISTRATOR (Full Access - Treated as Broker with Admin Privileges)
├── Has own broker profile with individual book of business
├── Can manage all entities
├── Can manage users and roles
├── Can view all data across all brokers
├── Can manage products and insurers
└── Can delete any record
BROKER_MANAGER (Limited Admin)
├── Has own broker profile with individual book of business
├── Can toggle to view all data ("Show All" toggle)
├── Can manage brokers
├── Can view all commissions (with toggle)
├── Can manage policies (all)
├── Can validate payments
├── Cannot manage products/insurers
└── Cannot delete brokers
BROKER (Basic Access)
├── Has own broker profile with individual book of business
├── Can manage own clients only
├── Can create offers/policies (assigned to self)
├── Can view own commissions only
├── Can view own policies only
└── Cannot access brokers page
For BROKER role, apply filters automatically:
// In PolicyService
public List<Policy> getPolicies(User currentUser) {
if (currentUser.getRole() == Role.BROKER) {
return policyRepository.findByBrokerId(currentUser.getBrokerId());
}
return policyRepository.findAll();
}
// For BROKER_MANAGER with showAll toggle
public List<Policy> getPolicies(User currentUser, boolean showAll) {
if (currentUser.getRole() == Role.ADMINISTRATOR) {
return policyRepository.findAll();
}
if (currentUser.getRole() == Role.BROKER_MANAGER && showAll) {
return policyRepository.findAll();
}
return policyRepository.findByBrokerId(currentUser.getBrokerId());
}
// In CommissionService
public List<Commission> getCommissions(User currentUser, boolean showAll) {
if (currentUser.getRole() == Role.ADMINISTRATOR) {
return commissionRepository.findAll();
}
if (currentUser.getRole() == Role.BROKER_MANAGER && showAll) {
return commissionRepository.findAll();
}
return commissionRepository.findByBrokerId(currentUser.getBrokerId());
}{
"timestamp": "2024-01-15T10:30:00Z",
"status": 400,
"error": "Bad Request",
"message": "Validation failed",
"path": "/api/clients",
"details": {
"cnp": "Invalid CNP format. Must be 13 digits.",
"email": "Invalid email format",
"firstName": "First name is required"
}
}| Code | Description |
|---|---|
| 200 | Success |
| 201 | Created |
| 400 | Bad Request (validation errors) |
| 401 | Unauthorized (not logged in) |
| 403 | Forbidden (insufficient permissions) |
| 404 | Not Found |
| 409 | Conflict (duplicate CNP, email, etc.) |
| 500 | Internal Server Error |
The backend must implement premium calculation with the following logic:
@Service
public class PremiumCalculationService {
public PremiumCalculationResponse calculatePremium(PremiumCalculationRequest request) {
Product product = productRepository.findById(request.getProductId())
.orElseThrow(() -> new NotFoundException("Product not found"));
// Base premium calculation
BigDecimal basePremium = request.getSumInsured()
.multiply(product.getBaseRate());
List<PremiumFactor> factors = new ArrayList<>();
BigDecimal totalMultiplier = BigDecimal.ONE;
// Apply custom field factors
for (ProductCustomField field : product.getCustomFields()) {
if (field.getFactorMultiplier() != null && field.getFactorCondition() != null) {
Object fieldValue = request.getCustomFieldValues().get(field.getName());
if (evaluateCondition(field.getFactorCondition(), fieldValue)) {
factors.add(new PremiumFactor(
field.getLabel(),
field.getFactorMultiplier(),
getFactorReason(field, fieldValue)
));
totalMultiplier = totalMultiplier.multiply(field.getFactorMultiplier());
}
}
}
// Apply client-based factors (age, location, etc.)
// ... additional factor logic based on clientData
BigDecimal finalPremium = basePremium.multiply(totalMultiplier)
.setScale(2, RoundingMode.HALF_UP);
return new PremiumCalculationResponse(
finalPremium,
new PremiumBreakdown(basePremium, factors)
);
}
private boolean evaluateCondition(FactorCondition condition, Object value) {
switch (condition.getOperator()) {
case "eq": return value.equals(condition.getValue());
case "lt": return compareNumeric(value, condition.getValue()) < 0;
case "gt": return compareNumeric(value, condition.getValue()) > 0;
case "lte": return compareNumeric(value, condition.getValue()) <= 0;
case "gte": return compareNumeric(value, condition.getValue()) >= 0;
default: return false;
}
}
}Romanian CNP (Cod Numeric Personal) validation:
public class CNPValidator {
private static final int[] CONTROL_KEY = {2, 7, 9, 1, 4, 6, 3, 5, 8, 2, 7, 9};
public static boolean isValid(String cnp) {
if (cnp == null || cnp.length() != 13) {
return false;
}
if (!cnp.matches("\\d{13}")) {
return false;
}
// Calculate control digit
int sum = 0;
for (int i = 0; i < 12; i++) {
sum += Character.getNumericValue(cnp.charAt(i)) * CONTROL_KEY[i];
}
int controlDigit = sum % 11;
if (controlDigit == 10) {
controlDigit = 1;
}
return controlDigit == Character.getNumericValue(cnp.charAt(12));
}
// Extract date of birth from CNP
public static LocalDate extractDateOfBirth(String cnp) {
int sexDigit = Character.getNumericValue(cnp.charAt(0));
int year = Integer.parseInt(cnp.substring(1, 3));
int month = Integer.parseInt(cnp.substring(3, 5));
int day = Integer.parseInt(cnp.substring(5, 7));
// Determine century based on sex digit
int century;
switch (sexDigit) {
case 1: case 2: century = 1900; break;
case 3: case 4: century = 1800; break;
case 5: case 6: century = 2000; break;
default: century = 1900;
}
return LocalDate.of(century + year, month, day);
}
}src/main/java/com/insurebroker/
├── InsureBrokerApplication.java
├── config/
│ ├── SecurityConfig.java
│ ├── JwtConfig.java
│ └── CorsConfig.java
├── controller/
│ ├── AuthController.java
│ ├── ClientController.java
│ ├── PolicyController.java
│ ├── OfferController.java
│ ├── ProductController.java
│ ├── PremiumController.java
│ ├── InsurerController.java
│ ├── BrokerController.java
│ ├── CommissionController.java
│ ├── RenewalController.java
│ └── DashboardController.java
├── dto/
│ ├── request/
│ │ ├── LoginRequest.java
│ │ ├── ClientRequest.java
│ │ ├── OfferRequest.java
│ │ ├── CheckoutRequest.java
│ │ └── PremiumCalculationRequest.java
│ └── response/
│ ├── AuthResponse.java
│ ├── ClientResponse.java
│ ├── OfferResponse.java
│ ├── PolicyResponse.java
│ └── PremiumCalculationResponse.java
├── entity/
│ ├── User.java
│ ├── Client.java
│ ├── Broker.java
│ ├── Offer.java
│ ├── Policy.java
│ ├── Product.java
│ ├── ProductCustomField.java
│ ├── Insurer.java
│ ├── Commission.java
│ ├── Renewal.java
│ └── ActivityLog.java
├── repository/
│ └── (JPA repositories)
├── service/
│ ├── AuthService.java
│ ├── ClientService.java
│ ├── OfferService.java
│ ├── PolicyService.java
│ ├── PremiumCalculationService.java
│ └── (other services)
├── security/
│ ├── JwtTokenProvider.java
│ ├── JwtAuthenticationFilter.java
│ └── UserPrincipal.java
├── exception/
│ ├── GlobalExceptionHandler.java
│ ├── ResourceNotFoundException.java
│ └── ValidationException.java
└── util/
└── CNPValidator.java
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies># Database
spring.datasource.url=jdbc:mysql://localhost:3306/insurebroker
spring.datasource.username=root
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
# JWT
jwt.secret=your-256-bit-secret-key-here
jwt.expiration=86400000
# File upload
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
file.upload-dir=./uploads
# Server
server.port=8080// src/services/api.ts
import axios from 'axios';
const API_BASE_URL = 'http://localhost:8080/api';
const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
// Add auth token to requests
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Handle auth errors
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export const premiumService = {
calculate: (request: PremiumCalculationRequest) =>
api.post<PremiumCalculationResponse>('/premium/calculate', request),
};
export const offerService = {
create: (offer: OfferRequest) => api.post<OfferResponse>('/offers', offer),
checkout: (offerId: number, payment: CheckoutRequest) =>
api.post<PolicyResponse>(`/offers/${offerId}/checkout`, payment),
};
export default api;When connecting to the real backend:
- Replace
mockProductswith API call to/products - Replace
mockClientswith API call to/clients - Replace
calculatePremiummock with API call to/premium/calculate - Replace
addOfferwith API call toPOST /offers - Replace
convertOfferToPolicywith API call toPOST /offers/{id}/checkout
| Action | BROKER | BROKER_MANAGER | ADMINISTRATOR |
|---|---|---|---|
| View own data | ✅ | ✅ | ✅ |
| View all data | ❌ | ✅ (with toggle) | ✅ |
| Create clients | ✅ | ✅ | ✅ |
| Delete clients | ❌ | ✅ | ✅ |
| Create offers | ✅ | ✅ | ✅ |
| Delete offers | ❌ | ✅ | ✅ |
| Create policies | ✅ | ✅ | ✅ |
| Cancel policies | ❌ | ✅ | ✅ |
| Delete policies | ❌ | ❌ | ❌ (never allowed) |
| Delete renewals | ❌ | ✅ | ✅ |
| Mark commission paid | ❌ | ✅ | ✅ |
| Manage brokers | ❌ | ✅ | ✅ |
| Manage products | ❌ | ❌ | ✅ |
| Manage insurers | ❌ | ❌ | ✅ |
The frontend implements a service layer (src/services/index.ts) that:
- Simulates backend filtering - Data is filtered based on user role before display
- Provides permission checks -
PermissionServicevalidates actions before allowing them - Handles soft deletes - Policies use status change instead of deletion
The usePermissions hook (src/hooks/usePermissions.ts) provides easy access to permission checks in components.
- Never delete policies - Always use status change to CANCELLED
- Activity logging required - All status changes must be logged
- BROKER isolation - BROKER role must only see their own data
- Toggle for managers - BROKER_MANAGER can toggle to view all data
- Admin as broker - ADMINISTRATOR is treated as a broker with admin privileges