Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

194 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Phoenix Exchange

CI Java Spring Boot Angular PostgreSQL Stripe

Full-stack platform for the buy-back and re-commercialisation of refurbished electronics. A device is dropped off, diagnosed, repaired if needed, then monetised (resale or rental via DAAS) with a transparent net payout to the client.

🔗 The app was deployed and live during the project demo (Railway · Vercel · Neon Postgres). The hosting has since been taken down to save costs, so the public URLs are no longer reachable - everything runs locally (see Running locally).


At a glance (for reviewers & recruiters)

Phoenix Exchange is an end-to-end web application I designed and built solo as my final Bachelor project in computer science. It was deployed in production during the project demo (Railway, Vercel and Neon), and the hosting has since been taken down to save costs. It models a real circular-economy business: collecting dormant devices, grading them, and routing each one to its most profitable outcome through an automated decision engine.

What it demonstrates

  • Backend architecture: A Spring Boot 3 REST API (~22 controllers, ~30 services) with a clean layered design (controller → service → repository), JPA/Hibernate persistence on PostgreSQL, and centralised error handling.
  • Stateless security: JWT authentication (HMAC-SHA256), BCrypt password hashing, and role-based authorization (CLIENT, OPERATOR, TECHNICIAN, ADMIN) enforced on both the API and the SPA.
  • A real business engine: A DecisionEngineService that scores each device and computes the financials of four scenarios (direct sale, value-add sale, rental/DAAS, return), then recommends the most profitable one. The money logic (SettlementService) is the part I prioritised for unit testing.
  • Real-time UX: In-app notifications persisted in the database and pushed live over WebSocket (STOMP/SockJS) to the right roles at every workflow step.
  • Modern frontend: Angular 21 SPA built entirely with Signals (no NgZone), standalone components, OnPush change detection, lazy-loaded role workspaces, route guards, and HTTP interceptors. PWA-ready.
  • Third-party integrations: Stripe (checkout + signed webhooks), eBay (market pricing + listing publication), Brevo (transactional email), and a HuggingFace-powered AI assistant for scan-time price estimation and client support.
  • DevOps: Dockerised local stack, GitHub Actions CI (build + tests on every push), and continuous deployment during the demo on Railway (API), Neon (PostgreSQL) and Vercel (frontend).

Stack: Java 17 · Spring Boot 3.5 · Spring Security · Spring Data JPA · WebSocket · PostgreSQL · Angular 21 · RxJS · TypeScript · Stripe · eBay API · HuggingFace · Docker · GitHub Actions · Railway · Neon · Vercel


Table of contents


Architecture

phoenix-exchange/
├── phoenix-api/          # Spring Boot 3 backend (REST + WebSocket)
├── phoenix-ui/           # Angular 21 frontend (SPA / PWA)
└── phoenix-doc/          # UML diagrams and documentation

The backend exposes a JWT-secured REST API. The frontend communicates exclusively through this API and receives real-time notifications over WebSocket (STOMP over SockJS). The modules are deployed independently.

Angular client  ──HTTPS──▶  Spring Boot API  ──JDBC──▶  PostgreSQL
                             │
                             ├──▶  eBay API        (pricing + listings)
                             ├──▶  Stripe API      (client payments)
                             ├──▶  HuggingFace     (AI estimation + assistant)
                             └──▶  Brevo (SMTP)    (transactional email)

Tech stack

Backend (phoenix-api)

Technology Version Role
Java 17 Language
Spring Boot 3.5 Application framework
Spring Security 6 Authentication / authorization
Spring Data JPA n/a ORM (Hibernate 6)
Spring WebSocket n/a Real-time notifications (STOMP)
PostgreSQL n/a Database
Springdoc OpenAPI n/a API documentation (Swagger UI)
Stripe Java SDK n/a Payments
JJWT n/a JWT generation / validation
Lombok n/a Boilerplate reduction

Frontend (phoenix-ui)

Technology Version Role
Angular 21 SPA framework
Angular Signals n/a Fine-grained reactivity (no NgZone)
Angular Router n/a Navigation with view transitions
RxJS n/a Async streams
@stomp/stompjs n/a STOMP WebSocket client
SockJS n/a WebSocket fallback
Angular PWA n/a Service Worker
Tailwind CSS n/a Utility-first styling

Project structure

Backend

phoenix-api/src/main/java/com/phoenix/exchange/
├── config/
│   ├── SecurityConfig.java          # Spring Security, CORS, JWT filters
│   ├── WebSocketConfig.java         # STOMP broker, SockJS endpoints
│   ├── OpenApiConfig.java           # Swagger UI
│   ├── AsyncConfig.java             # Async thread pool
│   ├── DataInitializer.java         # Seed data (admin, categories, demo accounts)
│   └── RoleMigration.java           # Role migration in DB
├── controller/                      # REST layer (one controller per resource)
├── service/                         # Business logic (~30 services)
├── model/                           # JPA entities
├── repository/                      # Spring Data repositories
├── dto/                             # Request/Response records
├── enums/                           # Domain enumerations
├── exception/                       # GlobalExceptionHandler + domain exceptions
├── security/                        # JwtFilter, JwtService, UserDetailsService
└── ebay/                            # eBay integration (pricing + listings + sync)

Frontend

phoenix-ui/src/app/
├── app.config.ts                    # Angular configuration (router, HTTP, i18n)
├── app.routes.ts                    # Root routes with lazy loading
├── core/
│   ├── guards/                      # AuthGuard, RoleGuard
│   ├── interceptors/                # AuthInterceptor (JWT), ErrorInterceptor
│   ├── models/                      # TypeScript interfaces
│   └── services/                    # HTTP services (one per resource)
├── features/
│   ├── auth/                        # Login, Register, Reset Password
│   ├── client/                      # Client workspace (dashboard, devices, revenue…)
│   ├── operator/                    # Operator workspace (devices, diagnostics, decisions…)
│   ├── technician/                  # Technician workspace (repairs, reports)
│   ├── admin/                       # Administration (users, categories, stats)
│   └── partner/                     # Repair-partner workspace
├── layout/
│   ├── client-layout/               # Client shell (sidebar + topbar + mobile bottom-nav)
│   └── backoffice-layout/           # Back-office shell (sidebar + topbar)
└── shared/
    └── components/                  # phoenix-icon, phoenix-logo, phoenix-toast,
                                     # phoenix-notification-bell, user-menu…

Data model

Core entities

User: User account with role(s) (CLIENT, OPERATOR, TECHNICIAN, ADMIN). Stores banking details (IBAN) for payouts.

Device: A device submitted by a client. Tracked across its whole lifecycle via DeviceStatus.

DiagnosticReport: Diagnostic report issued by a technician. Contains the Phoenix score (0–100), the estimated repair cost, and the list of DiagnosticItem (state of each component).

Decision: Financial decision generated automatically by the engine after diagnosis. Holds the estimates for the four possible scenarios and the scenario ultimately chosen by the operator.

RepairOrder: Repair order created following a VALUE_ADD_SALE decision. Linked to a RepairPartner.

Listing: Sale listing published on the internal channel or eBay.

Subscription: DAAS (Device as a Service) contract. Manages the tenant, monthly rent, duration, security deposit and client signature.

Settlement: Frozen financial statement between Phoenix Exchange and the owner. Can be a return after diagnosis, after repair, a sale, or a DAAS instalment. clientNet > 0 = amount owed to the client; clientNet < 0 = amount owed by the client.

Appointment: Physical handover appointment (drop-off or delivery).

UserNotification: Persisted in-app notification linked to a user.

SupportTicket: Client support ticket.

Warranty: Warranty attached to a device after sale.

RepairPartner: External repair partner, linked to technicians.

Device statuses (DeviceStatus)

PENDING_REVIEW → RECEIVED → ACQUIRED → DIAGNOSED → DECISION_PENDING
    → SENT_TO_REPAIR → IN_REPAIR → REPAIRED
    → RETRIEVAL_PENDING | LISTED → SOLD | DAAS_ACTIVE → DAAS_ENDED
NOT_RECEIVED   (parcel lost / not delivered)
ARCHIVED

Device lifecycle

1. SUBMISSION       Client submits a device via the client workspace or QR scan
2. RECEPTION        Operator physically receives the device → RECEIVED
3. ACQUISITION      Operator validates acquisition → ACQUIRED
4. DIAGNOSIS        Technician submits the diagnostic report → DIAGNOSED
5. DECISION         Engine generates estimates → DECISION_PENDING
6. EXECUTION        Operator picks and confirms a scenario:
   ├─ DIRECT SALE     Listing created and published → LISTED → SOLD
   ├─ VALUE-ADD SALE  Repair order → repair → listing → LISTED → SOLD
   ├─ DAAS            Rental contract created → DAAS_ACTIVE → DAAS_ENDED
   └─ RETURN          Settlement generated, client notified → RETRIEVAL_PENDING → ARCHIVED

Every step triggers automatic notifications to the right roles (client, operator, technician of the relevant agency) through a centralised matrix (WorkflowNotificationService).


Decision engine

DecisionEngineService analyses the diagnostic report and produces financial estimates for each scenario, then recommends the most profitable one.

Recommendation rule

Score ≥ 70  → DAAS if 24-month rental revenue > direct sale, otherwise DIRECT_SALE
Score 40–69 → VALUE_ADD_SALE if sale after repair > direct sale, otherwise DIRECT_SALE
Score < 40  → DIRECT_SALE

Financial calculations (PricingService)

  • Market price: queries the eBay API for recent sales of the same model.
  • Direct sale: marketPrice × (1 − acquisitionRatio) adjusted by condition.
  • Value-add sale: (marketPrice × 0.85) − repairCost − acquisitionCost.
  • Monthly DAAS: (marketPrice × commissionRate) / durationMonths.

Executing a scenario (POST /api/decisions/{id}/execute)

{
  "scenario": "VENTE_DIRECTE | VENTE_VALORISEE | DAAS | RESTITUTION",
  "salePrice": 299.00,
  "listingTitle": "...",
  "listingDescription": "...",
  "channel": "INTERNAL | EBAY | OTHER",
  "monthlyRent": 19.90,
  "durationMonths": 12,
  "daasStartDate": "2026-01-01",
  "tenantName": "...",
  "tenantEmail": "...",
  "rentalTarget": "B2B",
  "depositAmount": 50.00,
  "contractClauses": "...",
  "clientSignatureRequired": true
}

Business model

The client settlement (Settlement) is frozen in the database at each financial event:

Scenario Rule
Sale price − repairs − €10 grading − 15% commission
Return after diagnosis €15 diagnostic fee
Return after repair €15 + actual repair cost
DAAS instalment 15% of the rent (fixed fees deducted from the first instalment)

Diagnosis is free when the device goes through the full flow. The fee schedule is configurable via environment variables (FEE_*) without redeployment. Fees are paid online through Stripe Checkout; a signed webhook confirms the payment automatically.


Roles & permissions

Role Access
CLIENT Client workspace: submit devices, track lifecycle, revenue, support
OPERATOR Operator workspace: receive, decide, publish, manage contracts
TECHNICIAN Technician workspace: diagnose, create repair orders
ADMIN Global administration: users, categories, stats, partners

A user can hold multiple roles (stored as Set<UserRole>). The frontend uses a RoleGuard to protect each workspace.


REST API reference

Local base URL: http://localhost:8080/api Local Swagger UI: http://localhost:8080/swagger-ui/index.html (During the demo the API was hosted on Railway; that deployment has been taken down.)

All endpoints are JWT-protected except /api/auth/** and explicitly opened public routes.

Authentication

Method Endpoint Description
POST /auth/login Login → JWT
POST /auth/register Registration
POST /auth/forgot-password Send reset email
POST /auth/reset-password Reset with token

Devices

Method Endpoint Description
GET /devices All devices (OPERATOR/ADMIN)
GET /devices/{id} Device detail
POST /devices Create a device (back office)
POST /devices/submit Submit from the client workspace
PATCH /devices/{id}/status Change status
PATCH /devices/{id}/receive Mark as received
GET /devices/status/{status} Filter by status

Diagnostics

Method Endpoint Description
POST /diagnostics Create a diagnostic report
GET /diagnostics/device/{deviceId} Reports for a device
GET /diagnostics/{id} Report detail
POST /diagnostic-items Add an item to a diagnosis

Decisions

Method Endpoint Description
POST /decisions/generate/{deviceId} Generate estimates via the engine
POST /decisions/{id}/execute Execute the chosen scenario
GET /decisions/device/{deviceId} Decision for a device
GET /decisions/{id} Decision detail
POST /decisions/simulate Simulation without persistence

Repairs

Method Endpoint Description
POST /repair-orders Create a repair order
GET /repair-orders/device/{deviceId} Orders for a device
PATCH /repair-orders/{id}/status Update status

Listings

Method Endpoint Description
POST /listings Create a listing
POST /listings/{id}/publish Publish (→ eBay if channel=EBAY)
POST /listings/{id}/sold Mark as sold
GET /listings All listings

DAAS contracts

Method Endpoint Description
POST /subscriptions Create a contract
POST /subscriptions/{id}/approve Approve (client)
GET /subscriptions/device/{deviceId} Contract for a device

Settlements

Method Endpoint Description
GET /settlements/mine My settlements (client workspace)
GET /settlements/device/{deviceId} Settlements for a device
GET /settlements/preview Preview without persistence

Appointments

Method Endpoint Description
POST /appointments Create an appointment
GET /appointments List all appointments
POST /appointments/{id}/complete Mark as completed

Notifications

Method Endpoint Description
GET /notifications/recent 10 most recent notifications
GET /notifications/unread-count Unread count
PATCH /notifications/{id}/read Mark as read

Payments (Stripe)

Method Endpoint Description
POST /payments/checkout/{settlementId} Create a Stripe Checkout session
POST /payments/webhook Stripe webhook (signature verified)

Administration

Method Endpoint Description
GET /admin/stats Global statistics
GET /users All users (ADMIN)
POST /users Create a user
GET /categories Device categories
POST /categories Create a category

Angular frontend

Key patterns

Signals: Reactivity is handled exclusively via signal(), computed() and toSignal(). No async pipe and no manual subscriptions in views. ChangeDetectionStrategy.OnPush everywhere.

Standalone components: No NgModule. Each component declares its own imports.

Lazy loading: Each workspace (client, operator, technician, admin, partner) is a routed module loaded on demand via loadChildren.

Guards: AuthGuard redirects to /login if there is no valid token; RoleGuard redirects to /unauthorized if the role does not match.

HTTP interceptors:

  • AuthInterceptor: adds Authorization: Bearer <token> to every request.
  • ErrorInterceptor: shows a toast for 4xx/5xx errors (except requests marked SKIP_ERROR_TOAST).

Dropdowns: Floating menus (user-menu, notification bell) use position: fixed with coordinates computed via getBoundingClientRect() to escape overflow: hidden ancestors.

Functional workspaces

Workspace Routes Description
Client /dashboard, /devices, /revenues, /appointments, /scan, /support Track own devices and revenue
Operator /operator/devices, /operator/diagnostics, /operator/daas, /operator/listings Full workflow management
Technician /technician/repair-orders, /technician/diagnostics Repairs and reports
Admin /admin/users, /admin/categories, /admin/partners Administration
Partner /partner/repair-orders Incoming repair orders

Operator component: device detail

OperatorDeviceDetailComponent is the heart of the operator interface. It shows the device's current state with a status badge, the event history (DeviceEvent), the diagnostic report with the Phoenix score, the four decision scenarios with their estimates, the context-aware form for the selected scenario (price, rent, duration…), and the execution button that calls POST /api/decisions/{id}/execute.


JWT authentication

The JWT is signed with HMAC-SHA256. Its lifetime is 24 hours (app.jwt.expiration-ms=86400000).

Backend: The JwtFilter (Spring Security) extracts the token from the Authorization: Bearer ... header, validates the signature with JWT_SECRET, and loads the UserDetails from the database. No server session (stateless).

Frontend: The token is stored in TokenStorageService (localStorage). AuthService exposes a user() signal populated at startup from /api/auth/me.

⚠️ JWT_SECRET must be provided via an environment variable. No default value is defined in application.properties: startup fails if it is missing.


Real-time notifications

In-app notifications are persisted in the database (UserNotification) and delivered in real time over WebSocket STOMP.

Backend: WebSocketConfig configures an in-memory broker on /topic and /queue. Each relevant business action (WorkflowAction) triggers WorkflowNotificationService.dispatch(), which notifies the concerned recipients (device owner, operators, partner technicians).

Frontend: PhoenixNotificationBellComponent subscribes to the logged-in user's topic at login. The unread counter updates in real time.

Actions that trigger notifications

Action Recipients
DEVICE_RECEIVED Client (owner) + all operators
DIAGNOSTIC_SUBMITTED Client + operators
REPAIR_ORDER_CREATED Client + partner technicians + operators
REPAIR_SENT Client + partner technicians
REPAIR_STARTED Client + operators
REPAIR_COMPLETED Client + operators
REPAIR_VALIDATED Client
LISTING_PUBLISHED Client
DEVICE_SOLD Client + operators
DAAS_CONTRACT_CREATED Client + operators
RETRIEVAL_REQUESTED Client + operators

External integrations

eBay

The ebay/ module queries the eBay API to:

  • Pricing: fetch a model's market price from recent sales.
  • Listings: publish a listing directly on eBay from the operator workflow, with order synchronisation (EbayOrderSyncService).

Required configuration: EBAY_CLIENT_ID, EBAY_CLIENT_SECRET. Webhook callback URL: https://<your-domain>/api/ebay/webhook (during the demo this pointed to the Railway deployment).

Stripe

Used for settlement payments (returns, instalments):

  1. The operator / system generates a Settlement for the client.
  2. The client starts the payment → POST /api/payments/checkout/{settlementId} creates a Stripe Checkout session and returns the URL.
  3. Stripe sends a signed webhook to POST /api/payments/webhook → the settlement is marked PAID.

Required configuration: STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET.

AI assistant (HuggingFace)

An AI assistant (AssistantService, HuggingFaceService, SupportChatService) provides a value estimate when a device is scanned and powers the client support chat. Configured via HF_API_KEY (cleanly disabled if absent).

Email (Brevo / SMTP)

Transactional emails (password reset, notifications). Configured via BREVO_API_KEY or the standard Spring Mail variables (SPRING_MAIL_*).


Environment variables

Backend (required)

Variable Description
DATABASE_URL PostgreSQL JDBC URL (e.g. jdbc:postgresql://host:5432/db)
DATABASE_USERNAME PostgreSQL user
DATABASE_PASSWORD PostgreSQL password
JWT_SECRET HMAC-SHA256 secret to sign JWTs (≥ 32 chars, never commit)

Backend (optional)

Variable Default Description
PORT 8080 Server port
JPA_DDL_AUTO update Hibernate DDL strategy
EBAY_CLIENT_ID / EBAY_CLIENT_SECRET (empty) eBay publication (disabled if absent)
STRIPE_SECRET_KEY (empty) Stripe secret key
STRIPE_WEBHOOK_SECRET (empty) Stripe webhook validation secret
BREVO_API_KEY (empty) Transactional emails
HF_API_KEY (empty) AI estimation / assistant
FEE_DIAGNOSTIC 15.0 Diagnostic fee (€)
FEE_GRADING 10.0 Grading fee (€)
FEE_COMMISSION 0.15 Phoenix commission rate (15%)
WARRANTY_MONTHS 12 Default warranty duration (months)

Frontend

The API URL is configured in src/environments/environment.ts (development) and environment.prod.ts (production). In development, environment.ts uses the relative paths /api and /ws, which are proxied to http://localhost:8080 by proxy.conf.json — so no change is needed to run locally.


Running locally

Prerequisites

  • Java 17+ · Maven 3.8+ · Node.js 20+ and npm 10+ · PostgreSQL 14+ (or Docker)

Backend

# 1. Start only the database (the compose file uses phoenix / phoenix / phoenix_exchange)
docker-compose up -d db

# 2. Environment variables
#    JWT_SECRET is the only one strictly required. The database defaults already
#    match the Docker container, so the DATABASE_* lines are optional.
export JWT_SECRET=your_secret_at_least_32_characters
# export DATABASE_URL=jdbc:postgresql://localhost:5432/phoenix_exchange
# export DATABASE_USERNAME=phoenix
# export DATABASE_PASSWORD=phoenix

# 3. Start the API at http://localhost:8080/api  (Swagger: /swagger-ui/index.html)
cd phoenix-api && ./mvnw spring-boot:run

The external integrations (eBay, Stripe, Brevo, HuggingFace) are optional and stay disabled if their variables are unset, so the API boots with JWT_SECRET alone. Demo accounts are created on first startup (see DataInitializer).

Alternatively, run the whole stack in containers with docker-compose up -d (API + database + UI). The api service already sets a dev JWT_SECRET.

Frontend

cd phoenix-ui
npm install
npm start        # http://localhost:4200

No configuration is needed: npm start serves the app on http://localhost:4200 and proxies /api and /ws to the backend on port 8080 via proxy.conf.json.


Tests

cd phoenix-api && ./mvnw test

The settlement calculation rules (SettlementService), the core of the business model, are covered by targeted unit tests. The GitHub Actions CI replays build and tests on every push.


Deployment

For the project demo the app was continuously deployed: Railway hosted the API, Neon hosted the PostgreSQL database, Vercel hosted the frontend, and GitHub Actions replayed build and tests on every push. The hosting has since been taken down to save costs, but the configuration below documents how it was set up and remains reproducible.

phoenix-api service (Railway)

Railway auto-detected the Maven project. Start command:

java -jar target/phoenix-0.0.1-SNAPSHOT.jar

The DATABASE_URL, DATABASE_USERNAME and DATABASE_PASSWORD variables pointed to the Neon PostgreSQL instance; the other variables were set in the service settings.

phoenix-ui service (Vercel)

Build command : npm run build
Output dir    : dist/phoenix-ui/browser

CORS is configured in SecurityConfig.java to allow the frontend origin.


Security

  • JWT_SECRET has no default value. If the variable is not set, startup fails immediately.
  • The .env file is in .gitignore and must never be committed.
  • Stripe webhooks are validated by signature (Stripe-Signature) before any processing.
  • All routes are secured by default; public endpoints (/auth/**, /swagger-ui/**) are explicitly opened in SecurityConfig.
  • Passwords are hashed with BCrypt.
  • The JWT expires after 24 hours. No refresh-token strategy is implemented (re-login required on expiry).

Error handling

GlobalExceptionHandler centralises all error responses:

Exception HTTP code Use
MethodArgumentNotValidException 400 Bean Validation failed
BusinessException 400 Business rule violated
IllegalArgumentException 400 Invalid argument
ResourceNotFoundException 404 Entity not found
DuplicateResourceException 409 Resource already exists
BadCredentialsException 401 Wrong email / password
AccessDeniedException 403 Insufficient role
Exception (catch-all) 500 Internal error (logged server-side)

All error responses follow this format:

{
  "status": 400,
  "error": "Bad Request",
  "message": "Descriptive message",
  "timestamp": "2026-06-13T12:34:56.789"
}

Final-year Bachelor project in computer science · designed and built by Yann

About

Electronics trade-in and refurbishment platform: AI estimation, client drop-off, technician diagnostics, partner repair, eBay resale and DAAS rental. Angular + Spring Boot.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages