diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..9aba56d
Binary files /dev/null and b/.DS_Store differ
diff --git a/.gitignore b/.gitignore
index 667aaef..2deb6e7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,9 @@
HELP.md
target/
+logs/
+data/
+src/main/resources/application-oracle-local.properties
+src/main/resources/application-docker-local.properties
.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
diff --git a/.run/Docker Oracle DbVisualizer.run.xml b/.run/Docker Oracle DbVisualizer.run.xml
new file mode 100644
index 0000000..98e796e
--- /dev/null
+++ b/.run/Docker Oracle DbVisualizer.run.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/.run/Local Mac DbVisualizer.run.xml b/.run/Local Mac DbVisualizer.run.xml
new file mode 100644
index 0000000..93a377a
--- /dev/null
+++ b/.run/Local Mac DbVisualizer.run.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/.run/Oracle XE.run.xml b/.run/Oracle XE.run.xml
new file mode 100644
index 0000000..1cb8b9d
--- /dev/null
+++ b/.run/Oracle XE.run.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..5a7033b
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,23 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+---
+
+## [1.0.0] - 2026-05-20
+
+### Added
+- **Core Conversion Engine**: Implemented `z*[^z]` decoder logic to convert measurement input strings into package inflow total lists. Supports standard characters (`a-y`), multiplier prefixes (`z`), empty/zero values (`_`), and case insensitivity.
+- **Persistent REST History Endpoints**: Exposes standard endpoints to interact with history database tables:
+ - `GET /convert-measurements` (accepts `input` / `convert-measurements` query parameter, converts, logs, and persists details).
+ - `GET /history` (returns all records).
+ - `GET /history/{id}` (returns a specific record).
+ - `PUT /history/{id}` (replaces/updates record).
+ - `PATCH /history/{id}` (patches specific fields).
+ - `DELETE /history` (clears history record table).
+- **Dual Persistence Architecture**: Setup standard H2 in-memory configuration by default (for out-of-the-box local testing) and added a dedicated `prod` profile using `ojdbc11` to persist history records on production Oracle XE databases.
+- **7-day Rolling Log System**: Configured custom Logback rolling file appenders that generate structured daily logs under `logs/pkc-api.log` with a strict 7-day retention policy.
+- **Full Automated Testing**: Added `ConversionEngineTest` to verify correctness on all requirements and edge cases.
diff --git a/README.md b/README.md
index b1cccfd..29cb51b 100644
--- a/README.md
+++ b/README.md
@@ -1,61 +1,155 @@
-## Submission Instructions
-
-To submit your Oracle JAVA Spring Boot Maven project as a solution, please follow these steps:
-
-### Step 1: Install git on your PC
-- Install "git" as shown in this tutorial: [How to install git](https://youtu.be/iYkLrXobBbA?si=_l0haibv_X9NpIjJ)
-- Open command prompt and run
- ```bash
- git version
- ```
-- If you see the version, then git is successfully installed.
-
-### Step 2: Fork the Repository
-- Navigate to [this repository](https://github.com/CodelineAtyab/oraclequantapi) provided by Codeline.
-- Click on the "Fork" button at the top-right corner of the page to create a copy of the repository under your own GitHub account.
-
-### Step 3: Clone the Forked Repository
-- Open your terminal or command prompt.
-- Clone the forked repository to your local machine using the following command:
- ```bash
- git clone https://github.com/your-username/repo-name.git
- ```
-
-### Step 4: Create a new branch
-- Navigate to the cloned repository directory
- ```bash
- cd repo-name
- ```
-- Create a new branch for your code submissions (Replace your-name with your name in your-name-submission-branch):
- ```bash
- git checkout -b your-name-submission-branch
- ```
-
-
-### Step 5: Add Your Code
-- Implement the API
-
-### Step 6: Commit your changes
-- Run the following commands in order to commit your changes:
- ```bash
- git add *
- git commit -m "Meaningful commit message here"
- ```
-
-### Step 7: Push Your Branch to GitHub
-- Run the following commands to upload the changes to the forked github repository (Replace your-name with your name in your-name-submission-branch):
- ```bash
- git push origin your-name-submission-branch
- ```
-
-### Step 8: Create a Pull Request
-- Go to your forked repository on GitHub.
-- You should see a prompt to create a pull request. Click on "Compare & pull request".
-- Provide a title and description for your pull request, then click "Create pull request".
-
-### Step 9: Notify Codeline
-- Notify on slack that you have created a PR for your solution.
-
-## Note: If you face any issues in the process above, Please do the following:
-- Watch [this youtube tutorial](https://www.youtube.com/watch?v=a_FLqX3vGR4)
-- Contact Ikhlas or Atyab.
+# My OracleQuant API notes
+
+This is my small project for the evaluation.
+You send text like `aa` → you get `[1]`. It saves everything in **MEASUREMENT_HISTORY**.
+Jar: **pkc-api.jar** · runs on port **8081**
+
+I am still learning so this readme is just my story day by day.
+
+---
+
+## Day 1 · Tuesday 20 May 2026
+
+**What I tried to do**
+Start the task. Read the PDF. I did not understand everything at first (packages, letters a-z = numbers, etc).
+
+**What I did**
+- Opened the project in IntelliJ
+- Made the conversion code (`ConversionEngine`)
+- Ran unit tests — the 8 examples from PDF passed (I was happy here)
+- Started Spring Boot with **H2** database (teacher said easier for first step)
+
+**Challenges**
+- PDF is long and has many rules
+- I confused where to put my files in the package folder
+- First time using Spring Boot + Maven together
+
+**What worked at end of day**
+Tests green. I could think the math part is OK.
+
+---
+
+## Day 2 · Wednesday 21 May 2026
+
+**What I tried to do**
+Make the API real — controller, history save, try Postman.
+
+**What I did**
+- Added `/convert-measurements` and `/history` endpoints
+- Every Postman call should save a row (input, output, IP, time)
+- Fixed a stupid mistake: class name must match file name (`PackageConversionApplication`)
+- Connected **DbVisualizer** to H2 file database
+- Saw table **MEASUREMENT_HISTORY** with my test data
+
+**Challenges**
+- App would not compile sometimes (JPA package missing in IDE)
+- Had to run `mvn compile` to see real errors
+- Did not know JDBC URL for H2 at first
+
+**What worked at end of day**
+Postman: `http://localhost:8081/convert-measurements?input=aa` → `[1]` and row in DbVisualizer.
+
+---
+
+## Day 3 · Thursday 22 May 2026
+
+**What I tried to do**
+Move from H2 to **real Oracle** (because assignment wants Oracle XE).
+
+**What I did**
+- Installed Oracle XE on Mac
+- Wrote SQL scripts for user **pkc_user** password **PkcPassword1**
+- Made `oracle` profile in `application.properties`
+- Tried DbVisualizer with `localhost:1521` and service **XEPDB1**
+
+**Challenges**
+- **ORA-01017** — login failed (I typed wrong password many times)
+- **ORA-01045** — user exists but cannot connect (needed GRANT CREATE SESSION)
+- App started very slow or failed when Oracle was off
+- I did not understand SYS vs SYSTEM vs pkc_user at first
+
+**What worked at end of day**
+User `pkc_user` can login in SQL*Plus / DbVisualizer. Table created.
+
+---
+
+## Day 4 · Friday 23 May 2026
+
+**What I tried to do**
+Run the app on **Oracle Linux VM** (assignment also talks about Linux server).
+
+**What I did**
+- VM name: **AlharithLinux**, user **alharith**
+- Built jar: `mvn clean package` → copied **pkc-api.jar** to `/home/alharith/`
+- Tried SSH from Mac (port 2222) — did not work, used VM screen instead
+- Ran `java -jar pkc-api.jar` on Linux
+
+**Challenges**
+- I ran `pkc-api.out` once by mistake (that is not the app file)
+- Tried `mvn spring-boot:run` on VM → error **no plugin spring-boot** (no pom.xml there, only jar)
+- DbVisualizer from Mac to VM IP `10.0.2.15` → **timeout** (NAT network, I learned later)
+- Port 8081 sometimes already used from old run
+- Connection refused in Postman because app stuck on database
+
+**What worked at end of day**
+I understand: on VM you only need `java -jar`, not Maven. Scripts saved in `scripts/` folder.
+
+---
+
+## Day 5 · Saturday 24 May 2026
+
+**What I tried to do**
+Make everything work on **my Mac** with **Docker + DbVisualizer + Postman** (my main setup now).
+
+**What I did**
+- Oracle in Docker container **oracle-xe**
+- Script `docker-setup-db-only.sh` for pkc_user
+- Script `mac-start-docker.sh` to start DB + API
+- API runs **inside Docker** on network **pkc-net** with profile **docker-internal**
+- DbVisualizer on Mac still uses `127.0.0.1:1521`
+
+**Challenges**
+- Docker said container name already exists — had to `docker start oracle-xe` not create new one
+- `mvn spring-boot:run` with docker profile on Mac **hangs** forever (HikariPool) — big confusion
+- Postman **ECONNREFUSED** — app never really started
+- Port **8081 in use** — needed `scripts/stop-pkc-api.sh` first
+
+**What worked at end of day (finally)**
+```bash
+bash scripts/stop-pkc-api.sh
+bash scripts/mac-start-docker.sh
+```
+Postman OK. DbVisualizer shows **MEASUREMENT_HISTORY**. I can show my trainer.
+
+---
+
+## Quick run (what I use now)
+
+```bash
+cd /Users/mac/IdeaProjects/oraclequantapi
+bash scripts/stop-pkc-api.sh
+bash scripts/mac-start-docker.sh
+```
+
+- Postman: `http://127.0.0.1:8081/convert-measurements?input=aa`
+- History: `http://127.0.0.1:8081/history`
+- DbVisualizer: `jdbc:oracle:thin:@//127.0.0.1:1521/XEPDB1` · user `pkc_user` · pass `PkcPassword1`
+
+Build jar: `mvn clean package`
+
+---
+
+## Small hints (for me later)
+
+- docker profile + mvn on Mac = bad (hangs)
+- use `.jar` not `.out`
+- stop old app before start (8081)
+- VM = `java -jar` only if no project folder
+
+---
+
+## Submit
+
+branch `alharith-alsubhi-submission-branch` → push → PR → Slack trainer
+
+*last update 24 May 2026*
diff --git a/dbvis/README-DbVisualizer.md b/dbvis/README-DbVisualizer.md
new file mode 100644
index 0000000..72315ff
--- /dev/null
+++ b/dbvis/README-DbVisualizer.md
@@ -0,0 +1,120 @@
+# DbVisualizer — connect to your program's data on Oracle Linux
+
+Your API stores every conversion in **your own table**:
+
+| Item | Value |
+|------|--------|
+| **Schema (owner)** | `PKC_USER` |
+| **Table** | `MEASUREMENT_HISTORY` |
+| **Columns** | `ID`, `TIMESTAMP`, `SOURCE_IP_ADDRESS`, `INPUT_VALUE`, `OUTPUT_VALUE` |
+
+---
+
+## Part 1 — On Oracle Linux VM (one time)
+
+### 1. Run database setup
+
+As **SYSTEM** on the VM (SQL*Plus or SQL Developer connected to **XEPDB1**):
+
+```bash
+sqlplus system@localhost:1521/XEPDB1
+```
+
+Then:
+
+```sql
+@/home/YOUR_USER/oraclequantapi/scripts/oracle-linux-setup.sql
+```
+
+(Or copy/paste the file contents.)
+
+### 2. Open firewall for DbVisualizer (from your Mac)
+
+```bash
+sudo firewall-cmd --add-port=1521/tcp --permanent
+sudo firewall-cmd --add-port=8081/tcp --permanent
+sudo firewall-cmd --reload
+```
+
+### 3. Start the API on the VM
+
+```bash
+chmod +x scripts/start-pkc-api-oracle-linux.sh
+./scripts/start-pkc-api-oracle-linux.sh /home/YOUR_USER/pkc-api.jar
+```
+
+Send a test request on the VM:
+
+```bash
+curl "http://localhost:8081/convert-measurements?input=aa"
+```
+
+---
+
+## Part 2 — DbVisualizer on your Mac
+
+### New connection
+
+1. **Database** → **Create Database Connection**
+2. **Name:** `OracleQuant PKC API (Linux VM)`
+3. **Database type:** Oracle
+4. **Driver:** Thin
+
+| Field | Value |
+|-------|--------|
+| **Server** | Your VM IP (e.g. `192.168.56.101`) — **not** `localhost` |
+| **Port** | `1521` |
+| **Database (Service)** | `XEPDB1` |
+| **Userid** | `pkc_user` |
+| **Password** | `PkcPassword1` |
+
+**Database URL:**
+
+```text
+jdbc:oracle:thin:@//YOUR_VM_IP:1521/XEPDB1
+```
+
+Replace `YOUR_VM_IP` with the Oracle Linux VM address from VirtualBox/VMware.
+
+5. **Connect**
+
+### Find your data
+
+In the object tree:
+
+```text
+OracleQuant PKC API (Linux VM)
+ └── PKC_USER
+ └── Tables
+ └── MEASUREMENT_HISTORY ← double-click → Data tab → Refresh
+```
+
+### Sample query
+
+```sql
+SELECT id, timestamp, source_ip_address, input_value, output_value
+FROM pkc_user.measurement_history
+ORDER BY id DESC;
+```
+
+---
+
+## If connection fails
+
+| Error | Fix |
+|-------|-----|
+| Connection refused | Oracle listener not running on VM; open port 1521 |
+| ORA-01017 | Wrong password — re-run `oracle-linux-setup.sql` |
+| ORA-01045 | User missing grants — re-run `oracle-linux-setup.sql` |
+| ORA-12514 | Wrong service — try `FREEPDB1` instead of `XEPDB1` |
+| Table empty | Start `pkc-api.jar` with `oracle` profile and call the API |
+
+---
+
+## Oracle 23c Free on Linux
+
+Use service **FREEPDB1** and run `scripts/fix-pkc-user-FREEPDB1.sql` instead, then:
+
+```text
+jdbc:oracle:thin:@//YOUR_VM_IP:1521/FREEPDB1
+```
diff --git a/dbvis/README-Docker-DbVisualizer.md b/dbvis/README-Docker-DbVisualizer.md
new file mode 100644
index 0000000..4631190
--- /dev/null
+++ b/dbvis/README-Docker-DbVisualizer.md
@@ -0,0 +1,13 @@
+# DbVisualizer → Docker Oracle (same DB as Spring Boot)
+
+| Setting | Value |
+|---------|--------|
+| **URL** | `jdbc:oracle:thin:@//localhost:1521/XEPDB1` |
+| **User** | `pkc_user` |
+| **Password** | `PkcPassword1` |
+
+**Table:** `MEASUREMENT_HISTORY` under schema `PKC_USER`
+
+Start **Docker** and **Spring Boot** (`docker` profile) before connecting.
+
+After `GET http://localhost:8081/convert-measurements?input=aa`, refresh the Data tab.
diff --git a/dbvis/README-Mac-DbVisualizer.md b/dbvis/README-Mac-DbVisualizer.md
new file mode 100644
index 0000000..e39c03e
--- /dev/null
+++ b/dbvis/README-Mac-DbVisualizer.md
@@ -0,0 +1,81 @@
+# Mac — Spring Boot + DbVisualizer (no Oracle Linux)
+
+Your app stores data in a **file on your Mac**:
+
+```
+/Users/mac/IdeaProjects/oraclequantapi/data/pkcdb.mv.db
+```
+
+Table: **`MEASUREMENT_HISTORY`**
+
+---
+
+## 1) Start Spring Boot on Mac
+
+**IntelliJ:** Run **Local Mac DbVisualizer**
+
+**Terminal:**
+```bash
+cd /Users/mac/IdeaProjects/oraclequantapi
+bash scripts/mac-start-local.sh
+```
+
+Or:
+```bash
+mvn spring-boot:run -Dspring-boot.run.profiles=local
+```
+
+Wait for: `Started OraclequantapiApplication` and profile **`local`**.
+
+---
+
+## 2) Send test data (Postman or browser)
+
+```
+GET http://localhost:8081/convert-measurements?input=aa
+GET http://localhost:8081/history
+```
+
+---
+
+## 3) DbVisualizer connection
+
+1. **Database** → **Create Database Connection**
+2. **Driver:** H2
+3. Settings:
+
+| Field | Value |
+|-------|--------|
+| **Database URL** | `jdbc:h2:file:/Users/mac/IdeaProjects/oraclequantapi/data/pkcdb;AUTO_SERVER=TRUE` |
+| **User** | `sa` |
+| **Password** | *(empty)* |
+
+4. **Connect** (while Spring Boot is running)
+
+---
+
+## 4) Open your table
+
+```
+Connection → PUBLIC → Tables → MEASUREMENT_HISTORY → Data → Refresh
+```
+
+SQL:
+```sql
+SELECT * FROM measurement_history ORDER BY id DESC;
+```
+
+After each Postman call, **Refresh** — new rows should appear.
+
+---
+
+## Linked?
+
+| Step | OK? |
+|------|-----|
+| App runs with profile `local` | ☐ |
+| `curl /history` shows data | ☐ |
+| DbVisualizer connects to H2 URL above | ☐ |
+| New row after API call + Refresh | ☐ |
+
+All yes = Spring Boot and DbVisualizer share the same database on your Mac.
diff --git a/dbvis/connection-oracle-linux.properties b/dbvis/connection-oracle-linux.properties
new file mode 100644
index 0000000..d9c3cff
--- /dev/null
+++ b/dbvis/connection-oracle-linux.properties
@@ -0,0 +1,12 @@
+# Copy these values into DbVisualizer → Create Database Connection
+# Replace YOUR_VM_IP with your Oracle Linux VM address (e.g. 192.168.56.101)
+
+ConnectionName=OracleQuant PKC API (Linux VM)
+Driver=Oracle Thin
+URL=jdbc:oracle:thin:@//YOUR_VM_IP:1521/XEPDB1
+User=pkc_user
+Password=PkcPassword1
+
+# Your table after setup + API runs:
+# Schema: PKC_USER
+# Table: MEASUREMENT_HISTORY
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
new file mode 100644
index 0000000..96bcca9
--- /dev/null
+++ b/docker/docker-compose.yml
@@ -0,0 +1,25 @@
+# Oracle XE for Spring Boot + DbVisualizer on Mac (Docker Desktop)
+# Start: docker compose -f docker/docker-compose.yml up -d
+# Stop: docker compose -f docker/docker-compose.yml down
+
+services:
+ oracle-xe:
+ image: container-registry.oracle.com/database/express:21.3.0-xe
+ container_name: oracle-xe
+ ports:
+ - "1521:1521"
+ - "5500:5500"
+ environment:
+ ORACLE_PWD: ${ORACLE_PWD:-29999login}
+ volumes:
+ - oracle-data:/opt/oracle/oradata
+ - ./oracle-init:/opt/oracle/scripts/setup
+ healthcheck:
+ test: ["CMD", "healthcheck.sh"]
+ interval: 30s
+ timeout: 10s
+ retries: 10
+ start_period: 120s
+
+volumes:
+ oracle-data:
diff --git a/docker/oracle-init/01-pkc-user.sql b/docker/oracle-init/01-pkc-user.sql
new file mode 100644
index 0000000..959f0d6
--- /dev/null
+++ b/docker/oracle-init/01-pkc-user.sql
@@ -0,0 +1,20 @@
+-- Runs once on first DB creation (Docker setup scripts)
+ALTER SESSION SET CONTAINER = XEPDB1;
+
+CREATE USER pkc_user IDENTIFIED BY PkcPassword1
+ DEFAULT TABLESPACE USERS
+ TEMPORARY TABLESPACE TEMP
+ QUOTA UNLIMITED ON USERS;
+
+GRANT CREATE SESSION, CONNECT, RESOURCE TO pkc_user;
+GRANT CREATE TABLE, CREATE SEQUENCE, UNLIMITED TABLESPACE TO pkc_user;
+
+ALTER SESSION SET CURRENT_SCHEMA = pkc_user;
+
+CREATE TABLE measurement_history (
+ id NUMBER(19,0) GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ timestamp TIMESTAMP NOT NULL,
+ source_ip_address VARCHAR2(255) NOT NULL,
+ input_value VARCHAR2(4000) NOT NULL,
+ output_value VARCHAR2(4000) NOT NULL
+);
diff --git a/docker/setup-pkc-user-grants-only.sql b/docker/setup-pkc-user-grants-only.sql
new file mode 100644
index 0000000..a0ba9fa
--- /dev/null
+++ b/docker/setup-pkc-user-grants-only.sql
@@ -0,0 +1,30 @@
+-- Safe re-run: grants + table (no DROP USER)
+ALTER SESSION SET CONTAINER = XEPDB1;
+
+BEGIN
+ EXECUTE IMMEDIATE 'CREATE USER pkc_user IDENTIFIED BY PkcPassword1 DEFAULT TABLESPACE USERS TEMPORARY TABLESPACE TEMP QUOTA UNLIMITED ON USERS';
+EXCEPTION
+ WHEN OTHERS THEN
+ IF SQLCODE != -1920 THEN RAISE; END IF;
+END;
+/
+
+GRANT CREATE SESSION, CONNECT, RESOURCE TO pkc_user;
+GRANT CREATE TABLE, CREATE SEQUENCE, UNLIMITED TABLESPACE TO pkc_user;
+
+ALTER SESSION SET CURRENT_SCHEMA = pkc_user;
+
+BEGIN
+ EXECUTE IMMEDIATE 'CREATE TABLE measurement_history (
+ id NUMBER(19,0) GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ timestamp TIMESTAMP NOT NULL,
+ source_ip_address VARCHAR2(255) NOT NULL,
+ input_value VARCHAR2(4000) NOT NULL,
+ output_value VARCHAR2(4000) NOT NULL)';
+EXCEPTION
+ WHEN OTHERS THEN
+ IF SQLCODE != -955 THEN RAISE; END IF;
+END;
+/
+
+COMMIT;
diff --git a/docker/setup-pkc-user-manual.sql b/docker/setup-pkc-user-manual.sql
new file mode 100644
index 0000000..5625363
--- /dev/null
+++ b/docker/setup-pkc-user-manual.sql
@@ -0,0 +1,38 @@
+-- Run if DB already existed before init script (manual fix)
+-- docker exec -i oracle-xe sqlplus -s system/29999login@//localhost:1521/XEPDB1 @/tmp/setup.sql
+
+ALTER SESSION SET CONTAINER = XEPDB1;
+
+BEGIN
+ EXECUTE IMMEDIATE 'DROP USER pkc_user CASCADE';
+EXCEPTION
+ WHEN OTHERS THEN
+ IF SQLCODE NOT IN (-1918, -01918) THEN RAISE; END IF;
+END;
+/
+
+CREATE USER pkc_user IDENTIFIED BY PkcPassword1
+ DEFAULT TABLESPACE USERS TEMPORARY TABLESPACE TEMP QUOTA UNLIMITED ON USERS;
+
+GRANT CREATE SESSION, CONNECT, RESOURCE TO pkc_user;
+GRANT CREATE TABLE, CREATE SEQUENCE, UNLIMITED TABLESPACE TO pkc_user;
+
+ALTER SESSION SET CURRENT_SCHEMA = pkc_user;
+
+BEGIN
+ EXECUTE IMMEDIATE 'DROP TABLE measurement_history CASCADE CONSTRAINTS';
+EXCEPTION
+ WHEN OTHERS THEN
+ IF SQLCODE != -942 THEN RAISE; END IF;
+END;
+/
+
+CREATE TABLE measurement_history (
+ id NUMBER(19,0) GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ timestamp TIMESTAMP NOT NULL,
+ source_ip_address VARCHAR2(255) NOT NULL,
+ input_value VARCHAR2(4000) NOT NULL,
+ output_value VARCHAR2(4000) NOT NULL
+);
+
+COMMIT;
diff --git a/pom.xml b/pom.xml
index 20909d2..1b4930f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,36 +5,50 @@
org.springframework.boot
spring-boot-starter-parent
- 3.5.14
-
+ 3.2.5
+
com.oraclequantapi
oraclequantapi
- 0.0.1-SNAPSHOT
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ 1.0
+ oraclequantapi
+ OracleQuant ERP Package Measurement Conversion API
+
17
+ 17
+ 17
+ UTF-8
+
org.springframework.boot
spring-boot-starter-web
-
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ com.h2database
+ h2
+ runtime
+
+
+ com.oracle.database.jdbc
+ ojdbc11
+ runtime
+
+
+ com.oracle.database.jdbc
+ ojdbc11
+ 23.3.0.23.09
+
org.springframework.boot
spring-boot-starter-test
@@ -43,10 +57,14 @@
+ pkc-api
org.springframework.boot
spring-boot-maven-plugin
+
+ com.oraclequantapi.oraclequantapi.OraclequantapiApplication
+
diff --git a/scripts/JdbcPing.class b/scripts/JdbcPing.class
new file mode 100644
index 0000000..49b1b7b
Binary files /dev/null and b/scripts/JdbcPing.class differ
diff --git a/scripts/docker-setup-db-only.sh b/scripts/docker-setup-db-only.sh
new file mode 100755
index 0000000..d8c607b
--- /dev/null
+++ b/scripts/docker-setup-db-only.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+# Create pkc_user + MEASUREMENT_HISTORY in running oracle-xe container
+# Uses SYSDBA inside container (no SYSTEM password needed)
+set -u
+
+PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
+CONTAINER="${ORACLE_CONTAINER:-oracle-xe}"
+
+if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER}$"; then
+ echo "Container ${CONTAINER} is not running."
+ echo "Start it with:"
+ echo " cd ~/Desktop/oracle-xe-db-hosting && docker compose up -d"
+ echo " OR"
+ echo " cd ${PROJECT_DIR} && docker compose -f docker/docker-compose.yml up -d"
+ exit 1
+fi
+
+echo "Setting up pkc_user via SYSDBA in container ${CONTAINER}..."
+docker cp "${PROJECT_DIR}/docker/setup-pkc-user-grants-only.sql" "${CONTAINER}:/tmp/setup-pkc-user.sql"
+
+if docker exec -i "${CONTAINER}" bash -c 'sqlplus -s / as sysdba @/tmp/setup-pkc-user.sql'; then
+ echo ""
+ echo "DB setup OK."
+ echo " DbVisualizer URL: jdbc:oracle:thin:@//localhost:1521/XEPDB1"
+ echo " User / Password: pkc_user / PkcPassword1"
+ echo " Table: PKC_USER.MEASUREMENT_HISTORY"
+else
+ echo ""
+ echo "DB setup failed. Last lines from container:"
+ docker logs "${CONTAINER}" --tail 20
+ exit 1
+fi
diff --git a/scripts/docker-start-all.sh b/scripts/docker-start-all.sh
new file mode 100755
index 0000000..5991f0f
--- /dev/null
+++ b/scripts/docker-start-all.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+# Mac: Docker Oracle XE + DB user + Spring Boot (profile docker)
+# Run from: /Users/mac/IdeaProjects/oraclequantapi
+set -u
+
+PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
+cd "${PROJECT_DIR}"
+
+CONTAINER="oracle-xe"
+
+echo "=== [1/4] Oracle XE container ==="
+if docker ps -a --format '{{.Names}}' | grep -qx "${CONTAINER}"; then
+ echo "Container '${CONTAINER}' already exists — using it (no conflict)."
+ docker start "${CONTAINER}" 2>/dev/null || true
+else
+ echo "Creating new container..."
+ docker compose -f docker/docker-compose.yml up -d
+fi
+
+if ! docker ps --format '{{.Names}}' | grep -qx "${CONTAINER}"; then
+ echo "ERROR: ${CONTAINER} is not running. Start Docker Desktop, then:"
+ echo " cd ~/Desktop/oracle-xe-db-hosting && docker compose up -d"
+ exit 1
+fi
+
+echo "=== [2/4] Waiting for Oracle ==="
+for i in $(seq 1 60); do
+ if docker exec "${CONTAINER}" healthcheck.sh >/dev/null 2>&1; then
+ echo "Oracle is healthy."
+ break
+ fi
+ if nc -z localhost 1521 2>/dev/null; then
+ echo "Port 1521 is open."
+ sleep 15
+ break
+ fi
+ echo " waiting... ($i/60)"
+ sleep 5
+done
+
+echo "=== [3/4] Creating pkc_user + MEASUREMENT_HISTORY (SYSDBA — no SYSTEM password) ==="
+bash "${PROJECT_DIR}/scripts/docker-setup-db-only.sh" || exit 1
+
+echo "=== [4/4] Starting Spring Boot (profile: docker) ==="
+echo " API: http://localhost:8081/convert-measurements?input=aa"
+echo " DbVisualizer: jdbc:oracle:thin:@//localhost:1521/XEPDB1"
+echo " User: pkc_user / PkcPassword1"
+echo ""
+
+mvn spring-boot:run -Dspring-boot.run.profiles=docker
diff --git a/scripts/fix-pkc-user-FREEPDB1.sql b/scripts/fix-pkc-user-FREEPDB1.sql
new file mode 100644
index 0000000..dc1ed04
--- /dev/null
+++ b/scripts/fix-pkc-user-FREEPDB1.sql
@@ -0,0 +1,25 @@
+-- Use this file ONLY if XEPDB1 does not exist (Oracle Database 23c Free on Mac).
+-- Run as SYSTEM with Service name = FREEPDB1
+
+ALTER SESSION SET CONTAINER = FREEPDB1;
+
+BEGIN
+ EXECUTE IMMEDIATE 'DROP USER pkc_user CASCADE';
+EXCEPTION
+ WHEN OTHERS THEN
+ IF SQLCODE NOT IN (-1918, -01918) THEN
+ RAISE;
+ END IF;
+END;
+/
+
+CREATE USER pkc_user IDENTIFIED BY PkcPassword1
+ DEFAULT TABLESPACE USERS
+ TEMPORARY TABLESPACE TEMP
+ QUOTA UNLIMITED ON USERS;
+
+GRANT CREATE SESSION, CONNECT, RESOURCE TO pkc_user;
+GRANT CREATE TABLE, CREATE SEQUENCE, UNLIMITED TABLESPACE TO pkc_user;
+
+SELECT privilege FROM dba_sys_privs WHERE grantee = 'PKC_USER';
+COMMIT;
diff --git a/scripts/fix-pkc-user-complete.sql b/scripts/fix-pkc-user-complete.sql
new file mode 100644
index 0000000..3119964
--- /dev/null
+++ b/scripts/fix-pkc-user-complete.sql
@@ -0,0 +1,46 @@
+-- =============================================================================
+-- FIX ORA-01045 — run as SYSTEM in SQL Developer
+-- Connection: Service name = XEPDB1 (or FREEPDB1 for Oracle 23c Free — see below)
+-- Press F5 (Run Script) on the WHOLE file
+-- =============================================================================
+
+SELECT SYS_CONTEXT('USERENV', 'CON_NAME') AS current_container FROM DUAL;
+
+ALTER SESSION SET CONTAINER = XEPDB1;
+
+SELECT SYS_CONTEXT('USERENV', 'CON_NAME') AS after_switch FROM DUAL;
+
+-- Clean start inside the pluggable database
+BEGIN
+ EXECUTE IMMEDIATE 'DROP USER pkc_user CASCADE';
+EXCEPTION
+ WHEN OTHERS THEN
+ IF SQLCODE NOT IN (-1918, -01918) THEN
+ RAISE;
+ END IF;
+END;
+/
+
+CREATE USER pkc_user IDENTIFIED BY PkcPassword1
+ DEFAULT TABLESPACE USERS
+ TEMPORARY TABLESPACE TEMP
+ QUOTA UNLIMITED ON USERS;
+
+GRANT CREATE SESSION TO pkc_user;
+GRANT CONNECT TO pkc_user;
+GRANT RESOURCE TO pkc_user;
+GRANT CREATE TABLE TO pkc_user;
+GRANT CREATE SEQUENCE TO pkc_user;
+GRANT UNLIMITED TABLESPACE TO pkc_user;
+
+-- MUST return at least one row: CREATE SESSION
+SELECT privilege
+FROM dba_sys_privs
+WHERE grantee = 'PKC_USER'
+ORDER BY privilege;
+
+SELECT username, account_status, default_tablespace
+FROM dba_users
+WHERE username = 'PKC_USER';
+
+COMMIT;
diff --git a/scripts/fix-pkc-user-grants.sql b/scripts/fix-pkc-user-grants.sql
new file mode 100644
index 0000000..fb32dd1
--- /dev/null
+++ b/scripts/fix-pkc-user-grants.sql
@@ -0,0 +1,16 @@
+-- Run as SYSTEM connected to XEPDB1 (F5 = Run Script)
+-- Fixes: ORA-01045 user PKC_USER lacks CREATE SESSION privilege
+
+ALTER SESSION SET CONTAINER = XEPDB1;
+
+GRANT CREATE SESSION TO pkc_user;
+GRANT CONNECT TO pkc_user;
+GRANT RESOURCE TO pkc_user;
+GRANT CREATE TABLE TO pkc_user;
+GRANT CREATE SEQUENCE TO pkc_user;
+GRANT UNLIMITED TABLESPACE TO pkc_user;
+
+-- Must show OPEN
+SELECT username, account_status FROM dba_users WHERE username = 'PKC_USER';
+
+COMMIT;
diff --git a/scripts/mac-send-jar-to-linux.sh b/scripts/mac-send-jar-to-linux.sh
new file mode 100755
index 0000000..a40b6d5
--- /dev/null
+++ b/scripts/mac-send-jar-to-linux.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+# =============================================================================
+# Run on your MAC — builds JAR and copies to Oracle Linux VM
+# Usage: bash scripts/mac-send-jar-to-linux.sh
+# bash scripts/mac-send-jar-to-linux.sh alharith@127.0.0.1
+# =============================================================================
+
+set -e
+PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
+cd "${PROJECT_DIR}"
+
+# Default: SSH via VirtualBox port-forward (set in VM Network -> Port Forwarding: 2222 -> 22)
+VM_SSH_TARGET="${1:-alharith@127.0.0.1}"
+VM_SSH_PORT="${VM_SSH_PORT:-2222}"
+
+echo "Building pkc-api.jar..."
+mvn -q clean package -DskipTests
+
+echo "Copying to Linux (${VM_SSH_TARGET})..."
+scp -P "${VM_SSH_PORT}" "${PROJECT_DIR}/target/pkc-api.jar" "${VM_SSH_TARGET}:/home/alharith/pkc-api.jar"
+
+echo "Copying setup script..."
+scp -P "${VM_SSH_PORT}" "${PROJECT_DIR}/scripts/oracle-linux-all-in-one.sh" "${VM_SSH_TARGET}:/home/alharith/"
+
+echo ""
+echo "Done. Now SSH to Linux and run:"
+echo " ssh -p ${VM_SSH_PORT} alharith@127.0.0.1"
+echo " bash /home/alharith/oracle-linux-all-in-one.sh"
diff --git a/scripts/mac-start-docker.sh b/scripts/mac-start-docker.sh
new file mode 100755
index 0000000..e8a0d37
--- /dev/null
+++ b/scripts/mac-start-docker.sh
@@ -0,0 +1,71 @@
+#!/bin/bash
+# Mac: Oracle (existing container) + Spring Boot in Docker + DbVisualizer
+set -u
+PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
+cd "${PROJECT_DIR}"
+NETWORK="pkc-net"
+CONTAINER_ORACLE="oracle-xe"
+CONTAINER_API="pkc-api"
+
+echo "=== [1/6] Stop old API ==="
+bash "${PROJECT_DIR}/scripts/stop-pkc-api.sh" 2>/dev/null || true
+docker rm -f "${CONTAINER_API}" 2>/dev/null || true
+
+echo "=== [2/6] Build JAR ==="
+mvn -q clean package -DskipTests
+test -f target/pkc-api.jar || { echo "Missing target/pkc-api.jar"; exit 1; }
+
+echo "=== [3/6] Start Oracle container ==="
+if ! docker ps --format '{{.Names}}' | grep -qx "${CONTAINER_ORACLE}"; then
+ if docker ps -a --format '{{.Names}}' | grep -qx "${CONTAINER_ORACLE}"; then
+ docker start "${CONTAINER_ORACLE}"
+ else
+ echo "Start Oracle first:"
+ echo " cd ~/Desktop/oracle-xe-db-hosting && docker compose up -d"
+ exit 1
+ fi
+fi
+
+echo "=== [4/6] Docker network ${NETWORK} (so API can reach oracle-xe by name) ==="
+docker network create "${NETWORK}" 2>/dev/null || true
+docker network connect "${NETWORK}" "${CONTAINER_ORACLE}" 2>/dev/null || true
+
+echo "=== [5/6] Wait for Oracle + setup pkc_user ==="
+for i in $(seq 1 40); do
+ if docker exec "${CONTAINER_ORACLE}" healthcheck.sh >/dev/null 2>&1; then
+ echo "Oracle healthy."
+ break
+ fi
+ sleep 5
+done
+bash "${PROJECT_DIR}/scripts/docker-setup-db-only.sh"
+
+echo "=== [6/6] Start API container on ${NETWORK} ==="
+docker run -d --name "${CONTAINER_API}" \
+ --network "${NETWORK}" \
+ -p 8081:8081 \
+ -v "${PROJECT_DIR}/target/pkc-api.jar:/app/pkc-api.jar:ro" \
+ -e SPRING_PROFILES_ACTIVE=docker-internal \
+ eclipse-temurin:17-jre \
+ java -jar /app/pkc-api.jar
+
+echo "Waiting for API..."
+for i in $(seq 1 40); do
+ if curl -sf "http://127.0.0.1:8081/convert-measurements?input=aa" >/dev/null 2>&1; then
+ echo ""
+ echo "=============================================="
+ echo " READY"
+ echo " Postman: http://127.0.0.1:8081/convert-measurements?input=aa"
+ echo " DbVisualizer: jdbc:oracle:thin:@//127.0.0.1:1521/XEPDB1"
+ echo " pkc_user / PkcPassword1"
+ echo " Table: PKC_USER.MEASUREMENT_HISTORY"
+ echo " API logs: docker logs -f pkc-api"
+ echo "=============================================="
+ exit 0
+ fi
+ sleep 3
+done
+
+echo "API not ready. Logs:"
+docker logs "${CONTAINER_API}" --tail 60
+exit 1
diff --git a/scripts/mac-start-local.sh b/scripts/mac-start-local.sh
new file mode 100755
index 0000000..9c2d7f8
--- /dev/null
+++ b/scripts/mac-start-local.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+# Start pkc-api on Mac with persistent H2 (DbVisualizer can connect)
+cd "$(dirname "$0")/.."
+mvn -q spring-boot:run -Dspring-boot.run.profiles=local
diff --git a/scripts/oracle-linux-all-in-one.sh b/scripts/oracle-linux-all-in-one.sh
new file mode 100755
index 0000000..f2fb22e
--- /dev/null
+++ b/scripts/oracle-linux-all-in-one.sh
@@ -0,0 +1,185 @@
+#!/bin/bash
+# =============================================================================
+# Oracle Linux — full setup for alharith@AlharithLinux VM
+# Run on the VM: bash oracle-linux-all-in-one.sh
+# =============================================================================
+
+set -u
+
+LINUX_USER="${LINUX_USER:-alharith}"
+HOME_DIR="/home/${LINUX_USER}"
+JAR="${HOME_DIR}/pkc-api.jar"
+LOG="${HOME_DIR}/pkc-api.out"
+ORACLE_SERVICE="${ORACLE_SERVICE:-XEPDB1}"
+ORACLE_USER="${ORACLE_USER:-pkc_user}"
+ORACLE_PASSWORD="${ORACLE_PASSWORD:-PkcPassword1}"
+ORACLE_JDBC_URL="jdbc:oracle:thin:@//localhost:1521/${ORACLE_SERVICE}"
+API_PORT="${PORT:-8081}"
+
+echo "=============================================="
+echo " PKC-API Oracle Linux setup"
+echo " User: ${LINUX_USER} Service: ${ORACLE_SERVICE}"
+echo "=============================================="
+
+# --- 1) Java ---
+if ! command -v java &>/dev/null; then
+ echo "[1] Installing Java 17..."
+ sudo dnf install -y java-17-openjdk-devel || sudo yum install -y java-17-openjdk-devel
+fi
+echo "[1] Java: $(java -version 2>&1 | head -1)"
+
+# --- 2) JAR ---
+if [[ ! -f "${JAR}" ]]; then
+ echo "[2] ERROR: Missing ${JAR}"
+ echo " Copy from Mac:"
+ echo " scp /Users/mac/IdeaProjects/oraclequantapi/target/pkc-api.jar ${LINUX_USER}@:${JAR}"
+ exit 1
+fi
+echo "[2] JAR OK: $(ls -lh "${JAR}" | awk '{print $5, $9}')"
+
+# --- 3) Oracle listener ---
+echo "[3] Checking Oracle listener..."
+if command -v lsnrctl &>/dev/null; then
+ lsnrctl status | head -20 || true
+else
+ echo " lsnrctl not in PATH (Oracle may still run)"
+fi
+
+# --- 4) Database user + table ---
+SETUP_SQL="${HOME_DIR}/oracle-linux-setup.sql"
+cat > "${SETUP_SQL}" <<'SQL'
+ALTER SESSION SET CONTAINER = XEPDB1;
+
+BEGIN
+ EXECUTE IMMEDIATE 'DROP USER pkc_user CASCADE';
+EXCEPTION
+ WHEN OTHERS THEN
+ IF SQLCODE NOT IN (-1918, -01918) THEN RAISE; END IF;
+END;
+/
+
+CREATE USER pkc_user IDENTIFIED BY PkcPassword1
+ DEFAULT TABLESPACE USERS
+ TEMPORARY TABLESPACE TEMP
+ QUOTA UNLIMITED ON USERS;
+
+GRANT CREATE SESSION, CONNECT, RESOURCE TO pkc_user;
+GRANT CREATE TABLE, CREATE SEQUENCE, UNLIMITED TABLESPACE TO pkc_user;
+
+ALTER SESSION SET CURRENT_SCHEMA = pkc_user;
+
+BEGIN
+ EXECUTE IMMEDIATE 'DROP TABLE measurement_history CASCADE CONSTRAINTS';
+EXCEPTION
+ WHEN OTHERS THEN
+ IF SQLCODE != -942 THEN RAISE; END IF;
+END;
+/
+
+CREATE TABLE measurement_history (
+ id NUMBER(19,0) GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ timestamp TIMESTAMP NOT NULL,
+ source_ip_address VARCHAR2(255) NOT NULL,
+ input_value VARCHAR2(4000) NOT NULL,
+ output_value VARCHAR2(4000) NOT NULL
+);
+
+SELECT privilege FROM dba_sys_privs WHERE grantee = 'PKC_USER' ORDER BY privilege;
+COMMIT;
+SQL
+
+# Allow FREEPDB1
+if [[ "${ORACLE_SERVICE}" == "FREEPDB1" ]]; then
+ sed -i 's/XEPDB1/FREEPDB1/g' "${SETUP_SQL}"
+fi
+
+SQLPLUS=""
+for p in $(find /opt /u01 /home/oracle 2>/dev/null -name sqlplus -type f 2>/dev/null); do
+ SQLPLUS="$p"
+ break
+done
+[[ -z "${SQLPLUS}" ]] && command -v sqlplus &>/dev/null && SQLPLUS="sqlplus"
+
+echo "[4] Database setup..."
+if [[ -n "${SQLPLUS}" ]]; then
+ if [[ -z "${ORACLE_SYSTEM_PASSWORD:-}" ]]; then
+ echo " Enter SYSTEM password for Oracle (install password):"
+ read -rs ORACLE_SYSTEM_PASSWORD
+ echo
+ fi
+ echo "${ORACLE_SYSTEM_PASSWORD}" | "${SQLPLUS}" -S "system@localhost:1521/${ORACLE_SERVICE}" @"${SETUP_SQL}" \
+ && echo " DB setup OK" \
+ || echo " DB setup FAILED — run ${SETUP_SQL} manually in SQL Developer as SYSTEM"
+else
+ echo " sqlplus not found — run this file in SQL Developer as SYSTEM@${ORACLE_SERVICE}:"
+ echo " ${SETUP_SQL}"
+fi
+
+# --- 5) Firewall ---
+echo "[5] Opening ports 1521 and ${API_PORT}..."
+if command -v firewall-cmd &>/dev/null; then
+ sudo firewall-cmd --add-port=1521/tcp --permanent 2>/dev/null || true
+ sudo firewall-cmd --add-port=${API_PORT}/tcp --permanent 2>/dev/null || true
+ sudo firewall-cmd --reload 2>/dev/null || true
+fi
+
+# --- 6) Stop old app ---
+echo "[6] Stopping old pkc-api processes..."
+pkill -f "pkc-api.jar" 2>/dev/null || true
+sleep 2
+
+# --- 7) Start app ---
+echo "[7] Starting pkc-api.jar..."
+export SPRING_PROFILES_ACTIVE=oracle
+export ORACLE_JDBC_URL="${ORACLE_JDBC_URL}"
+export ORACLE_USER="${ORACLE_USER}"
+export ORACLE_PASSWORD="${ORACLE_PASSWORD}"
+
+cd "${HOME_DIR}"
+nohup java -Dspring.profiles.active=oracle -jar "${JAR}" > "${LOG}" 2>&1 &
+APP_PID=$!
+echo " PID: ${APP_PID}"
+echo " Log: ${LOG}"
+
+# --- 8) Wait for startup ---
+echo "[8] Waiting for port ${API_PORT}..."
+for i in $(seq 1 30); do
+ if ss -tlnp 2>/dev/null | grep -q ":${API_PORT} "; then
+ echo " Port ${API_PORT} is OPEN"
+ break
+ fi
+ if ! kill -0 "${APP_PID}" 2>/dev/null; then
+ echo " Process died. Last log lines:"
+ tail -40 "${LOG}"
+ exit 1
+ fi
+ sleep 2
+done
+
+if ! ss -tlnp 2>/dev/null | grep -q ":${API_PORT} "; then
+ echo " Port ${API_PORT} still closed. Log:"
+ tail -50 "${LOG}"
+ echo ""
+ echo " If you see ORA-01045 or ORA-01017: fix DB with ${SETUP_SQL}"
+ echo " If ORA-12514: try: ORACLE_SERVICE=FREEPDB1 bash $0"
+ exit 1
+fi
+
+# --- 9) Test API ---
+echo "[9] Testing API..."
+curl -sf "http://localhost:${API_PORT}/convert-measurements?input=aa" && echo ""
+curl -sf "http://localhost:${API_PORT}/history" | head -c 500 && echo ""
+
+echo ""
+echo "=============================================="
+echo " DONE — API is running on Oracle Linux"
+echo "=============================================="
+echo " VM test: curl http://localhost:${API_PORT}/convert-measurements?input=aa"
+echo " Table: PKC_USER.MEASUREMENT_HISTORY"
+echo ""
+echo " DbVisualizer on Mac (set VirtualBox port forward 1521 first):"
+echo " URL: jdbc:oracle:thin:@//127.0.0.1:1521/${ORACLE_SERVICE}"
+echo " User: ${ORACLE_USER}"
+echo " Password: ${ORACLE_PASSWORD}"
+echo " Tree: PKC_USER -> Tables -> MEASUREMENT_HISTORY"
+echo "=============================================="
diff --git a/scripts/oracle-linux-setup.sql b/scripts/oracle-linux-setup.sql
new file mode 100644
index 0000000..d6f2ba9
--- /dev/null
+++ b/scripts/oracle-linux-setup.sql
@@ -0,0 +1,60 @@
+-- =============================================================================
+-- Run ON ORACLE LINUX (as SYSTEM) — SQL*Plus or SQL Developer on the VM
+-- Service: XEPDB1 (or FREEPDB1 for Oracle 23c Free — change below)
+-- Creates: user PKC_USER + table MEASUREMENT_HISTORY (your program data)
+-- =============================================================================
+
+ALTER SESSION SET CONTAINER = XEPDB1;
+
+BEGIN
+ EXECUTE IMMEDIATE 'DROP USER pkc_user CASCADE';
+EXCEPTION
+ WHEN OTHERS THEN
+ IF SQLCODE NOT IN (-1918, -01918) THEN
+ RAISE;
+ END IF;
+END;
+/
+
+CREATE USER pkc_user IDENTIFIED BY PkcPassword1
+ DEFAULT TABLESPACE USERS
+ TEMPORARY TABLESPACE TEMP
+ QUOTA UNLIMITED ON USERS;
+
+GRANT CREATE SESSION, CONNECT, RESOURCE TO pkc_user;
+GRANT CREATE TABLE, CREATE SEQUENCE, UNLIMITED TABLESPACE TO pkc_user;
+
+ALTER SESSION SET CURRENT_SCHEMA = pkc_user;
+
+-- Your application's history table (Hibernate also maintains this)
+BEGIN
+ EXECUTE IMMEDIATE 'DROP TABLE measurement_history CASCADE CONSTRAINTS';
+EXCEPTION
+ WHEN OTHERS THEN
+ IF SQLCODE != -942 THEN
+ RAISE;
+ END IF;
+END;
+/
+
+CREATE TABLE measurement_history (
+ id NUMBER(19, 0) GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ timestamp TIMESTAMP NOT NULL,
+ source_ip_address VARCHAR2(255) NOT NULL,
+ input_value VARCHAR2(4000) NOT NULL,
+ output_value VARCHAR2(4000) NOT NULL
+);
+
+COMMENT ON TABLE measurement_history IS 'Package conversion API request history (pkc-api)';
+
+SELECT 'User OK' AS step, username, account_status
+FROM dba_users WHERE username = 'PKC_USER';
+
+SELECT 'Privileges' AS step, privilege
+FROM dba_sys_privs WHERE grantee = 'PKC_USER' ORDER BY privilege;
+
+SELECT 'Table OK' AS step, table_name
+FROM all_tables
+WHERE owner = 'PKC_USER' AND table_name = 'MEASUREMENT_HISTORY';
+
+COMMIT;
diff --git a/scripts/setup-oracle-xe.sql b/scripts/setup-oracle-xe.sql
new file mode 100644
index 0000000..2d8db83
--- /dev/null
+++ b/scripts/setup-oracle-xe.sql
@@ -0,0 +1,41 @@
+-- =============================================================================
+-- Run in Oracle SQL Developer as SYSTEM (password from your Oracle install)
+-- Connection MUST use Service name: XEPDB1 (not just "OracleXE" CDB only)
+-- =============================================================================
+-- How to connect in SQL Developer:
+-- 1. New Connection → User: SYSTEM, Role: default
+-- 2. Connection Type: Basic → Host: localhost, Port: 1521
+-- 3. Service name: XEPDB1 (important!)
+-- 4. Test → Connect
+-- 5. Open this file → press F5 (Run Script) to run ALL lines
+-- =============================================================================
+
+ALTER SESSION SET CONTAINER = XEPDB1;
+
+-- Remove old user if a previous attempt failed halfway
+BEGIN
+ EXECUTE IMMEDIATE 'DROP USER pkc_user CASCADE';
+EXCEPTION
+ WHEN OTHERS THEN
+ IF SQLCODE != -1918 THEN
+ RAISE;
+ END IF;
+END;
+/
+
+CREATE USER pkc_user IDENTIFIED BY "PkcPassword1"
+ DEFAULT TABLESPACE USERS
+ QUOTA UNLIMITED ON USERS;
+
+GRANT CREATE SESSION TO pkc_user;
+GRANT CONNECT TO pkc_user;
+GRANT RESOURCE TO pkc_user;
+GRANT CREATE TABLE TO pkc_user;
+GRANT CREATE SEQUENCE TO pkc_user;
+
+-- Verify (should show 1 row: OPEN)
+SELECT username, account_status, default_tablespace
+FROM dba_users
+WHERE username = 'PKC_USER';
+
+COMMIT;
diff --git a/scripts/start-pkc-api-oracle-linux.sh b/scripts/start-pkc-api-oracle-linux.sh
new file mode 100755
index 0000000..6eedd6a
--- /dev/null
+++ b/scripts/start-pkc-api-oracle-linux.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+# Run on Oracle Linux VM (same machine as Oracle XE)
+set -e
+cd "$(dirname "$0")/.."
+
+export SPRING_PROFILES_ACTIVE=oracle
+export ORACLE_JDBC_URL="${ORACLE_JDBC_URL:-jdbc:oracle:thin:@//localhost:1521/XEPDB1}"
+export ORACLE_USER="${ORACLE_USER:-pkc_user}"
+export ORACLE_PASSWORD="${ORACLE_PASSWORD:-PkcPassword1}"
+
+JAR="${1:-./pkc-api.jar}"
+if [[ ! -f "$JAR" ]]; then
+ echo "Missing JAR: $JAR"
+ echo "Usage: ./scripts/start-pkc-api-oracle-linux.sh /path/to/pkc-api.jar"
+ exit 1
+fi
+
+echo "Starting pkc-api with Oracle XE..."
+echo " URL: $ORACLE_JDBC_URL"
+echo " User: $ORACLE_USER"
+nohup java -Dspring.profiles.active=oracle \
+ -jar "$JAR" > pkc-api.out 2>&1 &
+echo "PID: $!"
+echo "Log: tail -f pkc-api.out"
+echo "API: http://$(hostname -I | awk '{print $1}'):8081/convert-measurements?input=aa"
diff --git a/scripts/stop-pkc-api.sh b/scripts/stop-pkc-api.sh
new file mode 100755
index 0000000..c1285d0
--- /dev/null
+++ b/scripts/stop-pkc-api.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+# Stop anything using port 8081 and old pkc-api / spring-boot runs
+echo "Stopping processes on port 8081..."
+for pid in $(lsof -ti :8081 2>/dev/null); do
+ echo " kill PID $pid"
+ kill "$pid" 2>/dev/null || kill -9 "$pid" 2>/dev/null
+done
+
+pkill -f "oraclequantapi.*spring-boot:run" 2>/dev/null || true
+pkill -f "pkc-api.jar" 2>/dev/null || true
+pkill -f "OraclequantapiApplication" 2>/dev/null || true
+docker stop pkc-api 2>/dev/null || true
+
+sleep 2
+if lsof -i :8081 >/dev/null 2>&1; then
+ echo "Port 8081 still in use — force kill:"
+ lsof -ti :8081 | xargs kill -9 2>/dev/null || true
+fi
+
+if lsof -i :8081 >/dev/null 2>&1; then
+ echo "ERROR: port 8081 still busy"
+ lsof -i :8081
+ exit 1
+fi
+
+echo "Port 8081 is free. You can start Spring Boot now."
diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplication.java b/src/main/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplication.java
index 5e28689..9fb794a 100644
--- a/src/main/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplication.java
+++ b/src/main/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplication.java
@@ -3,10 +3,15 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+ // Application entry point.
+ // components under this package
+
@SpringBootApplication
public class OraclequantapiApplication {
public static void main(String[] args) {
+
+ // Bootstraps Spring context then listens on server port
SpringApplication.run(OraclequantapiApplication.class, args);
}
diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/controller/HistoryController.java b/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/controller/HistoryController.java
new file mode 100644
index 0000000..ace47aa
--- /dev/null
+++ b/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/controller/HistoryController.java
@@ -0,0 +1,66 @@
+package com.oraclequantapi.oraclequantapi.alharith_alsubhi.packageconversion.controller;
+
+import com.oraclequantapi.oraclequantapi.alharith_alsubhi.packageconversion.model.HistoryRecord;
+import com.oraclequantapi.oraclequantapi.alharith_alsubhi.packageconversion.service.HistoryService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.server.ResponseStatusException;
+
+import java.sql.SQLOutput;
+import java.util.List;
+
+
+ // Web layer — CRUD-style REST API for persisted conversion history.
+@RestController
+@RequestMapping("/history")
+public class HistoryController {
+
+ private final HistoryService historyService;
+
+ @Autowired
+ public HistoryController(HistoryService historyService) {
+ this.historyService = historyService;
+ }
+
+ // GET /history — list every saved request (newest order depends on DB default). */
+ @GetMapping
+ public ResponseEntity> getAllHistory() {
+ List history = historyService.getAllRecords();
+ return ResponseEntity.ok(history);
+ }
+
+ // GET /history/{id} — fetch one record; 404 if id does not exist. */
+ @GetMapping("/{id}")
+ public ResponseEntity getHistoryById(@PathVariable Long id) {
+ HistoryRecord record = historyService.getRecordById(id)
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "History record not found with ID " + id));
+ return ResponseEntity.ok(record);
+ }
+
+ // PUT /history/{id} — replace fields on an existing row (full update). */
+ @PutMapping("/{id}")
+ public ResponseEntity updateHistory(
+ @PathVariable Long id,
+ @RequestBody HistoryRecord updatedRecord) {
+ HistoryRecord saved = historyService.updateRecord(id, updatedRecord);
+ return ResponseEntity.ok(saved);
+ }
+
+ // PATCH /history/{id} — change only fields sent in JSON body (partial update). */
+ @PatchMapping("/{id}")
+ public ResponseEntity patchHistory(
+ @PathVariable Long id,
+ @RequestBody HistoryRecord partialRecord) {
+ HistoryRecord saved = historyService.patchRecord(id, partialRecord);
+ return ResponseEntity.ok(saved);
+ }
+
+ // DELETE /history — wipe entire history table (204 No Content). */
+ @DeleteMapping
+ public ResponseEntity clearAllHistory() {
+ historyService.clearHistory();
+ return ResponseEntity.noContent().build();
+ }
+}
diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/controller/MeasurementController.java b/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/controller/MeasurementController.java
new file mode 100644
index 0000000..6da6d35
--- /dev/null
+++ b/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/controller/MeasurementController.java
@@ -0,0 +1,50 @@
+package com.oraclequantapi.oraclequantapi.alharith_alsubhi.packageconversion.controller;
+
+import com.oraclequantapi.oraclequantapi.alharith_alsubhi.packageconversion.service.MeasurementService;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+ // Web layer exposes the conversion REST endpoint.
+@RestController
+public class MeasurementController {
+
+ private final MeasurementService measurementService;
+
+ @Autowired
+ public MeasurementController(MeasurementService measurementService) {
+ this.measurementService = measurementService;
+ }
+
+ // Returns JSON array of package totals
+ @GetMapping("/convert-measurements")
+ public ResponseEntity> convertMeasurements(
+ @RequestParam(name = "input", required = false) String input,
+ @RequestParam(name = "convert-measurements", required = false) String convertMeasurements,
+ HttpServletRequest request) {
+
+ // Read query string
+ String targetInput = input != null ? input : convertMeasurements;
+ if (targetInput == null) {
+ targetInput = ""; // Hint: empty input → empty result list, not an error.
+ }
+
+ // Capture client IP for history
+ String clientIp = request.getRemoteAddr();
+ String xForwardedFor = request.getHeader("X-Forwarded-For");
+ if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
+ clientIp = xForwardedFor.split(",")[0].trim();
+ }
+
+ // Delegate: convert, log, and save row in database.
+ List result = measurementService.convertAndLog(targetInput, clientIp);
+
+ // Return 200 OK
+ return ResponseEntity.ok(result);
+ }
+}
diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/model/HistoryRecord.java b/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/model/HistoryRecord.java
new file mode 100644
index 0000000..5cb6924
--- /dev/null
+++ b/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/model/HistoryRecord.java
@@ -0,0 +1,79 @@
+package com.oraclequantapi.oraclequantapi.alharith_alsubhi.packageconversion.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import jakarta.persistence.*;
+import java.time.LocalDateTime;
+
+ // Domain and database entity gives one row per conversion request.
+@Entity
+@Table(name = "MEASUREMENT_HISTORY")
+public class HistoryRecord {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id; // auto-increment primary key
+
+ @Column(name = "TIMESTAMP", nullable = false)
+ private LocalDateTime timestamp; // when the request was processed
+
+ @JsonProperty("source_ip_address")
+ @Column(name = "SOURCE_IP_ADDRESS", nullable = false)
+ private String sourceIpAddress;
+
+ @Column(name = "INPUT_VALUE", length = 4000, nullable = false)
+ private String input; // raw query string
+
+ @Column(name = "OUTPUT_VALUE", length = 4000, nullable = false)
+ private String output; // JSON array as text
+
+ public HistoryRecord() {
+ }
+
+ // Measurement Service Impl when saving a new history row
+ public HistoryRecord(LocalDateTime timestamp, String sourceIpAddress, String input, String output) {
+ this.timestamp = timestamp;
+ this.sourceIpAddress = sourceIpAddress;
+ this.input = input;
+ this.output = output;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public LocalDateTime getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(LocalDateTime timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public String getSourceIpAddress() {
+ return sourceIpAddress;
+ }
+
+ public void setSourceIpAddress(String sourceIpAddress) {
+ this.sourceIpAddress = sourceIpAddress;
+ }
+
+ public String getInput() {
+ return input;
+ }
+
+ public void setInput(String input) {
+ this.input = input;
+ }
+
+ public String getOutput() {
+ return output;
+ }
+
+ public void setOutput(String output) {
+ this.output = output;
+ }
+}
diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/repository/HistoryRepository.java b/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/repository/HistoryRepository.java
new file mode 100644
index 0000000..e3f8fd8
--- /dev/null
+++ b/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/repository/HistoryRepository.java
@@ -0,0 +1,12 @@
+package com.oraclequantapi.oraclequantapi.alharith_alsubhi.packageconversion.repository;
+
+import com.oraclequantapi.oraclequantapi.alharith_alsubhi.packageconversion.model.HistoryRecord;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+ // Data access layer — talks to the database.
+ // JpaRepository provides save, findAll, findById and deleteAll with no SQL code.
+@Repository
+public interface HistoryRepository extends JpaRepository {
+ // Long is type of @Id field on HistoryRecord
+}
diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/service/ConversionEngine.java b/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/service/ConversionEngine.java
new file mode 100644
index 0000000..5f6aceb
--- /dev/null
+++ b/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/service/ConversionEngine.java
@@ -0,0 +1,70 @@
+package com.oraclequantapi.oraclequantapi.alharith_alsubhi.packageconversion.service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+ // Core algorithm — no Spring dependencies (easy to unit test).
+ // Two phases for decode string to numbers and group numbers into packages.
+public class ConversionEngine {
+
+ // Converts encoded measurement text into package inflow totals.
+ public static List convert(String input) {
+ // Guard is null
+ if (input == null || input.trim().isEmpty()) {
+ return List.of();
+ }
+
+ String cleanInput = input.trim();
+ List parsedNumbers = new ArrayList<>();
+ int i = 0;
+ int n = cleanInput.length();
+
+ // Scan left to right
+ while (i < n) {
+ int currentSum = 0;
+
+ while (i < n && Character.toLowerCase(cleanInput.charAt(i)) == 'z') {
+ currentSum += 26;
+ i++;
+ }
+
+ if (i < n) {
+ char ch = Character.toLowerCase(cleanInput.charAt(i));
+ int val = 0;
+ if (ch == '_') {
+ val = 0;
+ } else if (ch >= 'a' && ch <= 'y') {
+ val = ch - 'a' + 1;
+ } else {
+ val = 0; // for unknown chars replace 0
+ }
+ currentSum += val;
+ i++;
+ }
+ parsedNumbers.add(currentSum);
+ }
+
+ // Build packages
+ List packageTotals = new ArrayList<>();
+ int index = 0;
+ int numCount = parsedNumbers.size();
+
+ while (index < numCount) {
+ int count = parsedNumbers.get(index); // package size (how many values to read)
+ index++;
+
+ int totalValue = 0;
+ for (int k = 0; k < count; k++) {
+ if (index < numCount) {
+ totalValue += parsedNumbers.get(index);
+ index++;
+ } else {
+ break;
+ }
+ }
+ packageTotals.add(totalValue);
+ }
+
+ return packageTotals;
+ }
+}
diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/service/HistoryService.java b/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/service/HistoryService.java
new file mode 100644
index 0000000..a028174
--- /dev/null
+++ b/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/service/HistoryService.java
@@ -0,0 +1,26 @@
+package com.oraclequantapi.oraclequantapi.alharith_alsubhi.packageconversion.service;
+
+import com.oraclequantapi.oraclequantapi.alharith_alsubhi.packageconversion.model.HistoryRecord;
+
+import java.util.List;
+import java.util.Optional;
+
+ // Service contract for history CRUD.
+ // HistoryController talks to this interface not the repository directly
+public interface HistoryService {
+
+ // All rows
+ List getAllRecords();
+
+ // One row by primary key
+ Optional getRecordById(Long id);
+
+ // Full update
+ HistoryRecord updateRecord(Long id, HistoryRecord updatedRecord);
+
+ // Partial update
+ HistoryRecord patchRecord(Long id, HistoryRecord partialRecord);
+
+ // Delete all rows
+ void clearHistory();
+}
diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/service/HistoryServiceImpl.java b/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/service/HistoryServiceImpl.java
new file mode 100644
index 0000000..0874941
--- /dev/null
+++ b/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/service/HistoryServiceImpl.java
@@ -0,0 +1,91 @@
+package com.oraclequantapi.oraclequantapi.alharith_alsubhi.packageconversion.service;
+
+import com.oraclequantapi.oraclequantapi.alharith_alsubhi.packageconversion.model.HistoryRecord;
+import com.oraclequantapi.oraclequantapi.alharith_alsubhi.packageconversion.repository.HistoryRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.web.server.ResponseStatusException;
+
+import java.util.List;
+import java.util.Optional;
+
+ // Business logic for history endpoints
+ // Sits between HistoryController and HistoryRepository
+@Service
+public class HistoryServiceImpl implements HistoryService {
+
+ private static final Logger logger = LoggerFactory.getLogger(HistoryServiceImpl.class);
+
+ private final HistoryRepository historyRepository;
+
+ @Autowired
+ public HistoryServiceImpl(HistoryRepository historyRepository) {
+ this.historyRepository = historyRepository;
+ }
+
+ @Override
+ public List getAllRecords() {
+ logger.info("Fetching all history records from database.");
+ return historyRepository.findAll();
+ }
+
+ @Override
+ public Optional getRecordById(Long id) {
+ logger.info("Fetching history record by ID: {}", id);
+ return historyRepository.findById(id);
+ }
+
+ @Override
+ public HistoryRecord updateRecord(Long id, HistoryRecord updatedRecord) {
+ logger.info("Completely updating history record with ID: {}", id);
+ return historyRepository.findById(id)
+ .map(existing -> {
+
+ // overwrite each field when provided and others keep old value
+ existing.setTimestamp(updatedRecord.getTimestamp() != null ? updatedRecord.getTimestamp() : existing.getTimestamp());
+ existing.setSourceIpAddress(updatedRecord.getSourceIpAddress() != null ? updatedRecord.getSourceIpAddress() : existing.getSourceIpAddress());
+ existing.setInput(updatedRecord.getInput() != null ? updatedRecord.getInput() : existing.getInput());
+ existing.setOutput(updatedRecord.getOutput() != null ? updatedRecord.getOutput() : existing.getOutput());
+ HistoryRecord saved = historyRepository.save(existing);
+ logger.debug("Successfully updated history record: {}", id);
+ return saved;
+ })
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "History record not found with ID " + id));
+ }
+
+ @Override
+ public HistoryRecord patchRecord(Long id, HistoryRecord partialRecord) {
+ logger.info("Partially patching history record with ID: {}", id);
+ return historyRepository.findById(id)
+ .map(existing -> {
+
+ // only not null fields from JSON body are applied
+ if (partialRecord.getTimestamp() != null) {
+ existing.setTimestamp(partialRecord.getTimestamp());
+ }
+ if (partialRecord.getSourceIpAddress() != null) {
+ existing.setSourceIpAddress(partialRecord.getSourceIpAddress());
+ }
+ if (partialRecord.getInput() != null) {
+ existing.setInput(partialRecord.getInput());
+ }
+ if (partialRecord.getOutput() != null) {
+ existing.setOutput(partialRecord.getOutput());
+ }
+ HistoryRecord saved = historyRepository.save(existing);
+ logger.debug("Successfully patched history record: {}", id);
+ return saved;
+ })
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "History record not found with ID " + id));
+ }
+
+ @Override
+ public void clearHistory() {
+ logger.warn("Clearing all history records from the database!");
+ historyRepository.deleteAll(); // removes every row and table structure remains
+ logger.info("All history records cleared successfully.");
+ }
+}
diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/service/MeasurementService.java b/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/service/MeasurementService.java
new file mode 100644
index 0000000..6f0f0f2
--- /dev/null
+++ b/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/service/MeasurementService.java
@@ -0,0 +1,13 @@
+package com.oraclequantapi.oraclequantapi.alharith_alsubhi.packageconversion.service;
+
+import java.util.List;
+
+
+ // Service contract for conversion requests
+ // Interface keeps controller decoupled from MeasurementServiceImpl (polymorphism /testing)
+ // Run conversion, write logs, and persist one history row
+
+public interface MeasurementService {
+
+ List convertAndLog(String input, String sourceIpAddress);
+}
diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/service/MeasurementServiceImpl.java b/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/service/MeasurementServiceImpl.java
new file mode 100644
index 0000000..313dd80
--- /dev/null
+++ b/src/main/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/service/MeasurementServiceImpl.java
@@ -0,0 +1,64 @@
+package com.oraclequantapi.oraclequantapi.alharith_alsubhi.packageconversion.service;
+
+import com.oraclequantapi.oraclequantapi.alharith_alsubhi.packageconversion.model.HistoryRecord;
+import com.oraclequantapi.oraclequantapi.alharith_alsubhi.packageconversion.repository.HistoryRepository;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+ // Orchestrates conversion and audit trail.
+@Service
+public class MeasurementServiceImpl implements MeasurementService {
+
+ private static final Logger logger = LoggerFactory.getLogger(MeasurementServiceImpl.class);
+
+ private final HistoryRepository historyRepository;
+ private final ObjectMapper objectMapper;
+
+ @Autowired
+ public MeasurementServiceImpl(HistoryRepository historyRepository, ObjectMapper objectMapper) {
+ this.historyRepository = historyRepository;
+ this.objectMapper = objectMapper;
+ }
+
+ @Override
+ public List convertAndLog(String input, String sourceIpAddress) {
+ // Log incoming request
+ logger.info("Incoming measurement conversion request. Source IP: {}, Input: '{}'", sourceIpAddress, input);
+
+ // Run pure conversion logic
+ List result = ConversionEngine.convert(input);
+
+ // Serialize result as JSON string for the history
+ String jsonOutput = "[]";
+ try {
+ jsonOutput = objectMapper.writeValueAsString(result);
+ } catch (Exception e) {
+ logger.error("Error serializing conversion result to JSON: {}", e.getMessage(), e);
+ jsonOutput = result.toString(); // fallback so API still returns numbers to client
+ }
+
+ // Persist history
+ try {
+ HistoryRecord record = new HistoryRecord(
+ LocalDateTime.now(),
+ sourceIpAddress,
+ input != null ? input : "",
+ jsonOutput
+ );
+ historyRepository.save(record);
+ logger.debug("Successfully saved history record to database for Input: '{}'", input);
+ } catch (Exception e) {
+ // conversion still succeeds even if DB save fails
+ logger.error("Failed to persist history record to database: {}", e.getMessage(), e);
+ }
+
+ logger.info("Successfully processed conversion request. Output: {}", jsonOutput);
+ return result;
+ }
+}
diff --git a/src/main/resources/application-docker-internal.properties b/src/main/resources/application-docker-internal.properties
new file mode 100644
index 0000000..33082bb
--- /dev/null
+++ b/src/main/resources/application-docker-internal.properties
@@ -0,0 +1,14 @@
+# Spring Boot running INSIDE Docker (same network as oracle-xe container)
+spring.datasource.url=jdbc:oracle:thin:@//oracle-xe:1521/XEPDB1
+spring.datasource.username=pkc_user
+spring.datasource.password=PkcPassword1
+spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
+
+spring.datasource.hikari.connection-timeout=30000
+spring.datasource.hikari.maximum-pool-size=5
+
+spring.jpa.database-platform=org.hibernate.dialect.OracleDialect
+spring.jpa.hibernate.ddl-auto=update
+spring.jpa.show-sql=false
+
+spring.h2.console.enabled=false
diff --git a/src/main/resources/application-docker-local.properties.example b/src/main/resources/application-docker-local.properties.example
new file mode 100644
index 0000000..5815327
--- /dev/null
+++ b/src/main/resources/application-docker-local.properties.example
@@ -0,0 +1,8 @@
+# cp application-docker-local.properties.example application-docker-local.properties
+
+ORACLE_JDBC_URL=jdbc:oracle:thin:@//localhost:1521/XEPDB1
+ORACLE_USER=pkc_user
+ORACLE_PASSWORD=PkcPassword1
+
+# Docker SYSTEM password (only for manual SQL setup script)
+# ORACLE_SYSTEM_PASSWORD=29999login
diff --git a/src/main/resources/application-docker.properties b/src/main/resources/application-docker.properties
new file mode 100644
index 0000000..3372ab6
--- /dev/null
+++ b/src/main/resources/application-docker.properties
@@ -0,0 +1,24 @@
+# ==========================================
+# DOCKER profile — Oracle XE in Docker Desktop + DbVisualizer on Mac
+# Start Docker first, then Spring Boot with profile: docker
+# ==========================================
+
+# Use 127.0.0.1 (faster/more reliable than localhost on Mac)
+spring.datasource.url=${ORACLE_JDBC_URL:jdbc:oracle:thin:@//127.0.0.1:1521/XEPDB1}
+spring.datasource.username=${ORACLE_USER:pkc_user}
+spring.datasource.password=${ORACLE_PASSWORD:PkcPassword1}
+spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
+
+# Fail fast if Docker Oracle is not ready (avoids hanging at HikariPool)
+spring.datasource.hikari.connection-timeout=20000
+spring.datasource.hikari.maximum-pool-size=5
+spring.datasource.hikari.initialization-fail-timeout=1
+
+spring.jpa.database-platform=org.hibernate.dialect.OracleDialect
+spring.jpa.hibernate.ddl-auto=update
+spring.jpa.show-sql=false
+spring.jpa.properties.hibernate.jdbc.time_zone=UTC
+
+spring.h2.console.enabled=false
+
+spring.config.import=optional:classpath:application-docker-local.properties
diff --git a/src/main/resources/application-h2.properties b/src/main/resources/application-h2.properties
new file mode 100644
index 0000000..c9c077a
--- /dev/null
+++ b/src/main/resources/application-h2.properties
@@ -0,0 +1,9 @@
+# In-memory H2 — quick dev and unit tests (data lost on restart)
+spring.datasource.url=jdbc:h2:mem:pkcdb;DB_CLOSE_DELAY=-1
+spring.datasource.driver-class-name=org.h2.Driver
+spring.datasource.username=sa
+spring.datasource.password=
+spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
+spring.jpa.hibernate.ddl-auto=update
+spring.h2.console.enabled=true
+spring.h2.console.path=/h2-console
diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties
new file mode 100644
index 0000000..7c30a2b
--- /dev/null
+++ b/src/main/resources/application-local.properties
@@ -0,0 +1,16 @@
+# ==========================================
+# LOCAL profile — Mac: Spring Boot + DbVisualizer (persistent H2 file DB)
+# Run: -Dspring.profiles.active=local
+# Data file: ./data/pkcdb.mv.db
+# ==========================================
+
+spring.datasource.url=jdbc:h2:file:./data/pkcdb;AUTO_SERVER=TRUE;DB_CLOSE_DELAY=-1
+spring.datasource.driver-class-name=org.h2.Driver
+spring.datasource.username=sa
+spring.datasource.password=
+spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
+spring.jpa.hibernate.ddl-auto=update
+spring.jpa.show-sql=false
+
+spring.h2.console.enabled=true
+spring.h2.console.path=/h2-console
diff --git a/src/main/resources/application-oracle-local.properties.example b/src/main/resources/application-oracle-local.properties.example
new file mode 100644
index 0000000..c2393dc
--- /dev/null
+++ b/src/main/resources/application-oracle-local.properties.example
@@ -0,0 +1,15 @@
+# Copy to application-oracle-local.properties and set your real Oracle password.
+# cp application-oracle-local.properties.example application-oracle-local.properties
+
+# Mac Oracle XE 21c+ (pluggable DB — most common):
+# ORACLE_JDBC_URL=jdbc:oracle:thin:@//localhost:1521/XEPDB1
+
+# Older Oracle XE (SID XE) — uncomment if XEPDB1 does not work:
+# ORACLE_JDBC_URL=jdbc:oracle:thin:@localhost:1521:XE
+
+ORACLE_USER=pkc_user
+ORACLE_PASSWORD=PkcPassword1
+
+# Or use SYSTEM while testing (not recommended for production):
+# ORACLE_USER=system
+# ORACLE_PASSWORD=the_password_you_set_at_install
diff --git a/src/main/resources/application-oracle.properties b/src/main/resources/application-oracle.properties
new file mode 100644
index 0000000..a5ec84f
--- /dev/null
+++ b/src/main/resources/application-oracle.properties
@@ -0,0 +1,22 @@
+# ==========================================
+# Oracle XE profile (Mac or Oracle Linux VM)
+# Run: -Dspring.profiles.active=oracle
+# On Linux VM: ORACLE_JDBC_URL=jdbc:oracle:thin:@//localhost:1521/XEPDB1
+# DbVisualizer from Mac: use VM IP — see dbvis/README-DbVisualizer.md
+# ==========================================
+
+# XEPDB1 = Oracle 21c XE | FREEPDB1 = Oracle 23c Free on Mac
+spring.datasource.url=${ORACLE_JDBC_URL:jdbc:oracle:thin:@//localhost:1521/XEPDB1}
+spring.datasource.username=${ORACLE_USER:pkc_user}
+spring.datasource.password=${ORACLE_PASSWORD:PkcPassword1}
+spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
+
+spring.jpa.database-platform=org.hibernate.dialect.OracleDialect
+spring.jpa.hibernate.ddl-auto=update
+spring.jpa.show-sql=true
+spring.jpa.properties.hibernate.format_sql=true
+
+spring.h2.console.enabled=false
+
+# Optional: override credentials without editing this file (not committed)
+spring.config.import=optional:classpath:application-oracle-local.properties
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 99d0060..d1531ce 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1 +1,11 @@
-spring.application.name=oraclequantapi
+# ==========================================
+# Core — shared by all profiles
+# ==========================================
+spring.application.name=pkc-api
+server.port=${PORT:8081}
+logging.config=classpath:logback-spring.xml
+
+# Default: local = Spring Boot on Mac + H2 file (DbVisualizer H2)
+# Full Docker stack: run scripts/mac-start-docker.sh (Oracle + API in containers)
+# Profiles: docker-internal (API container), docker (legacy), h2 (tests), oracle (Linux VM)
+spring.profiles.default=local
diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml
new file mode 100644
index 0000000..5215df8
--- /dev/null
+++ b/src/main/resources/logback-spring.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+ ${LOG_DIR}/${LOG_FILE}.log
+
+ ${LOG_DIR}/${LOG_FILE}.%d{yyyy-MM-dd}.log.gz
+
+ 7
+ true
+
+
+ %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplicationTests.java b/src/test/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplicationTests.java
index 2de285b..ca03da1 100644
--- a/src/test/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplicationTests.java
+++ b/src/test/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplicationTests.java
@@ -2,8 +2,10 @@
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
@SpringBootTest
+@ActiveProfiles("h2")
class OraclequantapiApplicationTests {
@Test
diff --git a/src/test/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/service/ConversionEngineTest.java b/src/test/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/service/ConversionEngineTest.java
new file mode 100644
index 0000000..4b0ad08
--- /dev/null
+++ b/src/test/java/com/oraclequantapi/oraclequantapi/alharith_alsubhi/packageconversion/service/ConversionEngineTest.java
@@ -0,0 +1,83 @@
+package com.oraclequantapi.oraclequantapi.alharith_alsubhi.packageconversion.service;
+
+import org.junit.jupiter.api.Test;
+import java.util.List;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Verifies ConversionEngine against all PDF example inputs.
+ * Hint: Run with mvn test — no Spring context needed (fast unit tests).
+ */
+public class ConversionEngineTest {
+
+ // --- Official evaluation examples (must match auto-grader expected output) ---
+
+ @Test
+ public void testExample1_aa() {
+ List result = ConversionEngine.convert("aa");
+ assertEquals(List.of(1), result);
+ }
+
+ @Test
+ public void testExample2_abbcc() {
+ List result = ConversionEngine.convert("abbcc");
+ assertEquals(List.of(2, 6), result);
+ }
+
+ @Test
+ public void testExample3_dz_a_aazzaaa() {
+ List result = ConversionEngine.convert("dz_a_aazzaaa");
+ assertEquals(List.of(28, 53, 1), result);
+ }
+
+ @Test
+ public void testExample4_a_() {
+ List result = ConversionEngine.convert("a_");
+ assertEquals(List.of(0), result);
+ }
+
+ @Test
+ public void testExample5_abcdabcdab() {
+ List result = ConversionEngine.convert("abcdabcdab");
+ assertEquals(List.of(2, 7, 7), result);
+ }
+
+ @Test
+ public void testExample6_abcdabcdab_() {
+ List result = ConversionEngine.convert("abcdabcdab_");
+ assertEquals(List.of(2, 7, 7, 0), result);
+ }
+
+ @Test
+ public void testExample7_zdaaaaaaaabaaaaaaaabaaaaaaaabbaa() {
+ List result = ConversionEngine.convert("zdaaaaaaaabaaaaaaaabaaaaaaaabbaa");
+ assertEquals(List.of(34), result);
+ }
+
+ @Test
+ public void testExample8_za_a_a_a_a_a_a_a_a_a_a_a_a_azaaa() {
+ List result = ConversionEngine.convert("za_a_a_a_a_a_a_a_a_a_a_a_a_azaaa");
+ assertEquals(List.of(40, 1), result);
+ }
+
+ // --- Extra edge cases (robustness, not all in PDF) ---
+
+ @Test
+ public void testEdgeCases() {
+ // Null and empty strings
+ assertTrue(ConversionEngine.convert(null).isEmpty());
+ assertTrue(ConversionEngine.convert("").isEmpty());
+ assertTrue(ConversionEngine.convert(" ").isEmpty());
+
+ // Case insensitivity
+ assertEquals(List.of(1), ConversionEngine.convert("AA"));
+ assertEquals(List.of(2, 6), ConversionEngine.convert("aBBcc"));
+
+ // Truncated package counts/values gracefully handled
+ // Count is 2, but no values follow (sums available = 0)
+ assertEquals(List.of(0), ConversionEngine.convert("b"));
+
+ // Count is 2, but only 1 value follows (sums available = 1)
+ assertEquals(List.of(1), ConversionEngine.convert("ba"));
+ }
+}