You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by di...@apache.org on 2026/05/18 00:31:46 UTC

(airavata-custos) 06/09: Implemented the service layer for Allocation Change Requests and related events

This is an automated email from the ASF dual-hosted git repository.

DImuthuUpe pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git

commit f392978aeb75f4ffcf328f23f003b89c710194ad
Author: DImuthuUpe <di...@gmail.com>
AuthorDate: Sat May 16 21:21:59 2026 -0400

    Implemented the service layer for Allocation Change Requests and related events
---
 docs/API-Docs.md                                   | 186 +++++++++++++++++
 ...007_compute_allocation_change_requests.down.sql |  19 ++
 ...00007_compute_allocation_change_requests.up.sql |  49 +++++
 internal/server/server.go                          | 126 ++++++++++++
 ...ompute_allocation_change_request_event_store.go |  97 +++++++++
 .../compute_allocation_change_request_store.go     | 109 ++++++++++
 internal/store/store.go                            |  38 ++++
 pkg/models/allocation.go                           |  28 +--
 pkg/service/compute_allocation_change_request.go   | 219 +++++++++++++++++++++
 .../compute_allocation_change_request_event.go     | 119 +++++++++++
 pkg/service/service.go                             |   8 +
 11 files changed, 984 insertions(+), 14 deletions(-)

diff --git a/docs/API-Docs.md b/docs/API-Docs.md
index 69674e582..7854758d0 100644
--- a/docs/API-Docs.md
+++ b/docs/API-Docs.md
@@ -767,6 +767,192 @@ Return the most recent diff (highest `timestamp`) for the given allocation.
 
 ---
 
+## Compute Allocation Change Requests
+
+A change request represents a user- or admin-initiated proposal to mutate a
+compute allocation — e.g. asking for additional Service Units or to change
+its status. Each request carries a lifecycle (`change_status`: `PENDING`,
+`APPROVED`, `REJECTED`, etc.). Change requests are cascade-deleted when their
+parent allocation is deleted. Every create, update, and delete of a change
+request transactionally appends an entry to its event log (see below); the
+event log is intentionally **not** cascade-deleted so the audit trail
+survives the deletion of the parent change request.
+
+### `POST /compute-allocation-change-requests`
+
+Submit a new change request.
+
+**Required fields:** `compute_allocation_id`, `requester_id`
+**Optional fields:** `id`, `requested_su_amount`, `requested_status`, `reason`, `change_status` (defaults to `PENDING`), `approver_id`, `timestamp` (defaults to the server's current UTC time)
+
+**Request**
+
+```json
+{
+  "compute_allocation_id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56",
+  "requested_su_amount": 120000,
+  "requested_status": "ACTIVE",
+  "reason": "Need more SUs for upcoming HPC runs",
+  "requester_id": "11112222-3333-4444-5555-666677778888"
+}
+```
+
+**Response 201**
+
+```json
+{
+  "id": "9988aabb-ccdd-eeff-0011-223344556677",
+  "compute_allocation_id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56",
+  "requested_su_amount": 120000,
+  "requested_status": "ACTIVE",
+  "reason": "Need more SUs for upcoming HPC runs",
+  "change_status": "PENDING",
+  "requester_id": "11112222-3333-4444-5555-666677778888",
+  "timestamp": "2026-05-16T17:42:11.918Z"
+}
+```
+
+**Errors**
+
+- `400` — required field missing, or `compute_allocation_id` does not exist.
+
+---
+
+### `GET /compute-allocation-change-requests/{id}`
+
+Retrieve a single change request by its ID.
+
+**Errors**
+
+- `404` — no change request matches the supplied ID.
+
+---
+
+### `PUT /compute-allocation-change-requests/{id}`
+
+Replace mutable fields of a change request. Typically used by an approver to
+transition `change_status` (e.g. to `APPROVED` or `REJECTED`) and stamp
+`approver_id`. Omitted fields are preserved from the existing record.
+
+**Request**
+
+```json
+{
+  "change_status": "APPROVED",
+  "approver_id": "aaaa-bbbb-cccc-dddd-eeee"
+}
+```
+
+**Errors**
+
+- `400` — request id missing.
+- `404` — no change request matches the supplied ID.
+
+---
+
+### `DELETE /compute-allocation-change-requests/{id}`
+
+Remove a change request and (cascading) its event log.
+
+**Response 204** — empty body on success.
+
+---
+
+### `GET /compute-allocations/{id}/change-requests`
+
+List every change request ever submitted against the given allocation,
+ordered by `timestamp` ascending.
+
+---
+
+### `GET /users/{id}/change-requests`
+
+List every change request submitted by the given user, ordered by
+`timestamp` ascending.
+
+---
+
+## Compute Allocation Change Request Events
+
+Events are an append-only audit trail of state transitions applied to a
+change request — typically `CREATED`, `APPROVED`, `REJECTED`, `UPDATED`,
+`DELETED`, or arbitrary workflow markers. Create / update / delete of a
+change request each emit an event automatically; clients may also append
+custom events via the endpoint below. Events are **not** cascade-deleted
+when their parent change request is removed, so the audit trail is
+preserved indefinitely.
+
+### `POST /compute-allocation-change-request-events`
+
+Append a new event to a change request.
+
+**Required fields:** `compute_allocation_change_request_id`, `event_type`
+**Optional fields:** `id`, `description`, `timestamp` (defaults to the server's current UTC time)
+
+**Request**
+
+```json
+{
+  "compute_allocation_change_request_id": "9988aabb-ccdd-eeff-0011-223344556677",
+  "event_type": "APPROVED",
+  "description": "Change request approved by admin"
+}
+```
+
+**Response 201**
+
+```json
+{
+  "id": "ee11ff22-3344-5566-7788-99aabbccddee",
+  "compute_allocation_change_request_id": "9988aabb-ccdd-eeff-0011-223344556677",
+  "event_type": "APPROVED",
+  "description": "Change request approved by admin",
+  "timestamp": "2026-05-16T18:00:00.000Z"
+}
+```
+
+**Errors**
+
+- `400` — required field missing, or `compute_allocation_change_request_id` does not exist.
+
+---
+
+### `GET /compute-allocation-change-request-events/{id}`
+
+Retrieve a single event by its ID.
+
+**Errors**
+
+- `404` — no event matches the supplied ID.
+
+---
+
+### `DELETE /compute-allocation-change-request-events/{id}`
+
+Remove an event record. Intended for administrative cleanup; events are
+otherwise append-only.
+
+**Response 204** — empty body on success.
+
+---
+
+### `GET /compute-allocation-change-requests/{id}/events`
+
+List every event recorded against the given change request, ordered by
+`timestamp` ascending.
+
+---
+
+### `GET /compute-allocation-change-requests/{id}/events/latest`
+
+Return the most recent event for the given change request.
+
+**Errors**
+
+- `404` — the change request has no events recorded.
+
+---
+
 ## End-to-end example
 
 ```bash
diff --git a/internal/db/migrations/000007_compute_allocation_change_requests.down.sql b/internal/db/migrations/000007_compute_allocation_change_requests.down.sql
new file mode 100644
index 000000000..8140a01cb
--- /dev/null
+++ b/internal/db/migrations/000007_compute_allocation_change_requests.down.sql
@@ -0,0 +1,19 @@
+-- Licensed to the Apache Software Foundation (ASF) under one
+-- or more contributor license agreements.  See the NOTICE file
+-- distributed with this work for additional information
+-- regarding copyright ownership.  The ASF licenses this file
+-- to you under the Apache License, Version 2.0 (the
+-- "License"); you may not use this file except in compliance
+-- with the License.  You may obtain a copy of the License at
+--
+--   http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing,
+-- software distributed under the License is distributed on an
+-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+-- KIND, either express or implied.  See the License for the
+-- specific language governing permissions and limitations
+-- under the License.
+
+DROP TABLE IF EXISTS compute_allocation_change_request_events;
+DROP TABLE IF EXISTS compute_allocation_change_requests;
diff --git a/internal/db/migrations/000007_compute_allocation_change_requests.up.sql b/internal/db/migrations/000007_compute_allocation_change_requests.up.sql
new file mode 100644
index 000000000..aa04a8571
--- /dev/null
+++ b/internal/db/migrations/000007_compute_allocation_change_requests.up.sql
@@ -0,0 +1,49 @@
+-- Licensed to the Apache Software Foundation (ASF) under one
+-- or more contributor license agreements.  See the NOTICE file
+-- distributed with this work for additional information
+-- regarding copyright ownership.  The ASF licenses this file
+-- to you under the Apache License, Version 2.0 (the
+-- "License"); you may not use this file except in compliance
+-- with the License.  You may obtain a copy of the License at
+--
+--   http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing,
+-- software distributed under the License is distributed on an
+-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+-- KIND, either express or implied.  See the License for the
+-- specific language governing permissions and limitations
+-- under the License.
+
+CREATE TABLE IF NOT EXISTS compute_allocation_change_requests
+(
+    id                    VARCHAR(255) NOT NULL,
+    compute_allocation_id VARCHAR(255) NOT NULL,
+    requested_su_amount   BIGINT       NOT NULL DEFAULT 0,
+    requested_status      VARCHAR(64)  NOT NULL,
+    reason                TEXT         NOT NULL,
+    change_status         VARCHAR(64)  NOT NULL,
+    requester_id          VARCHAR(255) NOT NULL,
+    approver_id           VARCHAR(255) NOT NULL DEFAULT '',
+    timestamp             TIMESTAMP(6) NOT NULL,
+    created_at            TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
+    PRIMARY KEY (id),
+    KEY idx_compute_allocation_change_requests_allocation (compute_allocation_id, timestamp),
+    KEY idx_compute_allocation_change_requests_status (change_status),
+    KEY idx_compute_allocation_change_requests_requester (requester_id),
+    CONSTRAINT fk_compute_allocation_change_requests_allocation FOREIGN KEY (compute_allocation_id)
+        REFERENCES compute_allocations (id) ON DELETE CASCADE
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
+
+CREATE TABLE IF NOT EXISTS compute_allocation_change_request_events
+(
+    id                                  VARCHAR(255) NOT NULL,
+    compute_allocation_change_request_id VARCHAR(255) NOT NULL,
+    event_type                          VARCHAR(64)  NOT NULL,
+    description                         TEXT         NOT NULL,
+    timestamp                           TIMESTAMP(6) NOT NULL,
+    created_at                          TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
+    PRIMARY KEY (id),
+    KEY idx_compute_allocation_change_request_events_request (compute_allocation_change_request_id, timestamp),
+    KEY idx_compute_allocation_change_request_events_type (event_type)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
diff --git a/internal/server/server.go b/internal/server/server.go
index 3638c0751..70f1e5948 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -86,6 +86,19 @@ func (s *Server) routes() {
 	s.mux.HandleFunc("DELETE /compute-allocation-diffs/{id}", s.deleteComputeAllocationDiff)
 	s.mux.HandleFunc("GET /compute-allocations/{id}/diffs", s.listDiffsForAllocation)
 	s.mux.HandleFunc("GET /compute-allocations/{id}/diffs/latest", s.getLatestDiffForAllocation)
+
+	s.mux.HandleFunc("POST /compute-allocation-change-requests", s.createComputeAllocationChangeRequest)
+	s.mux.HandleFunc("GET /compute-allocation-change-requests/{id}", s.getComputeAllocationChangeRequest)
+	s.mux.HandleFunc("PUT /compute-allocation-change-requests/{id}", s.updateComputeAllocationChangeRequest)
+	s.mux.HandleFunc("DELETE /compute-allocation-change-requests/{id}", s.deleteComputeAllocationChangeRequest)
+	s.mux.HandleFunc("GET /compute-allocations/{id}/change-requests", s.listChangeRequestsForAllocation)
+	s.mux.HandleFunc("GET /users/{id}/change-requests", s.listChangeRequestsByRequester)
+
+	s.mux.HandleFunc("POST /compute-allocation-change-request-events", s.createComputeAllocationChangeRequestEvent)
+	s.mux.HandleFunc("GET /compute-allocation-change-request-events/{id}", s.getComputeAllocationChangeRequestEvent)
+	s.mux.HandleFunc("DELETE /compute-allocation-change-request-events/{id}", s.deleteComputeAllocationChangeRequestEvent)
+	s.mux.HandleFunc("GET /compute-allocation-change-requests/{id}/events", s.listEventsForChangeRequest)
+	s.mux.HandleFunc("GET /compute-allocation-change-requests/{id}/events/latest", s.getLatestEventForChangeRequest)
 }
 
 func (s *Server) healthz(w http.ResponseWriter, _ *http.Request) {
@@ -391,6 +404,119 @@ func (s *Server) getLatestDiffForAllocation(w http.ResponseWriter, r *http.Reque
 	writeJSON(w, http.StatusOK, diff)
 }
 
+func (s *Server) createComputeAllocationChangeRequest(w http.ResponseWriter, r *http.Request) {
+	var req models.ComputeAllocationChangeRequest
+	if err := decodeJSON(r, &req); err != nil {
+		writeError(w, http.StatusBadRequest, err)
+		return
+	}
+	created, err := s.svc.CreateComputeAllocationChangeRequest(r.Context(), &req)
+	if err != nil {
+		writeServiceError(w, err)
+		return
+	}
+	writeJSON(w, http.StatusCreated, created)
+}
+
+func (s *Server) getComputeAllocationChangeRequest(w http.ResponseWriter, r *http.Request) {
+	req, err := s.svc.GetComputeAllocationChangeRequest(r.Context(), r.PathValue("id"))
+	if err != nil {
+		writeServiceError(w, err)
+		return
+	}
+	writeJSON(w, http.StatusOK, req)
+}
+
+func (s *Server) updateComputeAllocationChangeRequest(w http.ResponseWriter, r *http.Request) {
+	var req models.ComputeAllocationChangeRequest
+	if err := decodeJSON(r, &req); err != nil {
+		writeError(w, http.StatusBadRequest, err)
+		return
+	}
+	req.ID = r.PathValue("id")
+	updated, err := s.svc.UpdateComputeAllocationChangeRequest(r.Context(), &req)
+	if err != nil {
+		writeServiceError(w, err)
+		return
+	}
+	writeJSON(w, http.StatusOK, updated)
+}
+
+func (s *Server) deleteComputeAllocationChangeRequest(w http.ResponseWriter, r *http.Request) {
+	if err := s.svc.DeleteComputeAllocationChangeRequest(r.Context(), r.PathValue("id")); err != nil {
+		writeServiceError(w, err)
+		return
+	}
+	w.WriteHeader(http.StatusNoContent)
+}
+
+func (s *Server) listChangeRequestsForAllocation(w http.ResponseWriter, r *http.Request) {
+	rows, err := s.svc.ListChangeRequestsForAllocation(r.Context(), r.PathValue("id"))
+	if err != nil {
+		writeServiceError(w, err)
+		return
+	}
+	writeJSON(w, http.StatusOK, rows)
+}
+
+func (s *Server) listChangeRequestsByRequester(w http.ResponseWriter, r *http.Request) {
+	rows, err := s.svc.ListChangeRequestsByRequester(r.Context(), r.PathValue("id"))
+	if err != nil {
+		writeServiceError(w, err)
+		return
+	}
+	writeJSON(w, http.StatusOK, rows)
+}
+
+func (s *Server) createComputeAllocationChangeRequestEvent(w http.ResponseWriter, r *http.Request) {
+	var evt models.ComputeAllocationChangeRequestEvent
+	if err := decodeJSON(r, &evt); err != nil {
+		writeError(w, http.StatusBadRequest, err)
+		return
+	}
+	created, err := s.svc.CreateComputeAllocationChangeRequestEvent(r.Context(), &evt)
+	if err != nil {
+		writeServiceError(w, err)
+		return
+	}
+	writeJSON(w, http.StatusCreated, created)
+}
+
+func (s *Server) getComputeAllocationChangeRequestEvent(w http.ResponseWriter, r *http.Request) {
+	evt, err := s.svc.GetComputeAllocationChangeRequestEvent(r.Context(), r.PathValue("id"))
+	if err != nil {
+		writeServiceError(w, err)
+		return
+	}
+	writeJSON(w, http.StatusOK, evt)
+}
+
+func (s *Server) deleteComputeAllocationChangeRequestEvent(w http.ResponseWriter, r *http.Request) {
+	if err := s.svc.DeleteComputeAllocationChangeRequestEvent(r.Context(), r.PathValue("id")); err != nil {
+		writeServiceError(w, err)
+		return
+	}
+	w.WriteHeader(http.StatusNoContent)
+}
+
+func (s *Server) listEventsForChangeRequest(w http.ResponseWriter, r *http.Request) {
+	rows, err := s.svc.ListEventsForChangeRequest(r.Context(), r.PathValue("id"))
+	if err != nil {
+		writeServiceError(w, err)
+		return
+	}
+	writeJSON(w, http.StatusOK, rows)
+}
+
+func (s *Server) getLatestEventForChangeRequest(w http.ResponseWriter, r *http.Request) {
+	evt, err := s.svc.GetLatestEventForChangeRequest(r.Context(), r.PathValue("id"))
+	if err != nil {
+		writeServiceError(w, err)
+		return
+	}
+	writeJSON(w, http.StatusOK, evt)
+}
+
 // LoggingMiddleware logs every request once it completes.
 func LoggingMiddleware(next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
diff --git a/internal/store/compute_allocation_change_request_event_store.go b/internal/store/compute_allocation_change_request_event_store.go
new file mode 100644
index 000000000..087a696d5
--- /dev/null
+++ b/internal/store/compute_allocation_change_request_event_store.go
@@ -0,0 +1,97 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package store
+
+import (
+	"context"
+	"database/sql"
+	"errors"
+
+	"github.com/jmoiron/sqlx"
+
+	"github.com/apache/airavata-custos/pkg/models"
+)
+
+const computeAllocationChangeRequestEventColumns = "id, compute_allocation_change_request_id, event_type, description, timestamp"
+
+type mysqlComputeAllocationChangeRequestEventStore struct {
+	db *sqlx.DB
+}
+
+// NewComputeAllocationChangeRequestEventStore returns a MySQL-backed
+// ComputeAllocationChangeRequestEventStore.
+func NewComputeAllocationChangeRequestEventStore(db *sqlx.DB) ComputeAllocationChangeRequestEventStore {
+	return &mysqlComputeAllocationChangeRequestEventStore{db: db}
+}
+
+func (s *mysqlComputeAllocationChangeRequestEventStore) FindByID(ctx context.Context, id string) (*models.ComputeAllocationChangeRequestEvent, error) {
+	var e models.ComputeAllocationChangeRequestEvent
+	err := s.db.GetContext(ctx, &e,
+		`SELECT `+computeAllocationChangeRequestEventColumns+` FROM compute_allocation_change_request_events WHERE id = ?`, id)
+	if err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			return nil, nil
+		}
+		return nil, err
+	}
+	return &e, nil
+}
+
+func (s *mysqlComputeAllocationChangeRequestEventStore) FindByChangeRequest(ctx context.Context, changeRequestID string) ([]models.ComputeAllocationChangeRequestEvent, error) {
+	var rows []models.ComputeAllocationChangeRequestEvent
+	err := s.db.SelectContext(ctx, &rows,
+		`SELECT `+computeAllocationChangeRequestEventColumns+`
+		 FROM compute_allocation_change_request_events
+		 WHERE compute_allocation_change_request_id = ?
+		 ORDER BY timestamp`, changeRequestID)
+	if err != nil {
+		return nil, err
+	}
+	return rows, nil
+}
+
+func (s *mysqlComputeAllocationChangeRequestEventStore) FindLatestByChangeRequest(ctx context.Context, changeRequestID string) (*models.ComputeAllocationChangeRequestEvent, error) {
+	var e models.ComputeAllocationChangeRequestEvent
+	err := s.db.GetContext(ctx, &e,
+		`SELECT `+computeAllocationChangeRequestEventColumns+`
+		 FROM compute_allocation_change_request_events
+		 WHERE compute_allocation_change_request_id = ?
+		 ORDER BY timestamp DESC
+		 LIMIT 1`, changeRequestID)
+	if err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			return nil, nil
+		}
+		return nil, err
+	}
+	return &e, nil
+}
+
+func (s *mysqlComputeAllocationChangeRequestEventStore) Create(ctx context.Context, tx *sql.Tx, e *models.ComputeAllocationChangeRequestEvent) error {
+	_, err := tx.ExecContext(ctx,
+		`INSERT INTO compute_allocation_change_request_events
+		     (id, compute_allocation_change_request_id, event_type, description, timestamp)
+		 VALUES (?, ?, ?, ?, ?)`,
+		e.ID, e.ComputeAllocationChangeRequestID, e.EventType, e.Description, e.Timestamp)
+	return err
+}
+
+func (s *mysqlComputeAllocationChangeRequestEventStore) Delete(ctx context.Context, tx *sql.Tx, id string) error {
+	_, err := tx.ExecContext(ctx, `DELETE FROM compute_allocation_change_request_events WHERE id = ?`, id)
+	return err
+}
diff --git a/internal/store/compute_allocation_change_request_store.go b/internal/store/compute_allocation_change_request_store.go
new file mode 100644
index 000000000..b0b87fe8f
--- /dev/null
+++ b/internal/store/compute_allocation_change_request_store.go
@@ -0,0 +1,109 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package store
+
+import (
+	"context"
+	"database/sql"
+	"errors"
+
+	"github.com/jmoiron/sqlx"
+
+	"github.com/apache/airavata-custos/pkg/models"
+)
+
+const computeAllocationChangeRequestColumns = "id, compute_allocation_id, requested_su_amount, requested_status, reason, change_status, requester_id, approver_id, timestamp"
+
+type mysqlComputeAllocationChangeRequestStore struct {
+	db *sqlx.DB
+}
+
+// NewComputeAllocationChangeRequestStore returns a MySQL-backed
+// ComputeAllocationChangeRequestStore.
+func NewComputeAllocationChangeRequestStore(db *sqlx.DB) ComputeAllocationChangeRequestStore {
+	return &mysqlComputeAllocationChangeRequestStore{db: db}
+}
+
+func (s *mysqlComputeAllocationChangeRequestStore) FindByID(ctx context.Context, id string) (*models.ComputeAllocationChangeRequest, error) {
+	var c models.ComputeAllocationChangeRequest
+	err := s.db.GetContext(ctx, &c,
+		`SELECT `+computeAllocationChangeRequestColumns+` FROM compute_allocation_change_requests WHERE id = ?`, id)
+	if err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			return nil, nil
+		}
+		return nil, err
+	}
+	return &c, nil
+}
+
+func (s *mysqlComputeAllocationChangeRequestStore) FindByAllocation(ctx context.Context, allocationID string) ([]models.ComputeAllocationChangeRequest, error) {
+	var rows []models.ComputeAllocationChangeRequest
+	err := s.db.SelectContext(ctx, &rows,
+		`SELECT `+computeAllocationChangeRequestColumns+`
+		 FROM compute_allocation_change_requests
+		 WHERE compute_allocation_id = ?
+		 ORDER BY timestamp`, allocationID)
+	if err != nil {
+		return nil, err
+	}
+	return rows, nil
+}
+
+func (s *mysqlComputeAllocationChangeRequestStore) FindByRequester(ctx context.Context, requesterID string) ([]models.ComputeAllocationChangeRequest, error) {
+	var rows []models.ComputeAllocationChangeRequest
+	err := s.db.SelectContext(ctx, &rows,
+		`SELECT `+computeAllocationChangeRequestColumns+`
+		 FROM compute_allocation_change_requests
+		 WHERE requester_id = ?
+		 ORDER BY timestamp`, requesterID)
+	if err != nil {
+		return nil, err
+	}
+	return rows, nil
+}
+
+func (s *mysqlComputeAllocationChangeRequestStore) Create(ctx context.Context, tx *sql.Tx, c *models.ComputeAllocationChangeRequest) error {
+	_, err := tx.ExecContext(ctx,
+		`INSERT INTO compute_allocation_change_requests
+		     (id, compute_allocation_id, requested_su_amount, requested_status, reason, change_status, requester_id, approver_id, timestamp)
+		 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+		c.ID, c.ComputeAllocationID, c.RequestedSUAmount, string(c.RequestedStatus), c.Reason, c.ChangeStatus, c.RequesterID, c.ApproverID, c.Timestamp)
+	return err
+}
+
+func (s *mysqlComputeAllocationChangeRequestStore) Update(ctx context.Context, tx *sql.Tx, c *models.ComputeAllocationChangeRequest) error {
+	_, err := tx.ExecContext(ctx,
+		`UPDATE compute_allocation_change_requests
+		    SET compute_allocation_id = ?,
+		        requested_su_amount   = ?,
+		        requested_status      = ?,
+		        reason                = ?,
+		        change_status         = ?,
+		        requester_id          = ?,
+		        approver_id           = ?,
+		        timestamp             = ?
+		  WHERE id = ?`,
+		c.ComputeAllocationID, c.RequestedSUAmount, string(c.RequestedStatus), c.Reason, c.ChangeStatus, c.RequesterID, c.ApproverID, c.Timestamp, c.ID)
+	return err
+}
+
+func (s *mysqlComputeAllocationChangeRequestStore) Delete(ctx context.Context, tx *sql.Tx, id string) error {
+	_, err := tx.ExecContext(ctx, `DELETE FROM compute_allocation_change_requests WHERE id = ?`, id)
+	return err
+}
diff --git a/internal/store/store.go b/internal/store/store.go
index 6624d8f5a..54a367c49 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -170,3 +170,41 @@ type ComputeAllocationDiffStore interface {
 	// Delete removes a diff by ID within the provided transaction.
 	Delete(ctx context.Context, tx *sql.Tx, id string) error
 }
+
+// ComputeAllocationChangeRequestStore defines persistence operations for
+// user/admin requests to change a compute allocation (e.g. asking for more
+// SUs or to change its status).
+type ComputeAllocationChangeRequestStore interface {
+	// FindByID returns the change request with the given ID, or nil if it does not exist.
+	FindByID(ctx context.Context, id string) (*models.ComputeAllocationChangeRequest, error)
+	// FindByAllocation returns every change request ever recorded against the
+	// given allocation, ordered by timestamp ascending.
+	FindByAllocation(ctx context.Context, allocationID string) ([]models.ComputeAllocationChangeRequest, error)
+	// FindByRequester returns every change request made by the given user,
+	// ordered by timestamp ascending.
+	FindByRequester(ctx context.Context, requesterID string) ([]models.ComputeAllocationChangeRequest, error)
+	// Create inserts a new change request within the provided transaction.
+	Create(ctx context.Context, tx *sql.Tx, c *models.ComputeAllocationChangeRequest) error
+	// Update replaces mutable fields of an existing change request within the provided transaction.
+	Update(ctx context.Context, tx *sql.Tx, c *models.ComputeAllocationChangeRequest) error
+	// Delete removes a change request by ID within the provided transaction.
+	Delete(ctx context.Context, tx *sql.Tx, id string) error
+}
+
+// ComputeAllocationChangeRequestEventStore defines persistence operations
+// for the append-only audit trail of state transitions applied to a
+// ComputeAllocationChangeRequest.
+type ComputeAllocationChangeRequestEventStore interface {
+	// FindByID returns the event with the given ID, or nil if it does not exist.
+	FindByID(ctx context.Context, id string) (*models.ComputeAllocationChangeRequestEvent, error)
+	// FindByChangeRequest returns every event recorded against the given
+	// change request, ordered by timestamp ascending.
+	FindByChangeRequest(ctx context.Context, changeRequestID string) ([]models.ComputeAllocationChangeRequestEvent, error)
+	// FindLatestByChangeRequest returns the most recent event for the given
+	// change request, or nil if none exist.
+	FindLatestByChangeRequest(ctx context.Context, changeRequestID string) (*models.ComputeAllocationChangeRequestEvent, error)
+	// Create inserts a new event within the provided transaction.
+	Create(ctx context.Context, tx *sql.Tx, e *models.ComputeAllocationChangeRequestEvent) error
+	// Delete removes an event by ID within the provided transaction.
+	Delete(ctx context.Context, tx *sql.Tx, id string) error
+}
diff --git a/pkg/models/allocation.go b/pkg/models/allocation.go
index b3e284927..a6003f987 100644
--- a/pkg/models/allocation.go
+++ b/pkg/models/allocation.go
@@ -58,23 +58,23 @@ type ComputeAllocationDiff struct { // Diff will occur either through a change r
 }
 
 type ComputeAllocationChangeRequest struct { // Represents a request to change the allocation, e.g., requesting more SUs, requesting a reduction in SUs, etc from users or admins.
-	ID                  string           `json:"id"`
-	ComputeAllocationID string           `json:"compute_allocation_id"`
-	RequestedSUAmount   int64            `json:"requested_su_amount"`   // The requested allocation amount in SUs, e.g., 1200 SUs, etc.
-	RequestedStatus     AllocationStatus `json:"requested_status"`      // ACTIVE, INACTIVE, DELETED, etc.
-	Reason              string           `json:"reason"`                // The reason for the change request, e.g., "Need more SUs for upcoming jobs", "Requesting reduction in SUs due to project completion", etc.
-	ChangeStatus        string           `json:"change_status"`         // "PENDING", "APPROVED", "REJECTED", etc.
-	RequesterID         string           `json:"requester_id"`          // The ID of the user who made the change request.
-	ApproverID          string           `json:"approver_id,omitempty"` // The ID of the user who approved/rejected the change request, if applicable.
-	Timestamp           time.Time        `json:"timestamp"`             // The time when the change request was made.
+	ID                  string           `json:"id"                    db:"id"`
+	ComputeAllocationID string           `json:"compute_allocation_id" db:"compute_allocation_id"`
+	RequestedSUAmount   int64            `json:"requested_su_amount"   db:"requested_su_amount"` // The requested allocation amount in SUs, e.g., 1200 SUs, etc.
+	RequestedStatus     AllocationStatus `json:"requested_status"      db:"requested_status"`    // ACTIVE, INACTIVE, DELETED, etc.
+	Reason              string           `json:"reason"                db:"reason"`              // The reason for the change request, e.g., "Need more SUs for upcoming jobs", "Requesting reduction in SUs due to project completion", etc.
+	ChangeStatus        string           `json:"change_status"         db:"change_status"`       // "PENDING", "APPROVED", "REJECTED", etc.
+	RequesterID         string           `json:"requester_id"          db:"requester_id"`        // The ID of the user who made the change request.
+	ApproverID          string           `json:"approver_id,omitempty" db:"approver_id"`         // The ID of the user who approved/rejected the change request, if applicable.
+	Timestamp           time.Time        `json:"timestamp"             db:"timestamp"`           // The time when the change request was made.
 }
 
 type ComputeAllocationChangeRequestEvent struct {
-	ID                               string    `json:"id"`
-	ComputeAllocationChangeRequestID string    `json:"compute_allocation_change_request_id"`
-	EventType                        string    `json:"event_type"`            // "CREATED", "APPROVED", "REJECTED", etc.
-	Description                      string    `json:"description,omitempty"` // Optional description of the event, e.g., "Change request created by user", "Change request approved by admin", etc.
-	Timestamp                        time.Time `json:"timestamp"`             // The time when the event occurred.
+	ID                               string    `json:"id"                                   db:"id"`
+	ComputeAllocationChangeRequestID string    `json:"compute_allocation_change_request_id" db:"compute_allocation_change_request_id"`
+	EventType                        string    `json:"event_type"                           db:"event_type"`  // "CREATED", "APPROVED", "REJECTED", etc.
+	Description                      string    `json:"description,omitempty"                db:"description"` // Optional description of the event, e.g., "Change request created by user", "Change request approved by admin", etc.
+	Timestamp                        time.Time `json:"timestamp"                            db:"timestamp"`   // The time when the event occurred.
 }
 
 type ComputeAllocationUsage struct { // Represents the usage of a compute allocation, e.g., when a job consumes some of the allocated SUs, etc.
diff --git a/pkg/service/compute_allocation_change_request.go b/pkg/service/compute_allocation_change_request.go
new file mode 100644
index 000000000..06bda3024
--- /dev/null
+++ b/pkg/service/compute_allocation_change_request.go
@@ -0,0 +1,219 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package service
+
+import (
+	"context"
+	"database/sql"
+	"fmt"
+
+	"github.com/apache/airavata-custos/pkg/models"
+)
+
+// CreateComputeAllocationChangeRequest records a new change request against a
+// compute allocation. The referenced allocation must exist and a requester
+// must be supplied. ChangeStatus defaults to "PENDING" and Timestamp to the
+// server's current UTC time. A "CREATED" event is appended to the change
+// request's audit log in the same transaction.
+func (s *Service) CreateComputeAllocationChangeRequest(ctx context.Context, req *models.ComputeAllocationChangeRequest) (*models.ComputeAllocationChangeRequest, error) {
+	if req == nil {
+		return nil, fmt.Errorf("%w: compute allocation change request is nil", ErrInvalidInput)
+	}
+	if req.ComputeAllocationID == "" {
+		return nil, fmt.Errorf("%w: compute_allocation_id is required", ErrInvalidInput)
+	}
+	if req.RequesterID == "" {
+		return nil, fmt.Errorf("%w: requester_id is required", ErrInvalidInput)
+	}
+
+	if alloc, err := s.allocs.FindByID(ctx, req.ComputeAllocationID); err != nil {
+		return nil, fmt.Errorf("lookup compute allocation: %w", err)
+	} else if alloc == nil {
+		return nil, fmt.Errorf("%w: compute allocation %q not found", ErrInvalidInput, req.ComputeAllocationID)
+	}
+
+	if req.ID == "" {
+		req.ID = newID()
+	}
+	if req.ChangeStatus == "" {
+		req.ChangeStatus = "PENDING"
+	}
+	if req.Timestamp.IsZero() {
+		req.Timestamp = nowUTC()
+	}
+
+	if err := s.inTx(ctx, func(tx *sql.Tx) error {
+		if err := s.changeRequests.Create(ctx, tx, req); err != nil {
+			return err
+		}
+		return s.changeEvents.Create(ctx, tx, &models.ComputeAllocationChangeRequestEvent{
+			ID:                               newID(),
+			ComputeAllocationChangeRequestID: req.ID,
+			EventType:                        "CREATED",
+			Description:                      "Change request created",
+			Timestamp:                        req.Timestamp,
+		})
+	}); err != nil {
+		return nil, fmt.Errorf("create compute allocation change request: %w", err)
+	}
+	return req, nil
+}
+
+// GetComputeAllocationChangeRequest retrieves a change request by its ID.
+// Returns ErrNotFound when no request matches.
+func (s *Service) GetComputeAllocationChangeRequest(ctx context.Context, id string) (*models.ComputeAllocationChangeRequest, error) {
+	c, err := s.changeRequests.FindByID(ctx, id)
+	if err != nil {
+		return nil, fmt.Errorf("get compute allocation change request: %w", err)
+	}
+	if c == nil {
+		return nil, ErrNotFound
+	}
+	return c, nil
+}
+
+// ListChangeRequestsForAllocation returns every change request ever recorded
+// against the given allocation, ordered chronologically by timestamp.
+func (s *Service) ListChangeRequestsForAllocation(ctx context.Context, allocationID string) ([]models.ComputeAllocationChangeRequest, error) {
+	if allocationID == "" {
+		return nil, fmt.Errorf("%w: compute_allocation_id is required", ErrInvalidInput)
+	}
+	rows, err := s.changeRequests.FindByAllocation(ctx, allocationID)
+	if err != nil {
+		return nil, fmt.Errorf("list change requests for allocation: %w", err)
+	}
+	return rows, nil
+}
+
+// ListChangeRequestsByRequester returns every change request submitted by the
+// given user, ordered chronologically by timestamp.
+func (s *Service) ListChangeRequestsByRequester(ctx context.Context, requesterID string) ([]models.ComputeAllocationChangeRequest, error) {
+	if requesterID == "" {
+		return nil, fmt.Errorf("%w: requester_id is required", ErrInvalidInput)
+	}
+	rows, err := s.changeRequests.FindByRequester(ctx, requesterID)
+	if err != nil {
+		return nil, fmt.Errorf("list change requests by requester: %w", err)
+	}
+	return rows, nil
+}
+
+// UpdateComputeAllocationChangeRequest replaces mutable fields of an existing
+// change request — typically used to transition change_status and record the
+// approver. An audit event is appended in the same transaction: when
+// change_status transitions to a new value the event_type is the new status
+// (e.g. "APPROVED"), otherwise "UPDATED".
+func (s *Service) UpdateComputeAllocationChangeRequest(ctx context.Context, req *models.ComputeAllocationChangeRequest) (*models.ComputeAllocationChangeRequest, error) {
+	if req == nil || req.ID == "" {
+		return nil, fmt.Errorf("%w: compute allocation change request id is required", ErrInvalidInput)
+	}
+	existing, err := s.changeRequests.FindByID(ctx, req.ID)
+	if err != nil {
+		return nil, fmt.Errorf("lookup compute allocation change request: %w", err)
+	}
+	if existing == nil {
+		return nil, ErrNotFound
+	}
+	if req.ComputeAllocationID == "" {
+		req.ComputeAllocationID = existing.ComputeAllocationID
+	}
+	if req.RequesterID == "" {
+		req.RequesterID = existing.RequesterID
+	}
+	if req.ChangeStatus == "" {
+		req.ChangeStatus = existing.ChangeStatus
+	}
+	if req.Timestamp.IsZero() {
+		req.Timestamp = existing.Timestamp
+	}
+
+	eventType := "UPDATED"
+	eventDescription := "Change request updated"
+	if req.ChangeStatus != existing.ChangeStatus {
+		eventType = req.ChangeStatus
+		eventDescription = fmt.Sprintf("Change request transitioned from %s to %s", existing.ChangeStatus, req.ChangeStatus)
+	}
+
+	// When a change request transitions into APPROVED, materialise the
+	// approved change as a ComputeAllocationDiff so the allocation's audit
+	// log reflects the agreed-upon new SU amount and status.
+	approved := req.ChangeStatus == "APPROVED" && existing.ChangeStatus != "APPROVED"
+
+	if err := s.inTx(ctx, func(tx *sql.Tx) error {
+		if err := s.changeRequests.Update(ctx, tx, req); err != nil {
+			return err
+		}
+		return s.changeEvents.Create(ctx, tx, &models.ComputeAllocationChangeRequestEvent{
+			ID:                               newID(),
+			ComputeAllocationChangeRequestID: req.ID,
+			EventType:                        eventType,
+			Description:                      eventDescription,
+			Timestamp:                        nowUTC(),
+		})
+	}); err != nil {
+		return nil, fmt.Errorf("update compute allocation change request: %w", err)
+	}
+
+	if approved {
+		diffStatus := req.RequestedStatus
+		if diffStatus == "" {
+			diffStatus = models.ACTIVE
+		}
+		if _, err := s.CreateComputeAllocationDiff(ctx, &models.ComputeAllocationDiff{
+			ComputeAllocationID: req.ComputeAllocationID,
+			DiffType:            "CHANGE_REQUEST_APPROVED",
+			NewSUAmount:         req.RequestedSUAmount,
+			Status:              diffStatus,
+			Description:         fmt.Sprintf("Applied approved change request %s", req.ID),
+		}); err != nil {
+			return nil, fmt.Errorf("record approved change request diff: %w", err)
+		}
+	}
+	return req, nil
+}
+
+// DeleteComputeAllocationChangeRequest removes a change request by ID. A
+// "DELETED" event is appended to the audit log first so the trail survives
+// the parent record's removal.
+func (s *Service) DeleteComputeAllocationChangeRequest(ctx context.Context, id string) error {
+	if id == "" {
+		return fmt.Errorf("%w: compute allocation change request id is required", ErrInvalidInput)
+	}
+	existing, err := s.changeRequests.FindByID(ctx, id)
+	if err != nil {
+		return fmt.Errorf("lookup compute allocation change request: %w", err)
+	}
+	if existing == nil {
+		return ErrNotFound
+	}
+	if err := s.inTx(ctx, func(tx *sql.Tx) error {
+		if err := s.changeEvents.Create(ctx, tx, &models.ComputeAllocationChangeRequestEvent{
+			ID:                               newID(),
+			ComputeAllocationChangeRequestID: id,
+			EventType:                        "DELETED",
+			Description:                      "Change request deleted",
+			Timestamp:                        nowUTC(),
+		}); err != nil {
+			return err
+		}
+		return s.changeRequests.Delete(ctx, tx, id)
+	}); err != nil {
+		return fmt.Errorf("delete compute allocation change request: %w", err)
+	}
+	return nil
+}
diff --git a/pkg/service/compute_allocation_change_request_event.go b/pkg/service/compute_allocation_change_request_event.go
new file mode 100644
index 000000000..7dd277796
--- /dev/null
+++ b/pkg/service/compute_allocation_change_request_event.go
@@ -0,0 +1,119 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package service
+
+import (
+	"context"
+	"database/sql"
+	"fmt"
+
+	"github.com/apache/airavata-custos/pkg/models"
+)
+
+// CreateComputeAllocationChangeRequestEvent records a new event against a
+// change request. The referenced change request must exist. If Timestamp is
+// zero the server's current UTC time is used. Event records are append-only
+// and have no update operation.
+func (s *Service) CreateComputeAllocationChangeRequestEvent(ctx context.Context, evt *models.ComputeAllocationChangeRequestEvent) (*models.ComputeAllocationChangeRequestEvent, error) {
+	if evt == nil {
+		return nil, fmt.Errorf("%w: compute allocation change request event is nil", ErrInvalidInput)
+	}
+	if evt.ComputeAllocationChangeRequestID == "" {
+		return nil, fmt.Errorf("%w: compute_allocation_change_request_id is required", ErrInvalidInput)
+	}
+	if evt.EventType == "" {
+		return nil, fmt.Errorf("%w: event_type is required", ErrInvalidInput)
+	}
+
+	if cr, err := s.changeRequests.FindByID(ctx, evt.ComputeAllocationChangeRequestID); err != nil {
+		return nil, fmt.Errorf("lookup compute allocation change request: %w", err)
+	} else if cr == nil {
+		return nil, fmt.Errorf("%w: compute allocation change request %q not found", ErrInvalidInput, evt.ComputeAllocationChangeRequestID)
+	}
+
+	if evt.ID == "" {
+		evt.ID = newID()
+	}
+	if evt.Timestamp.IsZero() {
+		evt.Timestamp = nowUTC()
+	}
+
+	if err := s.inTx(ctx, func(tx *sql.Tx) error {
+		return s.changeEvents.Create(ctx, tx, evt)
+	}); err != nil {
+		return nil, fmt.Errorf("create compute allocation change request event: %w", err)
+	}
+	return evt, nil
+}
+
+// GetComputeAllocationChangeRequestEvent retrieves an event by its ID.
+// Returns ErrNotFound when no event matches.
+func (s *Service) GetComputeAllocationChangeRequestEvent(ctx context.Context, id string) (*models.ComputeAllocationChangeRequestEvent, error) {
+	e, err := s.changeEvents.FindByID(ctx, id)
+	if err != nil {
+		return nil, fmt.Errorf("get compute allocation change request event: %w", err)
+	}
+	if e == nil {
+		return nil, ErrNotFound
+	}
+	return e, nil
+}
+
+// ListEventsForChangeRequest returns every event recorded against the given
+// change request, ordered chronologically by timestamp.
+func (s *Service) ListEventsForChangeRequest(ctx context.Context, changeRequestID string) ([]models.ComputeAllocationChangeRequestEvent, error) {
+	if changeRequestID == "" {
+		return nil, fmt.Errorf("%w: compute_allocation_change_request_id is required", ErrInvalidInput)
+	}
+	rows, err := s.changeEvents.FindByChangeRequest(ctx, changeRequestID)
+	if err != nil {
+		return nil, fmt.Errorf("list events for change request: %w", err)
+	}
+	return rows, nil
+}
+
+// GetLatestEventForChangeRequest returns the most recent event for the given
+// change request. Returns ErrNotFound when the change request has no events.
+func (s *Service) GetLatestEventForChangeRequest(ctx context.Context, changeRequestID string) (*models.ComputeAllocationChangeRequestEvent, error) {
+	if changeRequestID == "" {
+		return nil, fmt.Errorf("%w: compute_allocation_change_request_id is required", ErrInvalidInput)
+	}
+	e, err := s.changeEvents.FindLatestByChangeRequest(ctx, changeRequestID)
+	if err != nil {
+		return nil, fmt.Errorf("get latest event for change request: %w", err)
+	}
+	if e == nil {
+		return nil, ErrNotFound
+	}
+	return e, nil
+}
+
+// DeleteComputeAllocationChangeRequestEvent removes an event record by ID.
+// This is intended for administrative cleanup of erroneous entries; the
+// event log is otherwise append-only.
+func (s *Service) DeleteComputeAllocationChangeRequestEvent(ctx context.Context, id string) error {
+	if id == "" {
+		return fmt.Errorf("%w: compute allocation change request event id is required", ErrInvalidInput)
+	}
+	if err := s.inTx(ctx, func(tx *sql.Tx) error {
+		return s.changeEvents.Delete(ctx, tx, id)
+	}); err != nil {
+		return fmt.Errorf("delete compute allocation change request event: %w", err)
+	}
+	return nil
+}
diff --git a/pkg/service/service.go b/pkg/service/service.go
index 90de9e238..1942451be 100644
--- a/pkg/service/service.go
+++ b/pkg/service/service.go
@@ -43,6 +43,8 @@ type Service struct {
 	resourceMappings store.ComputeAllocationResourceMappingStore
 	resourceRates    store.ComputeAllocationResourceRateStore
 	allocDiffs       store.ComputeAllocationDiffStore
+	changeRequests   store.ComputeAllocationChangeRequestStore
+	changeEvents     store.ComputeAllocationChangeRequestEventStore
 }
 
 // New constructs a Service backed by the supplied database handle.
@@ -59,6 +61,8 @@ func New(database *sqlx.DB) *Service {
 		resourceMappings: store.NewComputeAllocationResourceMappingStore(database),
 		resourceRates:    store.NewComputeAllocationResourceRateStore(database),
 		allocDiffs:       store.NewComputeAllocationDiffStore(database),
+		changeRequests:   store.NewComputeAllocationChangeRequestStore(database),
+		changeEvents:     store.NewComputeAllocationChangeRequestEventStore(database),
 	}
 }
 
@@ -76,6 +80,8 @@ func NewWithStores(
 	resourceMappings store.ComputeAllocationResourceMappingStore,
 	resourceRates store.ComputeAllocationResourceRateStore,
 	allocDiffs store.ComputeAllocationDiffStore,
+	changeRequests store.ComputeAllocationChangeRequestStore,
+	changeEvents store.ComputeAllocationChangeRequestEventStore,
 ) *Service {
 	return &Service{
 		db:               database,
@@ -88,6 +94,8 @@ func NewWithStores(
 		resourceMappings: resourceMappings,
 		resourceRates:    resourceRates,
 		allocDiffs:       allocDiffs,
+		changeRequests:   changeRequests,
+		changeEvents:     changeEvents,
 	}
 }