Parcel Booking backend
Parcel Booking backend
# ParcelExpress — Backend Documentation
> **Version:** 1.0.0 | **Last Updated:** May 2026 | **Java:** 17 | **Spring Boot:** 3.2.5
---
## Table of Contents
1. [Project Overview](#1-project-overview)
2. [Tech Stack](#2-tech-stack)
3. [Prerequisites](#3-prerequisites)
4. [Project Structure](#4-project-structure)
5. [Configuration (application.properties)](#5-configuration-applicationproperties)
6. [pom.xml](#6-pomxml)
7. [Main Application Class](#7-main-application-class)
8. [Entity Models](#8-entity-models)
9. [DTOs (Data Transfer Objects)](#9-dtos-data-transfer-objects)
10. [Repositories](#10-repositories)
11. [Services](#11-services)
12. [Controllers](#12-controllers)
13. [CORS Configuration](#13-cors-configuration)
14. [Database Schema](#14-database-schema)
15. [Running the Application](#15-running-the-application)
16. [API Request / Response Examples](#16-api-request--response-examples)
17. [Business Rules](#17-business-rules)
18. [Error Handling Reference](#18-error-handling-reference)
19. [Troubleshooting](#19-troubleshooting)
---
## 1. Project Overview
**ParcelExpress** is a RESTful backend service built with Spring Boot that powers a full parcel booking and tracking platform. It exposes a clean JSON API consumed by an Angular frontend (running on `http://localhost:4200`).
### Key Features
| Feature | Description |
|---|---|
| Customer Registration | Validates unique email + mobile, auto-generates customer IDs |
| Admin Registration | Separate admin accounts with elevated privileges |
| Authentication | Simple password-based login with role-based identity |
| Booking Management | Create, view, and track parcels by booking ID or customer ID |
| Admin Controls | Update estimated pickup/drop times and delivery status |
| Auto-Inactivation | Scheduled daily job inactivates dormant customers (no bookings in 15+ days) |
| Booking History | Filter bookings by date range |
### Architecture Overview
┌─────────────────────────────────────────┐ │ Angular Frontend │ │ (http://localhost:4200) │ └──────────────────┬──────────────────────┘ │ HTTP / REST / JSON ┌──────────────────▼──────────────────────┐ │ Spring Boot Backend │ │ (port 8080) │ │ │ │ Controllers → Services → Repositories │ └──────────────────┬──────────────────────┘ │ JPA / Hibernate ┌──────────────────▼──────────────────────┐ │ Apache Derby (Embedded) │ │ parceldb/ │ └─────────────────────────────────────────┘
The backend follows a standard **layered architecture**:
- **Controller Layer** — Handles HTTP routing, request parsing, and response formatting.
- **Service Layer** — Contains all business logic and validation.
- **Repository Layer** — Spring Data JPA interfaces for database access.
- **Entity Layer** — JPA-mapped Java classes representing database tables.
- **DTO Layer** — Plain Java objects used to transfer data between layers and API boundaries.
---
## 2. Tech Stack
| Layer | Technology | Version |
|---|---|---|
| Framework | Spring Boot | 3.2.5 |
| Language | Java | 17 |
| Database | Apache Derby (Embedded) | 10.16.1.1 |
| ORM | Spring Data JPA / Hibernate | 6.x (bundled with Boot 3.2.5) |
| Build Tool | Apache Maven | 3.6+ |
| Scheduler | Spring `@Scheduled` | Built-in |
| API Style | RESTful JSON | — |
| CORS | Spring WebMVC `CorsRegistry` | — |
---
## 3. Prerequisites
Before you can build and run this project, ensure the following are installed on your system:
| Tool | Minimum Version | Download |
|---|---|---|
| Java JDK | 17 | https://adoptium.net |
| Apache Maven | 3.6 | https://maven.apache.org/download.cgi |
### Verify Your Installation
```bash
# Check Java version (must output 17 or higher)
java -version
# Check Maven version (must output 3.6 or higher)
mvn -version
Note: No separate database installation is required. Apache Derby runs embedded inside the JVM — the database files are created automatically in the
parceldb/directory inside your project root on first run.
4. Project Structure
parcel-booking-backend/
│
├── pom.xml # Maven build configuration
├── DOCUMENTATION.md # This file
│
├── src/
│ └── main/
│ ├── java/
│ │ └── com/
│ │ └── parcel/
│ │ │
│ │ ├── ParcelBookingApplication.java # Main entry point
│ │ │
│ │ ├── config/
│ │ │ └── CorsConfig.java # CORS configuration
│ │ │
│ │ ├── controller/
│ │ │ ├── AuthController.java # Auth endpoints
│ │ │ └── BookingController.java # Booking endpoints
│ │ │
│ │ ├── dto/
│ │ │ ├── AdminRegistrationDTO.java
│ │ │ ├── ApiResponse.java # Generic response wrapper
│ │ │ ├── BookingDTO.java
│ │ │ ├── CustomerRegistrationDTO.java
│ │ │ ├── DeliveryStatusDTO.java
│ │ │ ├── LoginDTO.java
│ │ │ └── PickupDropDTO.java
│ │ │
│ │ ├── model/
│ │ │ ├── Booking.java # Booking entity
│ │ │ ├── Customer.java # Customer entity
│ │ │ └── Login.java # Login/Auth entity
│ │ │
│ │ ├── repository/
│ │ │ ├── BookingRepository.java
│ │ │ ├── CustomerRepository.java
│ │ │ └── LoginRepository.java
│ │ │
│ │ └── service/
│ │ ├── AuthService.java # Auth & registration logic
│ │ ├── BookingService.java # Booking logic
│ │ └── SchedulerService.java # Auto-inactivation job
│ │
│ └── resources/
│ └── application.properties # App configuration
│
└── parceldb/ # Derby DB files (auto-created on first run)
├── seg0/
├── log/
└── service.properties
5. Configuration (application.properties)
Located at src/main/resources/application.properties.
# ── Application ────────────────────────────────────────────────────────────────
spring.application.name=parcel-booking-backend
server.port=8080
# ── Apache Derby Embedded Database ─────────────────────────────────────────────
# 'create=true' will create the database directory if it doesn't exist yet.
spring.datasource.url=jdbc:derby:parceldb;create=true
spring.datasource.driver-class-name=org.apache.derby.jdbc.EmbeddedDriver
spring.datasource.username=
spring.datasource.password=
# ── JPA / Hibernate ─────────────────────────────────────────────────────────────
# 'update' creates tables on first run and updates schema on subsequent runs.
# Change to 'create-drop' to reset the DB on every restart (dev only).
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.DerbyDialect
spring.jpa.properties.hibernate.format_sql=true
# ── CORS ─────────────────────────────────────────────────────────────────────────
spring.web.cors.allowed-origins=http://localhost:4200
Configuration Property Reference
| Property | Value | Purpose |
|---|---|---|
server.port |
8080 |
Port the embedded Tomcat listens on |
spring.datasource.url |
jdbc:derby:parceldb;create=true |
Relative path to Derby DB directory; auto-created |
spring.datasource.driver-class-name |
org.apache.derby.jdbc.EmbeddedDriver |
Derby embedded JDBC driver |
spring.jpa.hibernate.ddl-auto |
update |
Schema management strategy |
spring.jpa.show-sql |
true |
Logs all SQL to console (disable in production) |
spring.jpa.database-platform |
org.hibernate.dialect.DerbyDialect |
Hibernate SQL dialect for Derby |
spring.jpa.properties.hibernate.format_sql |
true |
Pretty-prints SQL in logs |
6. pom.xml
The complete Maven build descriptor. Derby ships two artifacts: derby (embedded engine) and derbyclient (network client driver, included for completeness).
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- ── Project Identity ─────────────────────────────────────────────────── -->
<groupId>com.parcel</groupId>
<artifactId>parcel-booking-backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>parcel-booking-backend</name>
<description>Parcel Booking Backend - Spring Boot + Apache Derby</description>
<!-- ── Spring Boot Parent ───────────────────────────────────────────────── -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<!-- ── Java Version ─────────────────────────────────────────────────────── -->
<properties>
<java.version>17</java.version>
</properties>
<!-- ── Dependencies ─────────────────────────────────────────────────────── -->
<dependencies>
<!-- Spring Web (REST controllers, embedded Tomcat) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data JPA + Hibernate -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Apache Derby Embedded Engine -->
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derby</artifactId>
<version>10.16.1.1</version>
</dependency>
<!-- Apache Derby Network Client (optional; included for completeness) -->
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derbyclient</artifactId>
<version>10.16.1.1</version>
</dependency>
<!-- Spring Boot Test (JUnit 5, Mockito, etc.) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- ── Build Plugins ────────────────────────────────────────────────────── -->
<build>
<plugins>
<!-- Packages the application as a self-contained executable JAR -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
7. Main Application Class
src/main/java/com/parcel/ParcelBookingApplication.java
package com.parcel;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* Entry point for the ParcelExpress backend application.
*
* @SpringBootApplication — Enables component scanning, auto-configuration,
* and configuration properties.
* @EnableScheduling — Activates Spring's task scheduling engine,
* required for the SchedulerService auto-inactivation job.
*/
@SpringBootApplication
@EnableScheduling
public class ParcelBookingApplication {
public static void main(String[] args) {
SpringApplication.run(ParcelBookingApplication.class, args);
}
}
Why
@EnableScheduling? Without this annotation, any@Scheduledmethods in the application (like the auto-inactivation job inSchedulerService) will be silently ignored at runtime.
8. Entity Models
Entities are JPA-managed Java classes that map directly to Derby database tables. Hibernate reads these class definitions and automatically creates or updates the corresponding SQL tables when the application starts (controlled by ddl-auto=update).
8.1 Customer.java
src/main/java/com/parcel/model/Customer.java
Represents a registered customer. Each customer has a manually assigned ID (format: CUST-XXXXXXXX), enforced unique constraints on both EMAIL and MOBILE, and a STATUS field that can be Active or Inactive.
package com.parcel.model;
import jakarta.persistence.*;
import java.time.LocalDate;
/**
* Represents a registered parcel service customer.
*
* Unique constraints are defined at the table level so Derby can enforce
* them with named constraint objects — useful for meaningful error messages.
*/
@Entity
@Table(
name = "CUSTOMER",
uniqueConstraints = {
@UniqueConstraint(name = "UK_CUSTOMER_EMAIL", columnNames = {"EMAIL"}),
@UniqueConstraint(name = "UK_CUSTOMER_MOBILE", columnNames = {"MOBILE"})
}
)
public class Customer {
/**
* Business-generated primary key in the format CUST-XXXXXXXX.
* Generated in AuthService, not by the database.
*/
@Id
@Column(name = "CONSUMER_ID", length = 20)
private String consumerId;
/** Full name of the customer. */
@Column(name = "NAME", nullable = false, length = 100)
private String name;
/** Email address — must be globally unique across all customers. */
@Column(name = "EMAIL", nullable = false, length = 100, unique = true)
private String email;
/** 10-digit mobile number — must be globally unique across all customers. */
@Column(name = "MOBILE", nullable = false, length = 15, unique = true)
private String mobile;
/** Full delivery/contact address. */
@Column(name = "ADDRESS", nullable = false, length = 500)
private String address;
/**
* Account status: "Active" (default) or "Inactive".
* Auto-inactivation scheduler may set this to "Inactive".
*/
@Column(name = "STATUS", length = 20)
private String status = "Active";
/** Date of registration — set at registration time. */
@Column(name = "REGISTRATION_DATE")
private LocalDate registrationDate;
// ── Getters and Setters ──────────────────────────────────────────────────
public String getConsumerId() { return consumerId; }
public void setConsumerId(String consumerId) { this.consumerId = consumerId; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getMobile() { return mobile; }
public void setMobile(String mobile) { this.mobile = mobile; }
public String getAddress() { return address; }
public void setAddress(String address) { this.address = address; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public LocalDate getRegistrationDate() { return registrationDate; }
public void setRegistrationDate(LocalDate registrationDate) {
this.registrationDate = registrationDate;
}
}
Field Reference:
| Field | DB Column | Type | Constraints | Notes |
|---|---|---|---|---|
consumerId |
CONSUMER_ID |
VARCHAR(20) |
PK | Format: CUST-XXXXXXXX |
name |
NAME |
VARCHAR(100) |
NOT NULL | Full display name |
email |
EMAIL |
VARCHAR(100) |
NOT NULL, UNIQUE | Validated for uniqueness before insert |
mobile |
MOBILE |
VARCHAR(15) |
NOT NULL, UNIQUE | Must be exactly 10 digits |
address |
ADDRESS |
VARCHAR(500) |
NOT NULL | Contact/delivery address |
status |
STATUS |
VARCHAR(20) |
— | Default: "Active" |
registrationDate |
REGISTRATION_DATE |
DATE |
— | Set at registration time |
8.2 Login.java
src/main/java/com/parcel/model/Login.java
Stores credentials and role for both customers and admins. The consumerId field links a customer login to its CUSTOMER row. Admin logins leave consumerId as null.
package com.parcel.model;
import jakarta.persistence.*;
/**
* Stores login credentials for both customers and administrators.
*
* For customers: consumerId links to the CUSTOMER table.
* For admins: consumerId is null; role is "Admin".
*/
@Entity
@Table(name = "LOGIN")
public class Login {
/**
* Login username (e.g. an email address or chosen handle).
* This is the primary key for the LOGIN table.
*/
@Id
@Column(name = "USER_ID", length = 50)
private String userId;
/**
* Plain-text password.
* NOTE: Passwords are stored and compared as plain text in this system.
* For production, use BCrypt or similar hashing.
*/
@Column(name = "PASSWORD", length = 100)
private String password;
/**
* Role of the account: "Customer" or "Admin".
* Used by the frontend to route to the appropriate dashboard.
*/
@Column(name = "ROLE", length = 20)
private String role;
/**
* Foreign key reference to CUSTOMER.CONSUMER_ID.
* Null for admin accounts.
*/
@Column(name = "CONSUMER_ID", length = 20)
private String consumerId;
/**
* Mirrors the customer's status ("Active" / "Inactive").
* Updated in sync with Customer.status by the scheduler.
*/
@Column(name = "STATUS", length = 20)
private String status;
// ── Getters and Setters ──────────────────────────────────────────────────
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
public String getConsumerId() { return consumerId; }
public void setConsumerId(String consumerId) { this.consumerId = consumerId; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
}
Field Reference:
| Field | DB Column | Type | Constraints | Notes |
|---|---|---|---|---|
userId |
USER_ID |
VARCHAR(50) |
PK | Typically the user's email |
password |
PASSWORD |
VARCHAR(100) |
— | Plain text (see security note above) |
role |
ROLE |
VARCHAR(20) |
— | "Customer" or "Admin" |
consumerId |
CONSUMER_ID |
VARCHAR(20) |
FK (logical) | Links to CUSTOMER.CONSUMER_ID; null for admins |
status |
STATUS |
VARCHAR(20) |
— | "Active" or "Inactive" |
8.3 Booking.java
src/main/java/com/parcel/model/Booking.java
Represents a single parcel booking. Captures sender details (from the linked customer), full recipient details, parcel specifications, scheduling times, and delivery status.
package com.parcel.model;
import jakarta.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* Represents a parcel booking placed by a customer.
*
* bookingId is auto-generated in BookingService (format: BK-XXXXXXXX).
* consumerId links this booking to a registered customer.
*/
@Entity
@Table(name = "BOOKING")
public class Booking {
/**
* Unique booking reference. Format: BK-XXXXXXXX.
* Generated in BookingService using UUID.
*/
@Id
@Column(name = "BOOKING_ID", length = 20)
private String bookingId;
/** FK to CUSTOMER.CONSUMER_ID — the customer who placed this booking. */
@Column(name = "CONSUMER_ID", length = 20)
private String consumerId;
/** Sender's name (populated from the customer record). */
@Column(name = "NAME", length = 100)
private String name;
/** Sender's address (pickup address). */
@Column(name = "ADDRESS", length = 500)
private String address;
/** Sender's contact details (phone or email). */
@Column(name = "CONTACT_DETAILS", length = 100)
private String contactDetails;
/** Recipient's full name. */
@Column(name = "REC_NAME", length = 100)
private String recName;
/** Recipient's full address. */
@Column(name = "REC_ADDRESS", length = 500)
private String recAddress;
/** Recipient's postal / PIN code. */
@Column(name = "REC_PIN", length = 10)
private String recPin;
/** Recipient's mobile number. */
@Column(name = "REC_MOBILE", length = 15)
private String recMobile;
/** Parcel weight in grams. */
@Column(name = "PAR_WEIGHT_GRAM")
private Double parWeightGram;
/** Short description of the parcel contents. */
@Column(name = "PAR_CONTENTS_DESCRIPTION", length = 300)
private String parContentsDescription;
/**
* Delivery type: e.g. "Standard", "Express", "Overnight".
* Drives SLA and pricing on the frontend.
*/
@Column(name = "PAR_DELIVERY_TYPE", length = 50)
private String parDeliveryType;
/**
* Packing preference: e.g. "Fragile", "Standard", "Bulk".
*/
@Column(name = "PAR_PACKING_PREFERENCE", length = 50)
private String parPackingPreference;
/**
* Customer-requested pickup time window (informational).
* Stored as a string, e.g. "Morning (9am–12pm)".
*/
@Column(name = "PAR_PICKUP_TIME", length = 100)
private String parPickupTime;
/**
* Customer-requested drop-off time window (informational).
* Stored as a string, e.g. "Afternoon (1pm–5pm)".
*/
@Column(name = "PAR_DROPOFF_TIME", length = 100)
private String parDropoffTime;
/** Admin-confirmed/updated pickup datetime. */
@Column(name = "PICKUP_UPDATED_TIME")
private LocalDateTime pickupUpdatedTime;
/** Admin-confirmed/updated drop-off datetime. */
@Column(name = "DROP_UPDATED_TIME")
private LocalDateTime dropUpdatedTime;
/**
* Current delivery status.
* Typical values: "Booked", "Picked Up", "In Transit", "Out for Delivery", "Delivered".
*/
@Column(name = "DELIVERY_STATUS", length = 50)
private String deliveryStatus;
/** Date on which this booking was created. */
@Column(name = "BOOKING_DATE")
private LocalDate bookingDate;
// ── Getters and Setters ──────────────────────────────────────────────────
public String getBookingId() { return bookingId; }
public void setBookingId(String bookingId) { this.bookingId = bookingId; }
public String getConsumerId() { return consumerId; }
public void setConsumerId(String consumerId) { this.consumerId = consumerId; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getAddress() { return address; }
public void setAddress(String address) { this.address = address; }
public String getContactDetails() { return contactDetails; }
public void setContactDetails(String contactDetails) { this.contactDetails = contactDetails; }
public String getRecName() { return recName; }
public void setRecName(String recName) { this.recName = recName; }
public String getRecAddress() { return recAddress; }
public void setRecAddress(String recAddress) { this.recAddress = recAddress; }
public String getRecPin() { return recPin; }
public void setRecPin(String recPin) { this.recPin = recPin; }
public String getRecMobile() { return recMobile; }
public void setRecMobile(String recMobile) { this.recMobile = recMobile; }
public Double getParWeightGram() { return parWeightGram; }
public void setParWeightGram(Double parWeightGram) { this.parWeightGram = parWeightGram; }
public String getParContentsDescription() { return parContentsDescription; }
public void setParContentsDescription(String parContentsDescription) {
this.parContentsDescription = parContentsDescription;
}
public String getParDeliveryType() { return parDeliveryType; }
public void setParDeliveryType(String parDeliveryType) { this.parDeliveryType = parDeliveryType; }
public String getParPackingPreference() { return parPackingPreference; }
public void setParPackingPreference(String parPackingPreference) {
this.parPackingPreference = parPackingPreference;
}
public String getParPickupTime() { return parPickupTime; }
public void setParPickupTime(String parPickupTime) { this.parPickupTime = parPickupTime; }
public String getParDropoffTime() { return parDropoffTime; }
public void setParDropoffTime(String parDropoffTime) { this.parDropoffTime = parDropoffTime; }
public LocalDateTime getPickupUpdatedTime() { return pickupUpdatedTime; }
public void setPickupUpdatedTime(LocalDateTime pickupUpdatedTime) {
this.pickupUpdatedTime = pickupUpdatedTime;
}
public LocalDateTime getDropUpdatedTime() { return dropUpdatedTime; }
public void setDropUpdatedTime(LocalDateTime dropUpdatedTime) {
this.dropUpdatedTime = dropUpdatedTime;
}
public String getDeliveryStatus() { return deliveryStatus; }
public void setDeliveryStatus(String deliveryStatus) { this.deliveryStatus = deliveryStatus; }
public LocalDate getBookingDate() { return bookingDate; }
public void setBookingDate(LocalDate bookingDate) { this.bookingDate = bookingDate; }
}
Field Reference:
| Field | DB Column | Type | Notes |
|---|---|---|---|
bookingId |
BOOKING_ID |
VARCHAR(20) |
PK; format: BK-XXXXXXXX |
consumerId |
CONSUMER_ID |
VARCHAR(20) |
FK to CUSTOMER |
name |
NAME |
VARCHAR(100) |
Sender name (from customer record) |
address |
ADDRESS |
VARCHAR(500) |
Pickup / sender address |
contactDetails |
CONTACT_DETAILS |
VARCHAR(100) |
Sender's phone or email |
recName |
REC_NAME |
VARCHAR(100) |
Recipient name |
recAddress |
REC_ADDRESS |
VARCHAR(500) |
Recipient delivery address |
recPin |
REC_PIN |
VARCHAR(10) |
Recipient postal code |
recMobile |
REC_MOBILE |
VARCHAR(15) |
Recipient mobile |
parWeightGram |
PAR_WEIGHT_GRAM |
DOUBLE |
Parcel weight in grams |
parContentsDescription |
PAR_CONTENTS_DESCRIPTION |
VARCHAR(300) |
What's inside |
parDeliveryType |
PAR_DELIVERY_TYPE |
VARCHAR(50) |
e.g., "Express" |
parPackingPreference |
PAR_PACKING_PREFERENCE |
VARCHAR(50) |
e.g., "Fragile" |
parPickupTime |
PAR_PICKUP_TIME |
VARCHAR(100) |
Customer-preferred pickup window |
parDropoffTime |
PAR_DROPOFF_TIME |
VARCHAR(100) |
Customer-preferred dropoff window |
pickupUpdatedTime |
PICKUP_UPDATED_TIME |
TIMESTAMP |
Admin-set confirmed pickup time |
dropUpdatedTime |
DROP_UPDATED_TIME |
TIMESTAMP |
Admin-set confirmed dropoff time |
deliveryStatus |
DELIVERY_STATUS |
VARCHAR(50) |
Current status in delivery pipeline |
bookingDate |
BOOKING_DATE |
DATE |
Date booking was created |
9. DTOs (Data Transfer Objects)
DTOs are plain Java classes with no persistence annotations. They serve as the contract between the HTTP API and the service layer, decoupling the API surface from the internal entity structure.
9.1 CustomerRegistrationDTO.java
src/main/java/com/parcel/dto/CustomerRegistrationDTO.java
Carries all information needed to register a new customer in a single request.
package com.parcel.dto;
/**
* Incoming payload for POST /api/auth/registerCustomer.
*
* userId → stored as Login.userId (the user's chosen login name / email)
* password → stored in Login.password (plain text)
* name → stored in Customer.name
* email → stored in Customer.email (must be unique)
* mobile → stored in Customer.mobile (must be exactly 10 digits, unique)
* address → stored in Customer.address
*/
public class CustomerRegistrationDTO {
private String userId;
private String password;
private String name;
private String email;
private String mobile;
private String address;
// ── Getters and Setters ──────────────────────────────────────────────────
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getMobile() { return mobile; }
public void setMobile(String mobile) { this.mobile = mobile; }
public String getAddress() { return address; }
public void setAddress(String address) { this.address = address; }
}
9.2 AdminRegistrationDTO.java
src/main/java/com/parcel/dto/AdminRegistrationDTO.java
Carries the minimal information needed to create an admin account (no customer record is created).
package com.parcel.dto;
/**
* Incoming payload for POST /api/auth/registerAdmin.
*
* userId → stored as Login.userId
* password → stored in Login.password
* name → stored in Login (or used for display; no Customer row is created)
*/
public class AdminRegistrationDTO {
private String userId;
private String password;
private String name;
// ── Getters and Setters ──────────────────────────────────────────────────
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
9.3 LoginDTO.java
src/main/java/com/parcel/dto/LoginDTO.java
Carries credentials for the login endpoint.
package com.parcel.dto;
/**
* Incoming payload for POST /api/auth/validateLogin.
*/
public class LoginDTO {
private String userId;
private String password;
// ── Getters and Setters ──────────────────────────────────────────────────
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
9.4 BookingDTO.java
src/main/java/com/parcel/dto/BookingDTO.java
Carries all parcel and recipient details needed to create a new booking.
package com.parcel.dto;
/**
* Incoming payload for POST /api/booking/viewbookService.
*
* consumerId links this booking to a registered customer.
* All rec* fields describe the parcel recipient.
* All par* fields describe the parcel itself.
*/
public class BookingDTO {
private String consumerId;
private String name;
private String address;
private String contactDetails;
// Recipient details
private String recName;
private String recAddress;
private String recPin;
private String recMobile;
// Parcel details
private Double parWeightGram;
private String parContentsDescription;
private String parDeliveryType;
private String parPackingPreference;
private String parPickupTime;
private String parDropoffTime;
// ── Getters and Setters ──────────────────────────────────────────────────
public String getConsumerId() { return consumerId; }
public void setConsumerId(String consumerId) { this.consumerId = consumerId; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getAddress() { return address; }
public void setAddress(String address) { this.address = address; }
public String getContactDetails() { return contactDetails; }
public void setContactDetails(String contactDetails) { this.contactDetails = contactDetails; }
public String getRecName() { return recName; }
public void setRecName(String recName) { this.recName = recName; }
public String getRecAddress() { return recAddress; }
public void setRecAddress(String recAddress) { this.recAddress = recAddress; }
public String getRecPin() { return recPin; }
public void setRecPin(String recPin) { this.recPin = recPin; }
public String getRecMobile() { return recMobile; }
public void setRecMobile(String recMobile) { this.recMobile = recMobile; }
public Double getParWeightGram() { return parWeightGram; }
public void setParWeightGram(Double parWeightGram) { this.parWeightGram = parWeightGram; }
public String getParContentsDescription() { return parContentsDescription; }
public void setParContentsDescription(String d) { this.parContentsDescription = d; }
public String getParDeliveryType() { return parDeliveryType; }
public void setParDeliveryType(String parDeliveryType) { this.parDeliveryType = parDeliveryType; }
public String getParPackingPreference() { return parPackingPreference; }
public void setParPackingPreference(String p) { this.parPackingPreference = p; }
public String getParPickupTime() { return parPickupTime; }
public void setParPickupTime(String parPickupTime) { this.parPickupTime = parPickupTime; }
public String getParDropoffTime() { return parDropoffTime; }
public void setParDropoffTime(String parDropoffTime) { this.parDropoffTime = parDropoffTime; }
}
9.5 PickupDropDTO.java
src/main/java/com/parcel/dto/PickupDropDTO.java
Used by admins to set confirmed pickup and drop-off timestamps on a booking.
package com.parcel.dto;
import java.time.LocalDateTime;
/**
* Incoming payload for PUT /api/booking/updatepickupanddrop/{bookingId}.
*
* Both fields are optional — only provided fields will be updated.
*/
public class PickupDropDTO {
/** Admin-confirmed pickup datetime. */
private LocalDateTime pickupUpdatedTime;
/** Admin-confirmed drop-off datetime. */
private LocalDateTime dropUpdatedTime;
// ── Getters and Setters ──────────────────────────────────────────────────
public LocalDateTime getPickupUpdatedTime() { return pickupUpdatedTime; }
public void setPickupUpdatedTime(LocalDateTime pickupUpdatedTime) {
this.pickupUpdatedTime = pickupUpdatedTime;
}
public LocalDateTime getDropUpdatedTime() { return dropUpdatedTime; }
public void setDropUpdatedTime(LocalDateTime dropUpdatedTime) {
this.dropUpdatedTime = dropUpdatedTime;
}
}
9.6 DeliveryStatusDTO.java
src/main/java/com/parcel/dto/DeliveryStatusDTO.java
Used by admins to update the delivery pipeline status of a booking.
package com.parcel.dto;
/**
* Incoming payload for PUT /api/booking/updateDeliveryStatus/{bookingId}.
*/
public class DeliveryStatusDTO {
/**
* New delivery status string.
* Typical values: "Booked", "Picked Up", "In Transit",
* "Out for Delivery", "Delivered".
*/
private String deliveryStatus;
// ── Getters and Setters ──────────────────────────────────────────────────
public String getDeliveryStatus() { return deliveryStatus; }
public void setDeliveryStatus(String deliveryStatus) {
this.deliveryStatus = deliveryStatus;
}
}
9.7 ApiResponse.java — Generic Response Wrapper
src/main/java/com/parcel/dto/ApiResponse.java
Every API endpoint returns an ApiResponse<T> — a consistent JSON envelope with a status, human-readable message, and optional generic data payload. This makes it easy for the frontend to handle both success and error cases uniformly.
package com.parcel.dto;
/**
* Generic API response envelope used by all endpoints.
*
* JSON shape:
* {
* "status": "success" | "error",
* "message": "Human-readable message",
* "data": { ... } | null
* }
*
* Usage:
* return ResponseEntity.ok(ApiResponse.success("Customer registered", customer));
* return ResponseEntity.badRequest().body(ApiResponse.error("Email already exists"));
*/
public class ApiResponse<T> {
private String status;
private String message;
private T data;
// ── Static Factory Methods ───────────────────────────────────────────────
/** Creates a success response with a data payload. */
public static <T> ApiResponse<T> success(String message, T data) {
ApiResponse<T> r = new ApiResponse<>();
r.status = "success";
r.message = message;
r.data = data;
return r;
}
/** Creates a success response with no data payload (data will be null). */
public static <T> ApiResponse<T> success(String message) {
return success(message, null);
}
/** Creates an error response. */
public static <T> ApiResponse<T> error(String message) {
ApiResponse<T> r = new ApiResponse<>();
r.status = "error";
r.message = message;
return r;
}
// ── Getters and Setters ──────────────────────────────────────────────────
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public T getData() { return data; }
public void setData(T data) { this.data = data; }
}
10. Repositories
Repositories are Spring Data JPA interfaces. Spring automatically generates the implementation at runtime — no SQL or boilerplate needed. Each interface extends JpaRepository<EntityType, PrimaryKeyType>, inheriting standard CRUD operations (save, findById, findAll, deleteById, etc.).
10.1 CustomerRepository.java
src/main/java/com/parcel/repository/CustomerRepository.java
package com.parcel.repository;
import com.parcel.model.Customer;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
@Repository
public interface CustomerRepository extends JpaRepository<Customer, String> {
/**
* Find a customer by their email address.
* Used during registration to enforce email uniqueness before the DB constraint fires.
*/
Optional<Customer> findByEmail(String email);
/**
* Find a customer by their mobile number.
* Used during registration to enforce mobile uniqueness.
*/
Optional<Customer> findByMobile(String mobile);
/**
* Find all customers with a given status (e.g. "Active" or "Inactive").
* Used by the scheduler to find active customers for inactivation checks.
*/
List<Customer> findByStatus(String status);
/**
* Find all Active customers whose registration date is on or before a given date.
* Used by the scheduler to identify dormant customers (registered > 15 days ago).
*
* Example: findByStatusAndRegistrationDateBefore("Active", LocalDate.now().minusDays(15))
*/
List<Customer> findByStatusAndRegistrationDateBefore(String status, LocalDate date);
}
Method Summary:
| Method | Generated SQL | Purpose |
|---|---|---|
findByEmail(email) |
SELECT * FROM CUSTOMER WHERE EMAIL = ? |
Uniqueness check before insert |
findByMobile(mobile) |
SELECT * FROM CUSTOMER WHERE MOBILE = ? |
Uniqueness check before insert |
findByStatus(status) |
SELECT * FROM CUSTOMER WHERE STATUS = ? |
Filter by Active/Inactive |
findByStatusAndRegistrationDateBefore(status, date) |
SELECT * FROM CUSTOMER WHERE STATUS = ? AND REGISTRATION_DATE < ? |
Scheduler inactivation query |
10.2 LoginRepository.java
src/main/java/com/parcel/repository/LoginRepository.java
package com.parcel.repository;
import com.parcel.model.Login;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface LoginRepository extends JpaRepository<Login, String> {
/**
* Find a login record by userId (the primary key).
* Used during authentication to look up credentials.
* JpaRepository.findById() can also be used, but this is more explicit.
*/
Optional<Login> findByUserId(String userId);
/**
* Find the login record associated with a given consumerId.
* Used by the scheduler to update LOGIN.STATUS when a customer is inactivated.
*/
Optional<Login> findByConsumerId(String consumerId);
/**
* Find all login records with a specific status.
* Useful for admin queries about active/inactive accounts.
*/
List<Login> findByStatus(String status);
}
Method Summary:
| Method | Generated SQL | Purpose |
|---|---|---|
findByUserId(userId) |
SELECT * FROM LOGIN WHERE USER_ID = ? |
Authentication lookup |
findByConsumerId(consumerId) |
SELECT * FROM LOGIN WHERE CONSUMER_ID = ? |
Sync status with customer |
findByStatus(status) |
SELECT * FROM LOGIN WHERE STATUS = ? |
Filter by account status |
10.3 BookingRepository.java
src/main/java/com/parcel/repository/BookingRepository.java
package com.parcel.repository;
import com.parcel.model.Booking;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
@Repository
public interface BookingRepository extends JpaRepository<Booking, String> {
/**
* Retrieve all bookings placed by a specific customer.
* Used for customer parcel tracking and history views.
*/
List<Booking> findByConsumerId(String consumerId);
/**
* Count bookings for a customer — used by the scheduler to determine
* whether a customer has ever placed a booking.
*/
long countByConsumerId(String consumerId);
/**
* Retrieve all bookings placed on a specific date.
* Used for the admin booking history/date-filter view.
*/
List<Booking> findByBookingDate(LocalDate bookingDate);
/**
* Retrieve all bookings within a date range (inclusive on both ends).
* Useful for generating date-range booking history reports.
*/
List<Booking> findByBookingDateBetween(LocalDate startDate, LocalDate endDate);
}
Method Summary:
| Method | Generated SQL | Purpose |
|---|---|---|
findByConsumerId(id) |
SELECT * FROM BOOKING WHERE CONSUMER_ID = ? |
Customer parcel history |
countByConsumerId(id) |
SELECT COUNT(*) FROM BOOKING WHERE CONSUMER_ID = ? |
Scheduler: zero-booking check |
findByBookingDate(date) |
SELECT * FROM BOOKING WHERE BOOKING_DATE = ? |
Filter by exact date |
findByBookingDateBetween(start, end) |
SELECT * FROM BOOKING WHERE BOOKING_DATE BETWEEN ? AND ? |
Date range report |
11. Services
Services contain all business logic. Controllers delegate to services; services use repositories.
11.1 AuthService.java
src/main/java/com/parcel/service/AuthService.java
Handles all authentication and registration operations.
Responsibilities:
- Validate all mandatory registration fields.
- Enforce mobile format (exactly 10 digits).
- Check uniqueness of email, mobile, and userId before attempting DB insert.
- Auto-generate a
consumerId(CUST-+ first 8 chars of UUID). - Set
registrationDateto today's date. - Create both a
Customerand aLoginrow atomically for customer registrations. - Create only a
Loginrow (with role"Admin") for admin registrations. - Validate login by matching
userIdandpasswordfrom theLOGINtable.
package com.parcel.service;
import com.parcel.dto.*;
import com.parcel.model.*;
import com.parcel.repository.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.Optional;
import java.util.UUID;
@Service
public class AuthService {
@Autowired
private CustomerRepository customerRepository;
@Autowired
private LoginRepository loginRepository;
// ── Customer Registration ────────────────────────────────────────────────
/**
* Registers a new customer.
*
* Validation chain (returns error message string on failure, null on success):
* 1. All fields must be non-null and non-blank.
* 2. Mobile must be exactly 10 numeric digits.
* 3. userId must not already exist in LOGIN.
* 4. Email must not already exist in CUSTOMER.
* 5. Mobile must not already exist in CUSTOMER.
*
* On success: saves a Customer row and a Login row (role = "Customer").
*
* @return null on success, or an error message string on validation failure.
*/
public String registerCustomer(CustomerRegistrationDTO dto) {
// ── Field presence validation ────────────────────────────────────────
if (dto.getUserId() == null || dto.getUserId().isBlank()) return "User ID is required.";
if (dto.getPassword() == null || dto.getPassword().isBlank()) return "Password is required.";
if (dto.getName() == null || dto.getName().isBlank()) return "Name is required.";
if (dto.getEmail() == null || dto.getEmail().isBlank()) return "Email is required.";
if (dto.getMobile() == null || dto.getMobile().isBlank()) return "Mobile is required.";
if (dto.getAddress() == null || dto.getAddress().isBlank()) return "Address is required.";
// ── Mobile format validation ─────────────────────────────────────────
if (!dto.getMobile().matches("\\d{10}")) {
return "Mobile number must be exactly 10 digits.";
}
// ── Uniqueness checks ────────────────────────────────────────────────
if (loginRepository.findByUserId(dto.getUserId()).isPresent()) {
return "User ID '" + dto.getUserId() + "' is already taken.";
}
if (customerRepository.findByEmail(dto.getEmail()).isPresent()) {
return "Email address '" + dto.getEmail() + "' is already registered.";
}
if (customerRepository.findByMobile(dto.getMobile()).isPresent()) {
return "Mobile number '" + dto.getMobile() + "' is already registered.";
}
// ── Generate consumerId ──────────────────────────────────────────────
String consumerId = "CUST-" + UUID.randomUUID().toString()
.replace("-", "")
.substring(0, 8)
.toUpperCase();
// ── Build and persist Customer ───────────────────────────────────────
Customer customer = new Customer();
customer.setConsumerId(consumerId);
customer.setName(dto.getName());
customer.setEmail(dto.getEmail());
customer.setMobile(dto.getMobile());
customer.setAddress(dto.getAddress());
customer.setStatus("Active");
customer.setRegistrationDate(LocalDate.now());
customerRepository.save(customer);
// ── Build and persist Login ──────────────────────────────────────────
Login login = new Login();
login.setUserId(dto.getUserId());
login.setPassword(dto.getPassword());
login.setRole("Customer");
login.setConsumerId(consumerId);
login.setStatus("Active");
loginRepository.save(login);
return null; // null == success
}
// ── Admin Registration ───────────────────────────────────────────────────
/**
* Registers a new admin account.
*
* Validation:
* 1. userId and password must be non-blank.
* 2. userId must not already exist in LOGIN.
*
* On success: saves a Login row only (role = "Admin", no Customer row).
*
* @return null on success, or an error message string on validation failure.
*/
public String registerAdmin(AdminRegistrationDTO dto) {
if (dto.getUserId() == null || dto.getUserId().isBlank()) return "User ID is required.";
if (dto.getPassword() == null || dto.getPassword().isBlank()) return "Password is required.";
if (loginRepository.findByUserId(dto.getUserId()).isPresent()) {
return "User ID '" + dto.getUserId() + "' is already taken.";
}
Login login = new Login();
login.setUserId(dto.getUserId());
login.setPassword(dto.getPassword());
login.setRole("Admin");
login.setConsumerId(null);
login.setStatus("Active");
loginRepository.save(login);
return null; // null == success
}
// ── Login Validation ─────────────────────────────────────────────────────
/**
* Validates a login attempt by matching userId and password.
*
* @return The Login entity on success, or null if credentials are invalid.
*/
public Login validateLogin(LoginDTO dto) {
Optional<Login> loginOpt = loginRepository.findByUserId(dto.getUserId());
if (loginOpt.isEmpty()) return null;
Login login = loginOpt.get();
if (!login.getPassword().equals(dto.getPassword())) return null;
return login;
}
// ── Customer Profile ─────────────────────────────────────────────────────
/**
* Retrieves a customer by their consumerId.
*
* @return The Customer entity, or null if not found.
*/
public Customer getCustomerById(String consumerId) {
return customerRepository.findById(consumerId).orElse(null);
}
}
11.2 BookingService.java
src/main/java/com/parcel/service/BookingService.java
Handles all booking-related operations.
Responsibilities:
- Create new bookings with auto-generated booking IDs.
- Track parcels by
bookingIdorconsumerId. - Update pickup and drop-off times (admin operation).
- Update delivery status (admin operation).
- Retrieve all bookings (admin dashboard).
- Retrieve bookings filtered by date (history view).
package com.parcel.service;
import com.parcel.dto.*;
import com.parcel.model.Booking;
import com.parcel.repository.BookingRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Service
public class BookingService {
@Autowired
private BookingRepository bookingRepository;
// ── Create Booking ───────────────────────────────────────────────────────
/**
* Creates a new parcel booking.
*
* Auto-generates a bookingId in the format BK-XXXXXXXX.
* Sets bookingDate to today and deliveryStatus to "Booked".
*
* @return The saved Booking entity.
*/
public Booking createBooking(BookingDTO dto) {
String bookingId = "BK-" + UUID.randomUUID().toString()
.replace("-", "")
.substring(0, 8)
.toUpperCase();
Booking booking = new Booking();
booking.setBookingId(bookingId);
booking.setConsumerId(dto.getConsumerId());
booking.setName(dto.getName());
booking.setAddress(dto.getAddress());
booking.setContactDetails(dto.getContactDetails());
booking.setRecName(dto.getRecName());
booking.setRecAddress(dto.getRecAddress());
booking.setRecPin(dto.getRecPin());
booking.setRecMobile(dto.getRecMobile());
booking.setParWeightGram(dto.getParWeightGram());
booking.setParContentsDescription(dto.getParContentsDescription());
booking.setParDeliveryType(dto.getParDeliveryType());
booking.setParPackingPreference(dto.getParPackingPreference());
booking.setParPickupTime(dto.getParPickupTime());
booking.setParDropoffTime(dto.getParDropoffTime());
booking.setDeliveryStatus("Booked");
booking.setBookingDate(LocalDate.now());
return bookingRepository.save(booking);
}
// ── Track Parcel ─────────────────────────────────────────────────────────
/**
* Tracks parcels by either bookingId or consumerId.
*
* If bookingId is provided: returns a list with the single matching booking.
* If consumerId is provided: returns all bookings for that customer.
* If neither is provided: returns an empty list.
*
* @param bookingId (nullable) the booking reference number
* @param consumerId (nullable) the customer ID
* @return List of matching bookings (may be empty, never null)
*/
public List<Booking> trackParcel(String bookingId, String consumerId) {
if (bookingId != null && !bookingId.isBlank()) {
return bookingRepository.findById(bookingId)
.map(List::of)
.orElse(List.of());
}
if (consumerId != null && !consumerId.isBlank()) {
return bookingRepository.findByConsumerId(consumerId);
}
return List.of();
}
// ── Update Pickup and Drop Times ─────────────────────────────────────────
/**
* Admin: Updates confirmed pickup and/or drop-off timestamps on a booking.
*
* Only updates fields that are provided (non-null) in the DTO.
*
* @return The updated Booking, or null if the bookingId does not exist.
*/
public Booking updatePickupAndDrop(String bookingId, PickupDropDTO dto) {
Optional<Booking> opt = bookingRepository.findById(bookingId);
if (opt.isEmpty()) return null;
Booking booking = opt.get();
if (dto.getPickupUpdatedTime() != null) {
booking.setPickupUpdatedTime(dto.getPickupUpdatedTime());
}
if (dto.getDropUpdatedTime() != null) {
booking.setDropUpdatedTime(dto.getDropUpdatedTime());
}
return bookingRepository.save(booking);
}
// ── Update Delivery Status ───────────────────────────────────────────────
/**
* Admin: Updates the delivery status of a booking.
*
* @return The updated Booking, or null if the bookingId does not exist.
*/
public Booking updateDeliveryStatus(String bookingId, DeliveryStatusDTO dto) {
Optional<Booking> opt = bookingRepository.findById(bookingId);
if (opt.isEmpty()) return null;
Booking booking = opt.get();
booking.setDeliveryStatus(dto.getDeliveryStatus());
return bookingRepository.save(booking);
}
// ── Get All Bookings ─────────────────────────────────────────────────────
/**
* Retrieves every booking in the system (admin dashboard view).
*
* @return List of all Booking entities.
*/
public List<Booking> getAllBookings() {
return bookingRepository.findAll();
}
// ── Get Booking History by Date ──────────────────────────────────────────
/**
* Retrieves bookings filtered by booking date.
*
* @param date The date to filter on (ISO format: yyyy-MM-dd).
* @return List of bookings placed on that date.
*/
public List<Booking> getBookingsByDate(LocalDate date) {
return bookingRepository.findByBookingDate(date);
}
}
11.3 SchedulerService.java
src/main/java/com/parcel/service/SchedulerService.java
Runs an automated daily job at midnight that inactivates customers who registered more than 15 days ago and have never placed a booking.
Why this matters: Keeping inactive accounts in an "Active" state would pollute analytics and customer communications. The scheduler enforces a "use it or lose it" policy automatically without requiring admin intervention.
How it works:
- Find all
Activecustomers withregistrationDateolder than 15 days (LocalDate.now().minusDays(15)). - For each such customer, count their bookings using
BookingRepository.countByConsumerId. - If the count is 0, set
Customer.status = "Inactive"and also update the correspondingLogin.status = "Inactive". - Save both records.
package com.parcel.service;
import com.parcel.model.Customer;
import com.parcel.model.Login;
import com.parcel.repository.BookingRepository;
import com.parcel.repository.CustomerRepository;
import com.parcel.repository.LoginRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
@Service
public class SchedulerService {
@Autowired
private CustomerRepository customerRepository;
@Autowired
private LoginRepository loginRepository;
@Autowired
private BookingRepository bookingRepository;
/**
* Auto-inactivation job.
*
* Runs every day at midnight (cron: "0 0 0 * * *").
*
* Logic:
* - Query: Active customers whose registrationDate is before (today - 15 days)
* - For each: if booking count == 0 → set Customer.status and Login.status to "Inactive"
*
* The @Scheduled annotation requires @EnableScheduling on the main application class.
*/
@Scheduled(cron = "0 0 0 * * *")
public void inactivateDormantCustomers() {
LocalDate cutoffDate = LocalDate.now().minusDays(15);
// Find active customers registered more than 15 days ago
List<Customer> candidates = customerRepository
.findByStatusAndRegistrationDateBefore("Active", cutoffDate);
System.out.println("[Scheduler] Checking " + candidates.size()
+ " active customer(s) registered before " + cutoffDate);
for (Customer customer : candidates) {
long bookingCount = bookingRepository.countByConsumerId(customer.getConsumerId());
if (bookingCount == 0) {
// Inactivate the customer record
customer.setStatus("Inactive");
customerRepository.save(customer);
// Inactivate the linked login record
Optional<Login> loginOpt = loginRepository.findByConsumerId(customer.getConsumerId());
loginOpt.ifPresent(login -> {
login.setStatus("Inactive");
loginRepository.save(login);
});
System.out.println("[Scheduler] Inactivated: " + customer.getConsumerId()
+ " (" + customer.getName() + ") — no bookings in 15+ days.");
}
}
System.out.println("[Scheduler] Auto-inactivation job completed.");
}
}
Cron Expression Reference:
| Expression | Meaning |
|---|---|
0 0 0 * * * |
Every day at 00:00:00 (midnight) |
0 0 2 * * * |
Every day at 02:00:00 (2 AM) |
0 */30 * * * * |
Every 30 minutes (for testing) |
0 0 * * * * |
Every hour on the hour |
Tip: To test the scheduler without waiting until midnight, temporarily change the cron to
"0 */1 * * * *"(every minute), verify it works, then restore"0 0 0 * * *".
12. Controllers
Controllers handle HTTP routing. They parse incoming requests, delegate to the appropriate service method, and wrap results in ApiResponse.
12.1 AuthController.java
src/main/java/com/parcel/controller/AuthController.java
API Endpoints
| Method | Endpoint | Request Body | Description |
|---|---|---|---|
POST |
/api/auth/registerCustomer |
CustomerRegistrationDTO |
Register a new customer |
POST |
/api/auth/registerAdmin |
AdminRegistrationDTO |
Register a new admin |
POST |
/api/auth/validateLogin |
LoginDTO |
Validate credentials |
GET |
/api/auth/customer/{consumerId} |
— | Get customer profile by ID |
package com.parcel.controller;
import com.parcel.dto.*;
import com.parcel.model.Customer;
import com.parcel.model.Login;
import com.parcel.service.AuthService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthService authService;
// ── POST /api/auth/registerCustomer ──────────────────────────────────────
/**
* Registers a new customer.
*
* Success: 200 OK + ApiResponse{ status:"success", data: null }
* Failure: 400 BAD REQUEST + ApiResponse{ status:"error", message: "<reason>" }
*/
@PostMapping("/registerCustomer")
public ResponseEntity<ApiResponse<Void>> registerCustomer(
@RequestBody CustomerRegistrationDTO dto) {
String error = authService.registerCustomer(dto);
if (error != null) {
return ResponseEntity.badRequest()
.body(ApiResponse.error(error));
}
return ResponseEntity.ok(
ApiResponse.success("Customer registered successfully."));
}
// ── POST /api/auth/registerAdmin ─────────────────────────────────────────
/**
* Registers a new admin account.
*
* Success: 200 OK + ApiResponse{ status:"success" }
* Failure: 400 BAD REQUEST + ApiResponse{ status:"error", message: "<reason>" }
*/
@PostMapping("/registerAdmin")
public ResponseEntity<ApiResponse<Void>> registerAdmin(
@RequestBody AdminRegistrationDTO dto) {
String error = authService.registerAdmin(dto);
if (error != null) {
return ResponseEntity.badRequest()
.body(ApiResponse.error(error));
}
return ResponseEntity.ok(
ApiResponse.success("Admin registered successfully."));
}
// ── POST /api/auth/validateLogin ─────────────────────────────────────────
/**
* Validates a login request.
*
* Success: 200 OK + ApiResponse{ status:"success", data: Login }
* Failure: 401 UNAUTHORIZED + ApiResponse{ status:"error", message: "Invalid credentials" }
*/
@PostMapping("/validateLogin")
public ResponseEntity<ApiResponse<Login>> validateLogin(
@RequestBody LoginDTO dto) {
Login login = authService.validateLogin(dto);
if (login == null) {
return ResponseEntity.status(401)
.body(ApiResponse.error("Invalid user ID or password."));
}
return ResponseEntity.ok(
ApiResponse.success("Login successful.", login));
}
// ── GET /api/auth/customer/{consumerId} ──────────────────────────────────
/**
* Retrieves a customer profile by consumerId.
*
* Success: 200 OK + ApiResponse{ status:"success", data: Customer }
* Failure: 404 NOT FOUND + ApiResponse{ status:"error", message: "..." }
*/
@GetMapping("/customer/{consumerId}")
public ResponseEntity<ApiResponse<Customer>> getCustomer(
@PathVariable String consumerId) {
Customer customer = authService.getCustomerById(consumerId);
if (customer == null) {
return ResponseEntity.status(404)
.body(ApiResponse.error("Customer not found: " + consumerId));
}
return ResponseEntity.ok(
ApiResponse.success("Customer found.", customer));
}
}
12.2 BookingController.java
src/main/java/com/parcel/controller/BookingController.java
API Endpoints
| Method | Endpoint | Request Body / Params | Description |
|---|---|---|---|
POST |
/api/booking/viewbookService |
BookingDTO |
Create a new booking |
GET |
/api/booking/viewbookService |
— | Get all bookings (admin) |
GET |
/api/booking/trackParcelStatus |
?bookingId= or ?consumerId= |
Track parcel |
PUT |
/api/booking/updatepickupanddrop/{bookingId} |
PickupDropDTO |
Update pickup/drop times (admin) |
PUT |
/api/booking/updateDeliveryStatus/{bookingId} |
DeliveryStatusDTO |
Update delivery status (admin) |
GET |
/api/booking/viewBookingHistory |
?date=yyyy-MM-dd |
Get bookings by date |
package com.parcel.controller;
import com.parcel.dto.*;
import com.parcel.model.Booking;
import com.parcel.service.BookingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
@RestController
@RequestMapping("/api/booking")
public class BookingController {
@Autowired
private BookingService bookingService;
// ── POST /api/booking/viewbookService ────────────────────────────────────
/**
* Creates a new parcel booking.
*
* Success: 200 OK + ApiResponse{ status:"success", data: Booking }
*/
@PostMapping("/viewbookService")
public ResponseEntity<ApiResponse<Booking>> createBooking(
@RequestBody BookingDTO dto) {
Booking booking = bookingService.createBooking(dto);
return ResponseEntity.ok(
ApiResponse.success("Booking created successfully.", booking));
}
// ── GET /api/booking/viewbookService ─────────────────────────────────────
/**
* Retrieves all bookings in the system (admin dashboard).
*
* Success: 200 OK + ApiResponse{ status:"success", data: [Booking, ...] }
*/
@GetMapping("/viewbookService")
public ResponseEntity<ApiResponse<List<Booking>>> getAllBookings() {
List<Booking> bookings = bookingService.getAllBookings();
return ResponseEntity.ok(
ApiResponse.success("Bookings retrieved.", bookings));
}
// ── GET /api/booking/trackParcelStatus ───────────────────────────────────
/**
* Tracks parcels by bookingId or consumerId query parameter.
*
* Usage:
* GET /api/booking/trackParcelStatus?bookingId=BK-ABCD1234
* GET /api/booking/trackParcelStatus?consumerId=CUST-ABCD1234
*
* Success: 200 OK + ApiResponse{ data: [Booking, ...] }
* Not Found: 404 + ApiResponse{ status:"error" }
*/
@GetMapping("/trackParcelStatus")
public ResponseEntity<ApiResponse<List<Booking>>> trackParcel(
@RequestParam(required = false) String bookingId,
@RequestParam(required = false) String consumerId) {
List<Booking> results = bookingService.trackParcel(bookingId, consumerId);
if (results.isEmpty()) {
return ResponseEntity.status(404)
.body(ApiResponse.error("No bookings found for the given criteria."));
}
return ResponseEntity.ok(
ApiResponse.success("Parcel(s) found.", results));
}
// ── PUT /api/booking/updatepickupanddrop/{bookingId} ─────────────────────
/**
* Admin: Updates confirmed pickup and/or drop-off times on a booking.
*
* Success: 200 OK + ApiResponse{ data: Booking }
* Not Found: 404 + ApiResponse{ status:"error" }
*/
@PutMapping("/updatepickupanddrop/{bookingId}")
public ResponseEntity<ApiResponse<Booking>> updatePickupAndDrop(
@PathVariable String bookingId,
@RequestBody PickupDropDTO dto) {
Booking updated = bookingService.updatePickupAndDrop(bookingId, dto);
if (updated == null) {
return ResponseEntity.status(404)
.body(ApiResponse.error("Booking not found: " + bookingId));
}
return ResponseEntity.ok(
ApiResponse.success("Pickup/drop times updated.", updated));
}
// ── PUT /api/booking/updateDeliveryStatus/{bookingId} ────────────────────
/**
* Admin: Updates the delivery status of a booking.
*
* Success: 200 OK + ApiResponse{ data: Booking }
* Not Found: 404 + ApiResponse{ status:"error" }
*/
@PutMapping("/updateDeliveryStatus/{bookingId}")
public ResponseEntity<ApiResponse<Booking>> updateDeliveryStatus(
@PathVariable String bookingId,
@RequestBody DeliveryStatusDTO dto) {
Booking updated = bookingService.updateDeliveryStatus(bookingId, dto);
if (updated == null) {
return ResponseEntity.status(404)
.body(ApiResponse.error("Booking not found: " + bookingId));
}
return ResponseEntity.ok(
ApiResponse.success("Delivery status updated.", updated));
}
// ── GET /api/booking/viewBookingHistory ──────────────────────────────────
/**
* Retrieves all bookings for a given date.
*
* Usage: GET /api/booking/viewBookingHistory?date=2024-06-15
*
* Success: 200 OK + ApiResponse{ data: [Booking, ...] }
*/
@GetMapping("/viewBookingHistory")
public ResponseEntity<ApiResponse<List<Booking>>> getBookingHistory(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
List<Booking> bookings = bookingService.getBookingsByDate(date);
return ResponseEntity.ok(
ApiResponse.success("Booking history for " + date, bookings));
}
}
13. CORS Configuration
src/main/java/com/parcel/config/CorsConfig.java
Cross-Origin Resource Sharing (CORS) must be configured to allow the Angular frontend (http://localhost:4200) to make API calls to the Spring Boot backend (http://localhost:8080). Without this, browsers will block all cross-origin requests.
package com.parcel.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Global CORS configuration.
*
* Permits the Angular dev server (http://localhost:4200) to call all
* /api/** endpoints using the standard HTTP methods.
*
* In production, replace "http://localhost:4200" with your actual
* frontend domain (e.g. "https://parcelexpress.example.com").
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:4200")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(false);
}
}
Configuration Breakdown:
| Setting | Value | Reason |
|---|---|---|
addMapping |
/api/** |
Apply CORS rules to all API routes |
allowedOrigins |
http://localhost:4200 |
Angular dev server |
allowedMethods |
GET, POST, PUT, DELETE, OPTIONS |
All methods used by the API; OPTIONS is required for preflight |
allowedHeaders |
* |
Accept any headers (including Content-Type, Authorization) |
allowCredentials |
false |
No cookies or HTTP auth are used |
14. Database Schema
Hibernate auto-creates these tables on first run based on the entity annotations. Below are the equivalent DDL definitions for reference.
CUSTOMER Table
CREATE TABLE CUSTOMER (
CONSUMER_ID VARCHAR(20) NOT NULL,
NAME VARCHAR(100) NOT NULL,
EMAIL VARCHAR(100) NOT NULL,
MOBILE VARCHAR(15) NOT NULL,
ADDRESS VARCHAR(500) NOT NULL,
STATUS VARCHAR(20),
REGISTRATION_DATE DATE,
CONSTRAINT PK_CUSTOMER PRIMARY KEY (CONSUMER_ID),
CONSTRAINT UK_CUSTOMER_EMAIL UNIQUE (EMAIL),
CONSTRAINT UK_CUSTOMER_MOBILE UNIQUE (MOBILE)
);
LOGIN Table
CREATE TABLE LOGIN (
USER_ID VARCHAR(50) NOT NULL,
PASSWORD VARCHAR(100),
ROLE VARCHAR(20),
CONSUMER_ID VARCHAR(20),
STATUS VARCHAR(20),
CONSTRAINT PK_LOGIN PRIMARY KEY (USER_ID)
);
BOOKING Table
CREATE TABLE BOOKING (
BOOKING_ID VARCHAR(20) NOT NULL,
CONSUMER_ID VARCHAR(20),
NAME VARCHAR(100),
ADDRESS VARCHAR(500),
CONTACT_DETAILS VARCHAR(100),
REC_NAME VARCHAR(100),
REC_ADDRESS VARCHAR(500),
REC_PIN VARCHAR(10),
REC_MOBILE VARCHAR(15),
PAR_WEIGHT_GRAM DOUBLE,
PAR_CONTENTS_DESCRIPTION VARCHAR(300),
PAR_DELIVERY_TYPE VARCHAR(50),
PAR_PACKING_PREFERENCE VARCHAR(50),
PAR_PICKUP_TIME VARCHAR(100),
PAR_DROPOFF_TIME VARCHAR(100),
PICKUP_UPDATED_TIME TIMESTAMP,
DROP_UPDATED_TIME TIMESTAMP,
DELIVERY_STATUS VARCHAR(50),
BOOKING_DATE DATE,
CONSTRAINT PK_BOOKING PRIMARY KEY (BOOKING_ID)
);
Entity Relationship Diagram
CUSTOMER LOGIN
────────────────── ──────────────────────
CONSUMER_ID (PK) ◄──────── CONSUMER_ID (logical FK)
NAME USER_ID (PK)
EMAIL (UNIQUE) PASSWORD
MOBILE (UNIQUE) ROLE
ADDRESS STATUS
STATUS
REGISTRATION_DATE
BOOKING
──────────────────────────
BOOKING_ID (PK)
CONSUMER_ID (logical FK) ──► CUSTOMER.CONSUMER_ID
NAME
ADDRESS
...all parcel fields...
DELIVERY_STATUS
BOOKING_DATE
Note: The foreign key relationships are logical (enforced in application code), not physical DB-level
FOREIGN KEYconstraints. This is common in embedded Derby setups for simplicity and performance.
15. Running the Application
Step 1 — Clone / Navigate to the Project
cd parcel-booking-backend
Step 2 — Build and Run
# Compile, test, and start the application
mvn clean spring-boot:run
On first run, Spring Boot will:
- Start the embedded Apache Derby engine.
- Create the
parceldb/directory in the project root. - Create
CUSTOMER,LOGIN, andBOOKINGtables automatically. - Start the embedded Tomcat server on port
8080.
You should see output similar to:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.5)
... Hibernate: create table CUSTOMER ...
... Hibernate: create table LOGIN ...
... Hibernate: create table BOOKING ...
... Started ParcelBookingApplication in 4.321 seconds ...
Step 3 — Verify the Application is Running
curl http://localhost:8080/api/booking/viewbookService
Expected response:
{
"status": "success",
"message": "Bookings retrieved.",
"data": []
}
Building a Deployable JAR
# Package the application (skipping tests for speed)
mvn clean package -DskipTests
# Run the packaged JAR
java -jar target/parcel-booking-backend-0.0.1-SNAPSHOT.jar
Resetting the Database
The Derby database is stored as files in parceldb/. To completely reset:
Windows (PowerShell or CMD):
# Stop the application first, then:
Remove-Item -Recurse -Force parceldb
# Or using CMD:
rd /s /q parceldb
# Restart the application to recreate everything fresh
mvn clean spring-boot:run
Linux / macOS:
rm -rf parceldb
mvn clean spring-boot:run
Warning: Deleting
parceldb/permanently destroys all data. Only do this in development.
16. API Request / Response Examples
All endpoints return JSON in the ApiResponse envelope format:
{
"status": "success | error",
"message": "Human-readable message",
"data": { ... } | null
}
Register a Customer
Request:
curl -X POST http://localhost:8080/api/auth/registerCustomer \
-H "Content-Type: application/json" \
-d '{
"userId": "john.doe@example.com",
"password": "secret123",
"name": "John Doe",
"email": "john.doe@example.com",
"mobile": "9876543210",
"address": "42 Elm Street, Springfield, 560001"
}'
Success Response (200 OK):
{
"status": "success",
"message": "Customer registered successfully.",
"data": null
}
Failure Response (400 Bad Request) — duplicate email:
{
"status": "error",
"message": "Email address 'john.doe@example.com' is already registered.",
"data": null
}
Register an Admin
Request:
curl -X POST http://localhost:8080/api/auth/registerAdmin \
-H "Content-Type: application/json" \
-d '{
"userId": "admin01",
"password": "adminpass",
"name": "Super Admin"
}'
Success Response (200 OK):
{
"status": "success",
"message": "Admin registered successfully.",
"data": null
}
Login
Request:
curl -X POST http://localhost:8080/api/auth/validateLogin \
-H "Content-Type: application/json" \
-d '{
"userId": "john.doe@example.com",
"password": "secret123"
}'
Success Response (200 OK):
{
"status": "success",
"message": "Login successful.",
"data": {
"userId": "john.doe@example.com",
"password": "secret123",
"role": "Customer",
"consumerId": "CUST-A1B2C3D4",
"status": "Active"
}
}
Failure Response (401 Unauthorized):
{
"status": "error",
"message": "Invalid user ID or password.",
"data": null
}
Get Customer Profile
Request:
curl http://localhost:8080/api/auth/customer/CUST-A1B2C3D4
Success Response (200 OK):
{
"status": "success",
"message": "Customer found.",
"data": {
"consumerId": "CUST-A1B2C3D4",
"name": "John Doe",
"email": "john.doe@example.com",
"mobile": "9876543210",
"address": "42 Elm Street, Springfield, 560001",
"status": "Active",
"registrationDate": "2024-06-10"
}
}
Failure Response (404 Not Found):
{
"status": "error",
"message": "Customer not found: CUST-XXXXXXXX",
"data": null
}
Create a Booking
Request:
curl -X POST http://localhost:8080/api/booking/viewbookService \
-H "Content-Type: application/json" \
-d '{
"consumerId": "CUST-A1B2C3D4",
"name": "John Doe",
"address": "42 Elm Street, Springfield, 560001",
"contactDetails": "9876543210",
"recName": "Jane Smith",
"recAddress": "10 Oak Avenue, Shelbyville, 560002",
"recPin": "560002",
"recMobile": "9123456780",
"parWeightGram": 1500,
"parContentsDescription": "Books and stationery",
"parDeliveryType": "Express",
"parPackingPreference": "Standard",
"parPickupTime": "Morning (9am-12pm)",
"parDropoffTime": "Afternoon (1pm-5pm)"
}'
Success Response (200 OK):
{
"status": "success",
"message": "Booking created successfully.",
"data": {
"bookingId": "BK-E5F6G7H8",
"consumerId": "CUST-A1B2C3D4",
"name": "John Doe",
"address": "42 Elm Street, Springfield, 560001",
"contactDetails": "9876543210",
"recName": "Jane Smith",
"recAddress": "10 Oak Avenue, Shelbyville, 560002",
"recPin": "560002",
"recMobile": "9123456780",
"parWeightGram": 1500.0,
"parContentsDescription": "Books and stationery",
"parDeliveryType": "Express",
"parPackingPreference": "Standard",
"parPickupTime": "Morning (9am-12pm)",
"parDropoffTime": "Afternoon (1pm-5pm)",
"pickupUpdatedTime": null,
"dropUpdatedTime": null,
"deliveryStatus": "Booked",
"bookingDate": "2024-06-15"
}
}
Track a Parcel by Booking ID
Request:
curl "http://localhost:8080/api/booking/trackParcelStatus?bookingId=BK-E5F6G7H8"
Success Response (200 OK):
{
"status": "success",
"message": "Parcel(s) found.",
"data": [
{
"bookingId": "BK-E5F6G7H8",
"consumerId": "CUST-A1B2C3D4",
"deliveryStatus": "In Transit",
"bookingDate": "2024-06-15"
}
]
}
Track by Customer ID:
curl "http://localhost:8080/api/booking/trackParcelStatus?consumerId=CUST-A1B2C3D4"
Update Pickup and Drop Times (Admin)
Request:
curl -X PUT \
http://localhost:8080/api/booking/updatepickupanddrop/BK-E5F6G7H8 \
-H "Content-Type: application/json" \
-d '{
"pickupUpdatedTime": "2024-06-16T09:30:00",
"dropUpdatedTime": "2024-06-17T14:00:00"
}'
Success Response (200 OK):
{
"status": "success",
"message": "Pickup/drop times updated.",
"data": {
"bookingId": "BK-E5F6G7H8",
"pickupUpdatedTime": "2024-06-16T09:30:00",
"dropUpdatedTime": "2024-06-17T14:00:00",
"deliveryStatus": "Booked"
}
}
Update Delivery Status (Admin)
Request:
curl -X PUT \
http://localhost:8080/api/booking/updateDeliveryStatus/BK-E5F6G7H8 \
-H "Content-Type: application/json" \
-d '{
"deliveryStatus": "Out for Delivery"
}'
Success Response (200 OK):
{
"status": "success",
"message": "Delivery status updated.",
"data": {
"bookingId": "BK-E5F6G7H8",
"deliveryStatus": "Out for Delivery"
}
}
Get All Bookings (Admin)
Request:
curl http://localhost:8080/api/booking/viewbookService
Success Response (200 OK):
{
"status": "success",
"message": "Bookings retrieved.",
"data": [
{ "bookingId": "BK-E5F6G7H8", "deliveryStatus": "In Transit", ... },
{ "bookingId": "BK-I9J0K1L2", "deliveryStatus": "Delivered", ... }
]
}
Get Booking History by Date
Request:
curl "http://localhost:8080/api/booking/viewBookingHistory?date=2024-06-15"
Success Response (200 OK):
{
"status": "success",
"message": "Booking history for 2024-06-15",
"data": [
{ "bookingId": "BK-E5F6G7H8", "bookingDate": "2024-06-15", ... }
]
}
17. Business Rules
The following rules are enforced by the service layer (not the frontend).
Customer Registration Rules
| Rule | Implementation |
|---|---|
| All fields (userId, password, name, email, mobile, address) are mandatory | Blank/null check in AuthService.registerCustomer() |
| Mobile must be exactly 10 numeric digits | Regex: \d{10} in AuthService |
| Email must be globally unique | CustomerRepository.findByEmail() before save |
| Mobile must be globally unique | CustomerRepository.findByMobile() before save |
| userId must be globally unique | LoginRepository.findByUserId() before save |
| Customer ID is auto-generated | Format: CUST- + 8-char UUID fragment (uppercase) |
| New customers are Active by default | customer.setStatus("Active") in service |
| Registration date is set to today | customer.setRegistrationDate(LocalDate.now()) |
| A Login row is also created on customer registration | Both customerRepository.save() and loginRepository.save() are called |
Admin Registration Rules
| Rule | Implementation |
|---|---|
| userId and password are mandatory | Blank/null check in AuthService.registerAdmin() |
| userId must be unique | LoginRepository.findByUserId() before save |
| No Customer row is created for admins | Only loginRepository.save() is called |
Admin role is set to "Admin" |
login.setRole("Admin") |
Booking Rules
| Rule | Implementation |
|---|---|
| Booking ID is auto-generated | Format: BK- + 8-char UUID fragment (uppercase) |
Initial delivery status is "Booked" |
booking.setDeliveryStatus("Booked") in service |
| Booking date is set to today | booking.setBookingDate(LocalDate.now()) |
| Only admins update pickup/drop times | Frontend enforces; backend has no role check on the PUT endpoints |
| Only admins update delivery status | Frontend enforces; backend has no role check on the PUT endpoints |
| Admins cannot place bookings | Frontend-enforced only — no backend restriction on the booking endpoint |
Auto-Inactivation Rules
| Rule | Implementation |
|---|---|
| Runs daily at midnight | @Scheduled(cron = "0 0 0 * * *") in SchedulerService |
| Targets Active customers only | findByStatusAndRegistrationDateBefore("Active", cutoffDate) |
| Customer must be registered more than 15 days ago | cutoffDate = LocalDate.now().minusDays(15) |
| Customer must have zero bookings | bookingRepository.countByConsumerId(id) == 0 |
Both CUSTOMER.STATUS and LOGIN.STATUS are updated |
Both entities are saved with "Inactive" |
Password Rules
Security Notice: Passwords are currently stored and compared as plain text. This is suitable for a learning/prototype environment only. For any production deployment, passwords must be hashed using a strong one-way algorithm (e.g., BCrypt via Spring Security's
BCryptPasswordEncoder).
18. Error Handling Reference
HTTP Status Codes Used
| Status Code | Meaning | When Used |
|---|---|---|
200 OK |
Request succeeded | All successful operations |
400 Bad Request |
Client-side validation failure | Missing fields, invalid mobile, duplicate email/mobile |
401 Unauthorized |
Authentication failure | Invalid credentials in /validateLogin |
404 Not Found |
Resource does not exist | Customer or booking not found |
500 Internal Server Error |
Unhandled server exception | Unexpected errors (Derby connectivity issues, etc.) |
Common Validation Errors
| Scenario | HTTP Status | message Field |
|---|---|---|
| Missing required field | 400 | "Name is required." |
| Invalid mobile format | 400 | "Mobile number must be exactly 10 digits." |
| Duplicate userId | 400 | "User ID '...' is already taken." |
| Duplicate email | 400 | "Email address '...' is already registered." |
| Duplicate mobile | 400 | "Mobile number '...' is already registered." |
| Wrong password | 401 | "Invalid user ID or password." |
| Booking not found | 404 | "Booking not found: BK-XXXXXXXX" |
| Customer not found | 404 | "Customer not found: CUST-XXXXXXXX" |
| No parcels match query | 404 | "No bookings found for the given criteria." |
19. Troubleshooting
Application Won't Start — Derby Lock Error
ERROR: Another instance of Derby may have shut down abnormally
Cause: A previous Derby instance crashed and left a lock file behind.
Fix:
# Stop any running instances, then delete the lock file:
del parceldb\db.lck # Windows
rm parceldb/db.lck # Linux/Mac
# Restart the application
mvn spring-boot:run
Port 8080 Already in Use
ERROR: Address already in use: bind
Fix (Windows — find and kill the process):
# Find the PID using port 8080
netstat -ano | findstr :8080
# Kill the process (replace <PID> with actual PID)
taskkill /PID <PID> /F
Or change the port in application.properties:
server.port=8081
Hibernate Tables Not Created / Schema Out of Date
Symptom: SQL errors like Table 'CUSTOMER' does not exist.
Fix: Ensure ddl-auto=update is set. If the schema is corrupted, reset the database:
rd /s /q parceldb # Windows
mvn clean spring-boot:run
CORS Error in Browser Console
Access to XMLHttpRequest at 'http://localhost:8080/api/...' from origin 'http://localhost:4200'
has been blocked by CORS policy
Fix: Confirm CorsConfig.java is present and the @Configuration annotation is not missing. Confirm the Angular dev server is on port 4200. Restart the backend.
@Scheduled Job Not Running
Symptom: The auto-inactivation log lines never appear.
Fix: Ensure @EnableScheduling is present on ParcelBookingApplication:
@SpringBootApplication
@EnableScheduling // ← This must be present
public class ParcelBookingApplication { ... }
Checking Derby Database Directly (Optional)
Apache Derby ships with an interactive SQL tool called ij. You can use it to inspect your database:
# From the project root
java -jar ~/.m2/repository/org/apache/derby/derby/10.16.1.1/derby-10.16.1.1.jar
# Inside ij:
connect 'jdbc:derby:parceldb';
show tables;
select * from CUSTOMER;
select * from LOGIN;
select * from BOOKING;
exit;
End of ParcelExpress Backend Documentation
Document maintained by the ParcelExpress development team.
For questions or issues, refer to the project repository or raise a ticket with your team lead.
메타데이터
- post_id
- 03a110e2c323
- slug
- parcel-booking-backend-03a110e2c323
- url
- https://medium.com/@LordKage/parcel-booking-backend-03a110e2c323
- canonical_url
- https://medium.com/@LordKage/parcel-booking-backend-03a110e2c323
- author_url
- https://medium.com/@LordKage
- status
- ok
- fetched_at
- 2026-06-16 19:09:56