← Back to list

Parcel Booking backend

STRIKERFLASH · 2026-05-23 20:33 · 3 claps · 37.6 min read
#java #spring-boot #coding
Open on Medium ↗
Wiki topics: 💻 · Programming 🌐 · Web Development

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 @Scheduled methods in the application (like the auto-inactivation job in SchedulerService) 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 registrationDate to today's date.
  • Create both a Customer and a Login row atomically for customer registrations.
  • Create only a Login row (with role "Admin") for admin registrations.
  • Validate login by matching userId and password from the LOGIN table.
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 bookingId or consumerId.
  • 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:

  1. Find all Active customers with registrationDate older than 15 days (LocalDate.now().minusDays(15)).
  2. For each such customer, count their bookings using BookingRepository.countByConsumerId.
  3. If the count is 0, set Customer.status = "Inactive" and also update the corresponding Login.status = "Inactive".
  4. 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 KEY constraints. 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:

  1. Start the embedded Apache Derby engine.
  2. Create the parceldb/ directory in the project root.
  3. Create CUSTOMER, LOGIN, and BOOKING tables automatically.
  4. 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