Skip to content

Repository files navigation

Wassalha — Backend

Spring Boot 3.3 / Java 21 backend for Wassalha, a peer-to-peer shipping marketplace that connects shippers (people who need something delivered) with carriers (drivers who already travel that route). The platform handles dynamic ML-based pricing, real-time bidding, geographic dispatch, live tracking, in-app chat, payments, and a full admin portal.

Java Spring Boot PostgreSQL Redis


Table of Contents

  1. What it does
  2. Stack
  3. Architecture at a glance
  4. Project layout
  5. Modules
  6. User roles
  7. Domain model
  8. Order lifecycle
  9. Key flows
  10. REST API surface
  11. Real-time (WebSocket) endpoints
  12. External integrations
  13. Cross-cutting concerns
  14. Scheduled jobs
  15. Resilience
  16. Redis key reference
  17. API response shape
  18. Running locally
  19. Configuration
  20. API docs

What it does

A shipper opens the app, describes a package (item, dimensions, weight, pickup + drop address), and gets an ML-predicted price range in seconds — adjusted for distance, day of week, weather, and traffic zone. The order is published to nearby carriers (filtered by geographic zone in Redis). Carriers place bids; the shipper accepts one, an atomic claim removes the order from dispatch, and a live tracking session opens over WebSocket. Pickup and drop are confirmed with an OTP/QR. Payment is settled via wallet + Stripe, with built-in reconciliation. Throughout, a separate animal-image checker screens uploaded photos to refuse livestock shipments.


Stack

Layer Choice
Language Java 21
Framework Spring Boot 3.3.0
Build Maven (wrapper included: mvnw / mvnw.cmd)
Persistence Spring Data JPA + Hibernate 6.5 on PostgreSQL
Cache / coordination Redis (Lettuce), ShedLock, Caffeine
HTTP client Spring WebFlux WebClient (Reactor Netty)
Real-time Spring WebSocket + STOMP (SockJS fallback)
Security Spring Security + JWT, Google OAuth, BCrypt
Resilience Resilience4j (circuit breakers, retries, timeouts)
Rate limiting Bucket4j on Redis
Mapping MapStruct
Validation Jakarta Bean Validation
Docs springdoc-openapi (Swagger UI)
Money BigDecimal (scale 2, HALF_UP)
Container Docker (multi-stage build, Alpine JRE 21)

Architecture at a glance

┌────────────────────────────────────────────────────────────────────────────┐
│  Mobile / Web client                                                       │
└──────────────┬───────────────────────────┬─────────────────────────────────┘
               │ REST + JWT                │ WebSocket (STOMP/SockJS)
               ▼                           ▼
┌────────────────────────────────────────────────────────────────────────────┐
│  Spring Boot backend (this repo)                                           │
│                                                                            │
│  Auth ─ Orders ─ Pricing ─ Dispatch ─ Bidding ─ Payment ─ Chat ─ Tracking  │
│           │           │         │         │                                │
│           │           │         │         └─ STOMP broker (driver pos.)    │
│           │           │         └─ Redis (zones, claim locks, rate limits) │
│           │           └─ Pricing AI (FastAPI) + OpenWeather / Open-Meteo   │
│           └─ Animal-checker AI (FastAPI, fail-closed)                      │
│                                                                            │
└────┬────────────────┬──────────────┬──────────────┬────────────────────────┘
     ▼                ▼              ▼              ▼
  Postgres         Stripe         Cloudinary     Firebase FCM
                 (payments)       (images)        (push)
  • Pricing flow: order request → parallel weather + ML calls (Mono.zip) → cached prediction → final price with a degraded flag if any upstream fails.
  • Dispatch flow: each order is bucketed into a bit-packed zone ID (latZone << 32) | (lngZone & 0xFFFFFFFFL) and tracked in Redis sets; carriers query their zone(s).
  • Bidding flow: carriers POST offers; shipper accepts → a Redis Lua script atomically claims the order + removes it from dispatch.
  • Image checks: any uploaded order image is screened by the animal-checker; the pipeline is fail-closed (upload rejected if the checker is unreachable).

Project layout

src/
├── main/
│   ├── java/com/spring/g_p/
│   │   ├── GPApplication.java              ← @SpringBootApplication
│   │   ├── auth/                ── registration, login, JWT, OTP, Google OAuth
│   │   ├── orders/              ── order lifecycle, events, status machine
│   │   ├── pricing/             ── ML-based dynamic pricing + weather
│   │   ├── dispatch/            ── zone-based carrier dispatch (Redis)
│   │   ├── bidding/             ── carrier offers & shipper acceptance
│   │   ├── payment/             ── wallet, Stripe, withdrawals, reconciliation
│   │   ├── notification/        ── Firebase Cloud Messaging
│   │   ├── inappchat/           ── WebSocket chat + conversations
│   │   ├── livetracking/        ── driver location via STOMP
│   │   ├── orderverification/   ── pickup / delivery OTP & QR confirm
│   │   ├── identityverification/── national-ID verification flows
│   │   ├── imagecheck/          ── animal detection (fail-closed)
│   │   ├── rating/              ── shipper ↔ carrier reviews
│   │   ├── profile/             ── profile, addresses, password
│   │   ├── admindashboard/      ── back-office (users, KYC, payments, support)
│   │   └── common/              ── security, JWT filter, exception handler,
│   │                              rate limiting, auditing, image service, OpenAPI
│   └── resources/
│       └── application.properties
└── test/

Modules

Package Purpose
auth Registration, login, JWT issue/refresh, OTP, Google OAuth, revoked tokens.
orders Create / cancel / track orders, status transitions, event publishing.
pricing Calls pricing ML + OpenWeather/Open-Meteo in parallel, caches predictions.
dispatch Zone-bucketed availability in Redis, reconciliation against Postgres.
bidding Carrier offer placement, shipper acceptance, atomic claim via Lua.
payment Wallet balances, Stripe top-up & webhooks, withdrawal requests.
notification Push notifications via Firebase, in-app notification feed.
inappchat One-to-one chat over STOMP; persisted conversations & messages.
livetracking Carrier location broadcast to shipper subscribers.
orderverification Pickup/delivery confirmation via OTP or QR.
identityverification KYC status, national-ID image handling.
imagecheck Animal detection on uploaded images (rejects if AI down).
rating Post-trip ratings + aggregate scores.
profile Profile fields, address book, password change, account deletion.
admindashboard Multi-controller back office: users, KYC, transactions, ratings, support, configs.
common Shared infra (security, exceptions, rate limiting, auditing, image upload, OpenAPI).

User roles

Role Granted on What it can do
USER Registration (default) Create / cancel orders, place bids, chat, rate, manage wallet & profile. The same user can act as shipper, receiver, or carrier depending on the order context.
ADMIN Provisioned manually Full admin dashboard: users, KYC verification, transactions, ratings, notifications, message moderation, support tickets, global search, platform configs. All /api/v1/admin/** routes require hasAuthority('ADMIN').

Business rule enforced server-side: on any given order, the sender, receiver, and carrier must all be distinct users.

Authorization is enforced by @PreAuthorize on controllers + JWT claim validation in JWTAuthenticationFilter.


Domain model

Core @Entity classes (all extend BaseEntityid, @Version, audit fields, soft-delete):

Entity Key fields Relationships
User username, email, phone, password (BCrypt), role, verified, deleted 1:1 → Wallet; 1:N → Order (as sender/receiver/carrier), Bid, Rating, Notification, ChatMessage
Order itemName, description, weight, L/W/H, pickup/drop addr + lat/lng, estimatedPrice, lowPrice/normalPrice/highPrice, finalPrice, status, zoneId, pickupCode, dropCode, payment-state flags ManyToOne → sender, receiver, carrier (all User)
Bid offerPrice (BigDecimal), status (PENDING / ACCEPTED / REJECTED / CANCELLED) ManyToOne → order, carrier
Wallet balance, availableBalance OneToOne → user
WalletTransaction amountCents, type (CREDIT / DEBIT / RESERVED), status, externalPaymentId ManyToOne → wallet, order
PaymentRecord externalPaymentId, status (PENDING / SUCCEEDED / FAILED / REFUNDED), webhookProcessed ManyToOne → user
WithdrawalRequest amountCents, destinationToken, externalPayoutId, status, otp ManyToOne → user
Rating stars (1–5), message; unique on (order_id, rater_id) ManyToOne → order, rater, driver
NotificationEntry title, message, type, referenceId, isRead ManyToOne → user
Conversation lastMessage, lastMessageTimestamp, chatType, unreadCountA/B OneToOne → order; ManyToOne → participantA / B
ChatMessage message, type (TEXT/…), timestamp ManyToOne → conversation, sender, receiver
UserAddress label, lat/lng, fields ManyToOne → user
RevokedToken token (indexed), expiryDate standalone
SupportTicket, SystemConfig admin & ops data

Order lifecycle

        ┌─ DRAFT
        │     │ shipper completes details + image check passes
        │     ▼
        │  AWAITING_RECEIVER_CONFIRMATION
        │     │ receiver confirms
        │     ▼
        │  WAIT_FOR_DELIVERY                ← published to dispatch (zone in Redis)
        │     │ shipper accepts a bid       ← atomic Redis claim removes from dispatch
        │     ▼
        │  MATCHED
        │     │ carrier confirms pickup (OTP / QR)
        │     ▼
        │  IN_TRANSIT
        │     │ carrier confirms delivery (OTP / QR)
        │     ▼
        │  DELIVERED  ──────────────► payment settles + ratings opened
        │
        └────► CANCELLED (terminal, reachable from most states)

Status transitions are guarded inside OrderService and emit Spring application events that the dispatch, notification, and payment modules listen to.


Key flows

Authentication

register ──► sendOTP ──► verifyOTP ──► login ──► access + refresh JWT
                                         │
                                         └──► /refresh (X-Refresh-Token header)
                                         └──► /logout  (token → RevokedToken table)

JWT claims: USERNAME, ROLE (USER / ADMIN), TYPE (ACCESS / REFRESH), iss, exp.

Token Default lifetime
Access 24 h (security.jwt.access-token-expiration)
Refresh 7 d (security.jwt.refresh-token-expiration)
  • Refresh tokens are sent via the X-Refresh-Token header and validated against the TYPE claim.
  • Logout writes the token into revoked_tokens; the JWT filter rejects any token present there.
  • A nightly job purges expired revoked tokens at 03:00.

Pricing

PricingService.getPrice(TripReqForPricing) ──┐
                                             │
              Mono.zip( pickupWeather,       │  ─► OpenWeatherMap / Open-Meteo (CB)
                        dropWeather,         │
                        aiPrediction )       │  ─► Pricing-AI POST /predict_price (CB + retry)
                                             │
                ─► PricingResponse {         │
                       lowPrice,             │  BigDecimal, scale 2
                       normalPrice,          │
                       highPrice,            │
                       degraded: true|falsetrue if AI or weather call failed
                   }
  • Cache: 30-min Caffeine cache on AI predictions keyed by request shape.
  • Fallback heuristic when AI is down: 8 + 0.5×distanceKm + 10(if rain) + 10(if extreme temp) + size/weight uplift.
  • Price clamped to [pricing.min-price, pricing.max-price].
  • AI request is snake_case; DTO uses @JsonProperty per field. rain & day_of_week are strings.

Dispatch & bidding

Zone math (GridService):

  • Grid cell = 0.02° × 0.02° lat/lng.
  • zoneId = ((latZone << 32) | (lngZone & 0xFFFFFFFFL)) — single long.
  • Carrier query expands to a square neighbourhood (configurable via dispatch.grid.default-range).

Redis structures (ZoneRedisRepository):

Key Type Purpose TTL
zone:{zoneId} Set Order IDs in that zone 25 min
order:zone:{orderId} String Reverse map: order → zone
order:claim:{orderId} String Atomic claim lock during bid acceptance 30 s
zone:index Set All active zones (used by reconciliation)

Atomic claim: Lua script CLAIM_AND_REMOVE_SCRIPT does SET NX + SREM in one round trip, preventing two shippers from accepting the same bid.

Reconciliation: RedisReconciliationJob (ShedLock-protected) periodically re-syncs Postgres active orders with Redis zones, healing drift.

Payment & wallet

  • Top-up: client creates a PaymentIntent via Stripe; on payment_intent.succeeded webhook the backend credits the wallet exactly once (idempotent via webhookProcessed flag + dedup on externalPaymentId).
  • Order settlement: carrier deposit + sender amount are reserved (WalletTransaction type=RESERVED) when the order is matched, and committed on delivery confirmation.
  • Withdrawal: user requests payout → OTP sent → on confirmation a WithdrawalRequest is created; stripe.payout.mock=true short-circuits this in dev.
  • Reconciliation job: ReconciliationJob sweeps pending PaymentRecords and reconciles them against Stripe.

Stripe webhook events handled (/payments webhook):

Event Action
payment_intent.succeeded mark PaymentRecord SUCCEEDED, credit wallet
payment_intent.payment_failed mark PaymentRecord FAILED
charge.failed mark PaymentRecord FAILED
(others) logged at DEBUG, ignored

Signature is verified with stripe.webhook.secret. Replays are idempotent.


REST API surface

Full interactive docs: /swagger-ui.html once the app is running.

Base path Module Highlights
/auth auth POST /register, POST /login, POST /refresh, POST /verify-otp, POST /logout
/orders orders create, list, get, cancel, update-status, search
/orders/carrier dispatch GET /availableOrders (zone-filtered)
/orders/verify orderverification POST /verifyPickup, POST /verifyDelivery
/pricing pricing POST /predict
/bids bidding check availability, place / accept / reject offers
/wallet payment top-up, balance, status
/payments payment Stripe webhook
/profile profile profile, addresses, password, delete account
/ratings rating create, get, list
/notifications notification feed, unread count, mark as read
/chat inappchat REST list / get messages
/track/{orderId} livetracking current driver position
/api/v1/admin/auth admin login, refresh, logout
/api/v1/admin/users admin list / detail / updateStatus / bulkActions
/api/v1/admin/verifications admin list, approve, reject
/api/v1/admin/dashboard admin stats, orders-over-time, recent transactions
/api/v1/admin/transactions admin list, detail
/api/v1/admin/ratings admin list, detail
/api/v1/admin/notifications admin send, list
/api/v1/admin/messages admin search, block
/api/v1/admin/support admin list, detail, respond
/api/v1/admin/search admin global search
/api/v1/admin/configs admin manage platform settings

Real-time (WebSocket) endpoints

STOMP over SockJS; clients connect with the JWT in the CONNECT frame.

Destination Direction Purpose
/app/chat.sendMessage client → server Send a chat message in a conversation
/user/queue/messages server → client Receive chat messages (per-user queue)
/app/track.updateLocation/{orderId} carrier → server Push current GPS position
/topic/track/{orderId} server → shipper Broadcast carrier position
/user/queue/errors server → client WebSocket error channel (see GlobalExceptionHandler#handleChatErrors)

External integrations

Service Purpose Resilience
PostgreSQL Primary store Hikari pool, optimistic locking via @Version
Redis Dispatch zones, rate-limit buckets, ShedLock Lettuce pool
Stripe Card payments, webhooks, payout (mockable) Webhook signature verification
Cloudinary Order / profile / ID images
Firebase Admin SDK Push notifications
Pricing AI (FastAPI) ML price prediction Resilience4j CB pricingAi, 5 s timeout, 2 retries, Caffeine cache
Animal-checker AI (FastAPI) Reject animal images Resilience4j CB animalChecker, fail-closed
OpenWeatherMap Current weather CB openWeather
Open-Meteo Fallback weather CB openMeteo

Cross-cutting concerns

Located under common/:

  • WebSecurityConfig — stateless JWT auth, CORS allow-list, method security via @PreAuthorize.
  • JWTAuthenticationFilter — extracts & validates Bearer tokens once per request; consults revoked_tokens.
  • GlobalExceptionHandler — maps domain exceptions to consistent ApiResponse payloads (404, 401, 403, 409, 422, 429, 500), including optimistic-lock conflicts, validation errors, and rate-limit 429 with Retry-After.
  • @RateLimited + RateLimitInterceptor — Bucket4j-on-Redis annotation-driven throttling.
  • BaseEntityid, @Version, Hibernate @CreationTimestamp / @UpdateTimestamp, Spring @CreatedBy / @LastModifiedBy, soft-delete flag.
  • AuditorAwareImpl — pulls current principal for auditing.
  • ImageService — Cloudinary upload helper.
  • FirebaseConfig — Admin SDK init from classpath: service-account JSON.
  • OpenApiConfig — Swagger UI with JWT bearer auth.

Scheduled jobs

Job Location Cadence Notes
ReconciliationJob payment/scheduler @Scheduled Reconciles pending Stripe payments.
RedisReconciliationJob dispatch/job @Scheduled + @SchedulerLock Re-syncs active orders into Redis zones. ShedLock prevents double-run.
Revoked-token cleanup auth Daily 03:00 Deletes tokens past their expiryDate.

Resilience

Every outbound integration is wrapped in a Resilience4j circuit breaker configured in application.properties:

Instance Used by
pricingAi Pricing ML service
animalChecker Animal image checker
openWeather OpenWeatherMap
openMeteo Open-Meteo (weather fallback)

Shared defaults: sliding-window 20, minimum-calls 10, failure-rate threshold 50 %, open-state 30 s, half-open with 3 permitted calls, auto half-open. Overlaid with per-client timeouts (5–15 s) and retry filters that only retry transient network failures (never 4xx).


Redis key reference

Key pattern Type Purpose TTL
zone:{zoneId} Set order IDs in zone 25 min
order:zone:{orderId} String reverse zone lookup
order:claim:{orderId} String atomic accept-bid lock 30 s
zone:index Set all currently populated zones
zone:tmp:{...} Set temp keys during batched ops short
rl:{policy}:{discriminator} Bucket4j rate-limit token bucket per policy
withdrawal:req:{transactionId} String/Hash OTP state for payout request 60 min
shedlock:{jobName} String scheduled-job lease per lock

API response shape

All non-streaming responses use ApiResponse<T>:

{
  "message": "Order created successfully",
  "data": {
    "id": 142,
    "status": "AWAITING_RECEIVER_CONFIRMATION"
  }
}
  • data is omitted when null (@JsonInclude(NON_NULL)).
  • Errors share the same envelope; HTTP status code is the source of truth.
  • Validation errors include a per-field data map:
{
  "message": "Pickup zone is required, Distance must be positive",
  "data": {
    "pickupZone": "Pickup zone is required",
    "distanceKm": "Distance must be positive"
  }
}
HTTP When
200 / 201 success
400 validation, illegal arg, notification general
401 bad credentials, unverified user, bad JWT
403 access denied (auth ok, perms missing)
404 user / notification / resource not found
409 duplicate user, optimistic-lock conflict, integrity violation
422 animal detected in image
429 rate limit exceeded (response has Retry-After)
500 unexpected — response carries an error reference UUID also written to logs

Running locally

Prerequisites

  • JDK 21
  • PostgreSQL (local or remote — point spring.datasource.* at it)
  • Redis 7

Steps

# 1. Start Redis (one-time)
docker run -d --name g_p-redis -p 6379:6379 redis:7-alpine

# 2. Build
./mvnw clean package -DskipTests      # Linux/macOS
.\mvnw.cmd clean package -DskipTests   # Windows

# 3. Run
java -jar target/G_P-0.0.1-SNAPSHOT.jar
# or
./mvnw spring-boot:run

The server listens on http://localhost:8080. Swagger is at /swagger-ui.html.

Run with Docker

docker build -t wassalha-backend:local .
docker run --rm -p 8080:8080 --network host wassalha-backend:local

Configuration

Key properties in src/main/resources/application.properties:

Property Controls
spring.datasource.url / .username / .password Postgres connection
spring.data.redis.host / .port Redis connection (defaults to localhost:6379)
security.jwt.secret-key, *-token-expiration JWT signing & lifetimes
google.client-id / .client-secret Google OAuth
firebase.service-account-file Path to FCM admin key (classpath: or filesystem)
cloudinary.cloud_name / .api_key / .api_secret Image uploads
stripe.api-key / .webhook.secret / .payout.mock Stripe
openweathermap.api.key OpenWeather
pricing.ai.base-url Pricing ML service
pricing.min-price / .max-price Final price clamp
animal.checker.base-url / .api-key Animal-checker AI
resilience4j.circuitbreaker.instances.* CB tuning per upstream
dispatch.grid.default-range Zone neighbourhood radius
dispatch.cache.zone-ttl TTL for cached zone entries

⚠️ Production: load every credential from environment variables or --spring.config.location=... — do not ship the file with real secrets baked in.


API docs

  • Swagger UI: /swagger-ui.html
  • OpenAPI JSON: /v3/api-docs

Both are alphabetically sorted, support "Try it out", and display request duration.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages