- Overview
- Technology Stack
- Project Structure
- Getting Started
- Database Schema
- API Documentation
- Security & Authentication
- Business Logic
- Error Handling
- Configuration
- Testing
The Analify backend is a robust Spring Boot REST API that powers a multi-store retail analytics and bidding platform. It provides secure, role-based access to:
- Multi-store order processing
- Product and inventory management
- Employee management with hierarchical access control
- Real-time analytics and reporting
- Monthly bidding system for product sections
✅ RESTful API with comprehensive endpoints
✅ JWT-based authentication with role-based authorization
✅ PostgreSQL database with JPA/Hibernate ORM
✅ Automated monthly bidding cycles with scheduled tasks
✅ Complex business logic with transaction management
✅ DTO pattern for clean API contracts
✅ MapStruct for efficient entity-DTO mapping
- Spring Boot 3.4.13
- Java 21
- Maven 3.6+
- Spring Web - REST API controllers
- Spring Data JPA - Database access layer
- Spring Security - Authentication & authorization
- Spring Validation - Request validation
- PostgreSQL 14+ - Primary database
- Hibernate - ORM implementation
- HikariCP - Connection pooling (default)
- Spring Security - Security framework
- JWT (JSON Web Tokens) - Stateless authentication
- BCrypt - Password hashing
- Lombok - Reduce boilerplate code
- MapStruct - Entity-DTO mapping
- Spring Boot DevTools - Hot reload during development
backAnalify/
├── src/
│ ├── main/
│ │ ├── java/com/analyfy/analify/
│ │ │ ├── Controller/ # REST API Endpoints
│ │ │ │ ├── AuthController.java # Login/Authentication
│ │ │ │ ├── OrderController.java # Order CRUD operations
│ │ │ │ ├── ProductController.java # Product management
│ │ │ │ ├── EmployeeController.java # Employee management
│ │ │ │ ├── AnalyticsController.java # Dashboard analytics
│ │ │ │ └── BiddingController.java # Bidding system
│ │ │ │
│ │ │ ├── Service/ # Business Logic Layer
│ │ │ │ ├── AuthService.java # Authentication logic
│ │ │ │ ├── OrderService.java # Order processing
│ │ │ │ ├── ProductService.java # Product operations
│ │ │ │ ├── EmployeeService.java # Employee operations
│ │ │ │ ├── AnalyticsService.java # Analytics calculation
│ │ │ │ └── BiddingService.java # Bidding logic
│ │ │ │
│ │ │ ├── Repository/ # Database Access Layer
│ │ │ │ ├── UserRepository.java
│ │ │ │ ├── OrderRepository.java
│ │ │ │ ├── ProductRepository.java
│ │ │ │ ├── StoreRepository.java
│ │ │ │ ├── BidRepository.java
│ │ │ │ └── SectionRepository.java
│ │ │ │
│ │ │ ├── Entity/ # JPA Entities (Database Models)
│ │ │ │ ├── User.java # Abstract user class
│ │ │ │ ├── Caissier.java # Cashier entity
│ │ │ │ ├── AdminStore.java # Store admin entity
│ │ │ │ ├── AdminG.java # General admin entity
│ │ │ │ ├── Investor.java # Investor entity
│ │ │ │ ├── Order.java # Order entity
│ │ │ │ ├── Product.java # Product entity
│ │ │ │ ├── Store.java # Store entity
│ │ │ │ ├── Bid.java # Bid entity
│ │ │ │ ├── Section.java # Section entity
│ │ │ │ └── ... (30+ entities)
│ │ │ │
│ │ │ ├── DTO/ # Data Transfer Objects
│ │ │ │ ├── LoginRequestDTO.java
│ │ │ │ ├── LoginResponseDTO.java
│ │ │ │ ├── OrderDTO.java
│ │ │ │ ├── ProductDTO.java
│ │ │ │ ├── EmployeeResponseDTO.java
│ │ │ │ ├── BidDTO.java
│ │ │ │ └── ... (40+ DTOs)
│ │ │ │
│ │ │ ├── Mapper/ # Entity-DTO Mappers
│ │ │ │ ├── UserMapper.java # MapStruct mapper
│ │ │ │ ├── OrderMapper.java
│ │ │ │ ├── ProductMapper.java
│ │ │ │ └── BiddingMapper.java
│ │ │ │
│ │ │ ├── Security/ # Security Configuration
│ │ │ │ ├── JwtService.java # JWT generation/validation
│ │ │ │ ├── SecurityConfig.java # Security filter chain
│ │ │ │ └── JwtRequestFilter.java # JWT authentication filter
│ │ │ │
│ │ │ ├── Enum/ # Enumerations
│ │ │ │ ├── UserRole.java # User roles
│ │ │ │ ├── BidStatus.java # Bid statuses
│ │ │ │ └── SectionStatus.java # Section statuses
│ │ │ │
│ │ │ ├── Exception/ # Custom Exceptions
│ │ │ │ ├── ResourceNotFoundException.java
│ │ │ │ ├── AccessDeniedException.java
│ │ │ │ ├── BusinessValidationException.java
│ │ │ │ └── GlobalExceptionHandler.java # Exception handler
│ │ │ │
│ │ │ └── AnalifyApplication.java # Main Application Class
│ │ │
│ │ └── resources/
│ │ ├── application.properties # Configuration file
│ │ └── data.sql # (Optional) Initial data
│ │
│ └── test/
│ └── java/com/analyfy/analify/
│ └── (Test classes)
│
├── pom.xml # Maven dependencies
├── mvnw # Maven wrapper (Unix)
├── mvnw.cmd # Maven wrapper (Windows)
└── README.md # This file
- Java Development Kit (JDK) 21 or higher
- Maven 3.6+ (or use included Maven wrapper)
- PostgreSQL 14 or higher
- IDE (IntelliJ IDEA, Eclipse, or VS Code with Java extensions)
-
Install PostgreSQL (if not installed)
-
Create Database:
CREATE DATABASE analify;- Create User (optional, recommended for security):
CREATE USER analify_user WITH PASSWORD 'your_secure_password';
GRANT ALL PRIVILEGES ON DATABASE analify TO analify_user;-
Open
src/main/resources/application.properties -
Update Database Connection:
spring.datasource.url=jdbc:postgresql://localhost:5432/analify
spring.datasource.username=your_postgres_username
spring.datasource.password=your_postgres_password- Configure Server Port (default is 8081):
server.port=8081On Linux/Mac:
# Build the project
./mvnw clean install
# Run the application
./mvnw spring-boot:runOn Windows:
# Build the project
mvnw.cmd clean install
# Run the application
mvnw.cmd spring-boot:run# Build
mvn clean install
# Run
mvn spring-boot:run-
Check if server is running:
- Console should show:
Started AnalifyApplication in X seconds - Default URL: http://localhost:8081
- Console should show:
-
Test an endpoint:
curl http://localhost:8081/api/bidding/categoriesYou should receive a JSON response (may be empty initially).
Region (1) ──→ (N) State
State (1) ──→ (N) City
City (1) ──→ (N) Store
Store (1) ──→ (N) Caissier
Store (1) ──→ (1) AdminStore
Investor (1) ──→ (N) Product
Product (1) ──→ (N) Subcategory
Subcategory (1) ──→ (N) Category
Caissier (1) ──→ (N) Order
Order (1) ──→ (N) OrderItem
Product (1) ──→ (N) OrderItem
Store (1) ──→ (N) Stock
Product (1) ──→ (N) Stock
# Bidding System
Category (1) ──→ (N) Rang
Rang (1) ──→ (N) Face
Face (1) ──→ (N) Section
Section (1) ──→ (N) Bid
Investor (1) ──→ (N) Bid
users- Base user tablecaissier- Cashier-specific fieldsadmin_store- Store admin-specific fieldsadmin_g- General admin-specific fieldsinvestor- Investor-specific fields
store- Physical store locationsproduct- Product catalogorders- Order headersorder_item- Order line itemsstock- Product inventory by store
category- Top-level bidding categoriesrang- Second-level classificationface- Third-level classificationsection- Biddable sectionsbid- Bid records
The application uses Hibernate's DDL auto-generation:
# In application.properties
spring.jpa.hibernate.ddl-auto=updateOptions:
update- Update schema on startup (recommended for development)create- Drop and recreate schema (⚠️ destroys data)create-drop- Create on startup, drop on shutdownvalidate- Validate schema matches entitiesnone- No schema management
http://localhost:8081/api
All endpoints (except /auth/login) require JWT token:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...POST /api/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "password123"
}Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"userId": 1,
"userName": "John Doe",
"mail": "user@example.com",
"role": "INVESTOR"
}
}GET /api/orders?filterStoreId=5&filterRegionId=2
Authorization: Bearer {token}Query Parameters:
filterStoreId- Filter by storefilterRegionId- Filter by regionfilterStateId- Filter by statefilterCaissierId- Filter by cashierfilterProductId- Filter by product
POST /api/orders
Authorization: Bearer {token}
Content-Type: application/json
{
"cashierId": 3,
"items": [
{
"productId": 15,
"quantity": 2,
"discount": 0.1
}
]
}PATCH /api/orders/123/ship-date?shipDate=2026-01-15
Authorization: Bearer {token}DELETE /api/orders/123
Authorization: Bearer {token}GET /api/products?filterStoreId=5
Authorization: Bearer {token}POST /api/products
Authorization: Bearer {token}
Content-Type: application/json
{
"productName": "Laptop Dell XPS",
"price": 1299.99,
"subcategoryId": 8,
"investorId": 4
}PUT /api/products/25
Authorization: Bearer {token}
Content-Type: application/json
{
"productName": "Laptop Dell XPS 15",
"price": 1399.99
}PATCH /api/products/25/stock
Authorization: Bearer {token}
Content-Type: application/json
{
"storeId": 3,
"quantity": 50
}GET /api/products/alerts/low-stock
Authorization: Bearer {token}GET /api/employees/getall
Authorization: Bearer {token}GET /api/employees/store/5
Authorization: Bearer {token}GET /api/employees/12
Authorization: Bearer {token}POST /api/employees/add
Authorization: Bearer {token}
Content-Type: application/json
{
"userName": "Jane Smith",
"mail": "jane@example.com",
"password": "securePassword123",
"dateOfBirth": "1990-05-15",
"role": "CAISSIER",
"storeId": 5,
"salary": 2500.00,
"dateStarted": "2024-01-01"
}PUT /api/employees/12
Authorization: Bearer {token}
Content-Type: application/json
{
"userName": "Jane Doe",
"mail": "jane.doe@example.com",
"salary": 2800.00
}PUT /api/employees/12/assign-role?newRole=ADMIN_STORE&storeId=5
Authorization: Bearer {token}GET /api/analytics/dashboard?startDate=2024-01-01&endDate=2024-12-31&storeId=5
Authorization: Bearer {token}Query Parameters:
startDate- Filter start dateendDate- Filter end datestoreId- Filter by storeinvestorId- Filter by investorproductId- Filter by product
Response:
{
"totalRevenue": 156789.50,
"totalStockValue": 89234.00,
"totalOrders": 1245,
"totalProductsSold": 3456,
"averageOrderValue": 125.89,
"lowStockCount": 12,
"revenueOverTime": [...],
"topProducts": [...],
"salesByRegion": {...}
}GET /api/bidding/categories
Authorization: Bearer {token}GET /api/bidding/categories/5/rangs
Authorization: Bearer {token}GET /api/bidding/rangs/8/faces
Authorization: Bearer {token}GET /api/bidding/faces/12/sections
Authorization: Bearer {token}GET /api/bidding/sections/45
Authorization: Bearer {token}POST /api/bidding/bids
Authorization: Bearer {token}
Content-Type: application/json
{
"sectionId": 45,
"amount": 5000.00
}DELETE /api/bidding/bids/123
Authorization: Bearer {token}GET /api/bidding/my-bids
Authorization: Bearer {token}GET /api/bidding/my-current-winning-bids
Authorization: Bearer {token}GET /api/bidding/my-possessions
Authorization: Bearer {token}GET /api/bidding/season/current
Authorization: Bearer {token}Response:
{
"currentMonth": 1,
"currentPeriod": 1,
"isBiddingOpen": true,
"daysUntilClose": 15,
"periodStartDate": "2026-01-01",
"periodEndDate": "2026-01-31",
"biddingOpenDate": "2026-01-01",
"biddingCloseDate": "2026-01-31"
}1. User sends credentials → POST /api/auth/login
2. Server validates credentials
3. Server generates JWT token (contains userId, role)
4. Client stores token (localStorage)
5. Client sends token with every request: Authorization: Bearer {token}
6. JwtRequestFilter intercepts request
7. JwtService validates token
8. Request attributes populated: userId, role
9. Controller receives authenticated request
10. Service layer performs role-based authorization
Payload:
{
"userId": 123,
"role": "INVESTOR",
"iat": 1704067200,
"exp": 1704153600
}Authorization is enforced in the Service Layer:
public List<OrderDTO> getAllOrders(Long userId, UserRole role) {
if (role == UserRole.ADMIN_STORE) {
// Get store admin's store ID
Long storeId = getStoreIdForAdmin(userId);
// Return only orders from that store
return orderRepository.findByStoreId(storeId);
} else if (role == UserRole.ADMIN_G) {
// Return all orders
return orderRepository.findAll();
} else {
throw new AccessDeniedException("Insufficient permissions");
}
}Security Filter Chain (SecurityConfig.java):
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}Passwords are hashed using BCrypt:
@Service
public class AuthService {
private final PasswordEncoder passwordEncoder;
public void createUser(String password) {
String hashedPassword = passwordEncoder.encode(password);
// Store hashedPassword in database
}
}Automated Scheduler (BiddingService.java):
@Scheduled(cron = "0 0 0 1 * *") // Runs at midnight on 1st of every month
public void increasePricesForNewMonth() {
// 1. Get all sections
List<Section> sections = sectionRepository.findAll();
// 2. Increase prices by 2%
sections.forEach(section -> {
BigDecimal newPrice = section.getBasePrice()
.multiply(BigDecimal.valueOf(1.02));
section.setBasePrice(newPrice);
section.setCurrentPrice(newPrice);
section.setStatus(SectionStatus.OPEN);
section.setWinnerInvestor(null);
});
// 3. Set new deadline
LocalDate lastDayOfMonth = LocalDate.now().withDayOfMonth(
LocalDate.now().lengthOfMonth()
);
sections.forEach(section ->
section.setDeadline(lastDayOfMonth.atTime(23, 59, 59))
);
sectionRepository.saveAll(sections);
}When investor places a bid:
- Validate bid amount > current price
- Check section is OPEN
- Update previous PENDING bid → OUTBID
- Create new bid with status PENDING
- Update section's current price
- Save all changes in transaction
When section closes:
- Find highest bid (status = PENDING)
- Mark bid as WINNER
- Assign investor to section
- Set section status to CLOSED
public BigDecimal calculateOrderTotal(Order order) {
return order.getItems().stream()
.map(item -> {
BigDecimal price = item.getPrice();
BigDecimal discount = item.getDiscount();
int quantity = item.getQuantity();
BigDecimal lineTotal = price
.multiply(BigDecimal.valueOf(quantity))
.multiply(BigDecimal.ONE.subtract(discount));
return lineTotal;
})
.reduce(BigDecimal.ZERO, BigDecimal::add);
}All exceptions are caught by GlobalExceptionHandler.java:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(ex.getMessage()));
}
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorResponse> handleAccessDenied(AccessDeniedException ex) {
return ResponseEntity
.status(HttpStatus.FORBIDDEN)
.body(new ErrorResponse(ex.getMessage()));
}
}- ResourceNotFoundException - 404 Not Found
- AccessDeniedException - 403 Forbidden
- BusinessValidationException - 400 Bad Request
- AuthenticationException - 401 Unauthorized
# Application Name
spring.application.name=analify
# Server Configuration
server.port=8081
# Database Configuration
spring.datasource.url=jdbc:postgresql://localhost:5432/analify
spring.datasource.username=postgres
spring.datasource.password=your_password
spring.datasource.driver-class-name=org.postgresql.Driver
# JPA/Hibernate Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.format_sql=true
# Logging Configuration
logging.level.root=INFO
logging.level.com.analyfy.analify=DEBUG
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
# JWT Configuration (in code)
jwt.secret=your-secret-key-min-256-bits
jwt.expiration=86400000./mvnw test./mvnw verify./mvnw clean test jacoco:reportCoverage report: target/site/jacoco/index.html
./mvnw clean packageOutput: target/analify-0.0.1-SNAPSHOT.jar
java -jar target/analify-0.0.1-SNAPSHOT.jarexport DB_URL=jdbc:postgresql://prod-db.example.com:5432/analify
export DB_USERNAME=prod_user
export DB_PASSWORD=secure_password
export JWT_SECRET=very-secure-secret-key-for-production
export SERVER_PORT=8081
java -jar analify.jarCreate Dockerfile:
FROM openjdk:21-jdk-slim
COPY target/analify-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "/app.jar"]Build and run:
docker build -t analify-backend .
docker run -p 8081:8081 \
-e DB_URL=jdbc:postgresql://host.docker.internal:5432/analify \
-e DB_USERNAME=postgres \
-e DB_PASSWORD=yourpassword \
analify-backendPort already in use:
# Change port in application.properties
server.port=8082Database connection failed:
- Verify PostgreSQL is running:
sudo systemctl status postgresql - Check credentials in
application.properties - Test connection:
psql -U postgres -d analify
Lombok not working:
- Enable annotation processing in IDE
- IntelliJ: Settings → Build → Compiler → Annotation Processors → Enable
- Eclipse: Install Lombok plugin
MapStruct errors:
# Clean and rebuild
./mvnw clean install -UJWT token issues:
- Check token expiration time
- Verify secret key is at least 256 bits
- Ensure token is sent in Authorization header
Add indexes for frequently queried fields:
CREATE INDEX idx_order_cashier ON orders(cashier_id);
CREATE INDEX idx_product_investor ON product(investor_id);
CREATE INDEX idx_bid_section ON bid(section_id);In application.properties:
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=20000Use @EntityGraph to avoid N+1 queries:
@EntityGraph(attributePaths = {"items", "items.product"})
@Query("SELECT o FROM Order o WHERE o.cashier.id = :cashierId")
List<Order> findByCashierId(@Param("cashierId") Long cashierId);- Follow Java naming conventions
- Use Lombok for boilerplate reduction
- Keep controllers thin (delegate to services)
- Use DTOs for all API responses
- Document complex business logic
- Create feature branches:
feature/add-bidding-analytics - Write descriptive commit messages
- Run tests before committing
- Keep commits atomic and focused
- Main README:
../README.md - Frontend README:
../frontAnalify/README.md - Bidding System:
../BIDDING_SYSTEM_COMPLETE_FEATURES.md
Built with Spring Boot 3.4.13 and Java 21