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 9db46dcef4c845cb10f05c33a0122e88dd9236f0
Author: DImuthuUpe <[email protected]>
AuthorDate: Sat May 16 20:57:27 2026 -0400

    Implemented service layer for ComputeAllocationDiff
---
 docs/API-Docs.md                                   | 116 ++++++++++++++++++++
 .../000006_compute_allocation_diffs.down.sql       |  18 +++
 .../000006_compute_allocation_diffs.up.sql         |  33 ++++++
 internal/server/server.go                          |  55 ++++++++++
 internal/store/compute_allocation_diff_store.go    |  97 ++++++++++++++++
 internal/store/store.go                            |  18 +++
 pkg/models/allocation.go                           |  14 +--
 pkg/service/compute_allocation_diff.go             | 122 +++++++++++++++++++++
 pkg/service/service.go                             |   4 +
 9 files changed, 470 insertions(+), 7 deletions(-)

diff --git a/docs/API-Docs.md b/docs/API-Docs.md
index cec6e6331..69674e582 100644
--- a/docs/API-Docs.md
+++ b/docs/API-Docs.md
@@ -666,6 +666,107 @@ GET 
/compute-allocation-resources/c0a1.../rates/effective?at=2026-05-16T12:00:00
 
 ---
 
+## Compute Allocation Diffs
+
+A diff is an append-only audit record of a change applied to a compute
+allocation — for example a usage update or a status transition. Diffs are
+cascade-deleted when their parent allocation is deleted.
+
+### `POST /compute-allocation-diffs`
+
+Record a new diff against a compute allocation.
+
+**Required fields:** `compute_allocation_id`, `diff_type`, `status`
+**Optional fields:** `id`, `new_su_amount` (defaults to `0`), `timestamp` 
(defaults to the server's current UTC time), `description`
+
+`diff_type` is a free-form short code such as `USAGE_UPDATE` or
+`ALLOCATION_STATUS_CHANGE`. `status` must be one of `ACTIVE`, `INACTIVE`,
+`DELETED`.
+
+**Request**
+
+```json
+{
+  "compute_allocation_id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56",
+  "diff_type": "USAGE_UPDATE",
+  "new_su_amount": 90000,
+  "status": "ACTIVE",
+  "description": "Charged 10000 SUs for completed jobs"
+}
+```
+
+**Response 201**
+
+```json
+{
+  "id": "44bb55cc-66dd-77ee-88ff-aabbccddeeff",
+  "compute_allocation_id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56",
+  "diff_type": "USAGE_UPDATE",
+  "new_su_amount": 90000,
+  "status": "ACTIVE",
+  "timestamp": "2026-05-16T17:42:11.918Z",
+  "description": "Charged 10000 SUs for completed jobs"
+}
+```
+
+**Errors**
+
+- `400` — required field missing, or `compute_allocation_id` does not exist.
+
+---
+
+### `GET /compute-allocation-diffs/{id}`
+
+Retrieve a single diff by its ID.
+
+**Errors**
+
+- `404` — no diff matches the supplied ID.
+
+---
+
+### `DELETE /compute-allocation-diffs/{id}`
+
+Remove a diff record. Intended for administrative cleanup; diffs are
+otherwise append-only.
+
+**Response 204** — empty body on success.
+
+---
+
+### `GET /compute-allocations/{id}/diffs`
+
+List every diff ever recorded against the given compute allocation, ordered
+by `timestamp` ascending.
+
+**Response 200**
+
+```json
+[
+  {
+    "id": "44bb55cc-66dd-77ee-88ff-aabbccddeeff",
+    "compute_allocation_id": "2f6a8c1d-3e4b-4a7d-8c91-aa12bb34cc56",
+    "diff_type": "USAGE_UPDATE",
+    "new_su_amount": 90000,
+    "status": "ACTIVE",
+    "timestamp": "2026-05-16T17:42:11.918Z",
+    "description": "Charged 10000 SUs for completed jobs"
+  }
+]
+```
+
+---
+
+### `GET /compute-allocations/{id}/diffs/latest`
+
+Return the most recent diff (highest `timestamp`) for the given allocation.
+
+**Errors**
+
+- `404` — the allocation has no diffs recorded.
+
+---
+
 ## End-to-end example
 
 ```bash
@@ -717,6 +818,21 @@ curl -s -X POST $BASE/compute-allocation-resource-rates \
 # Look up the currently-effective rate.
 curl -s $BASE/compute-allocation-resources/$RES_ID/rates/effective | jq
 
+# Record a usage diff against the allocation.
+curl -s -X POST $BASE/compute-allocation-diffs \
+  -H 'Content-Type: application/json' \
+  -d "{
+        \"compute_allocation_id\":\"$ALLOC_ID\",
+        \"diff_type\":\"USAGE_UPDATE\",
+        \"new_su_amount\":90000,
+        \"status\":\"ACTIVE\",
+        \"description\":\"Charged 10000 SUs for completed jobs\"
+      }" | jq
+
+# Inspect the diff history.
+curl -s $BASE/compute-allocations/$ALLOC_ID/diffs | jq
+curl -s $BASE/compute-allocations/$ALLOC_ID/diffs/latest | jq
+
 # Bidirectional lookups.
 curl -s $BASE/compute-allocations/$ALLOC_ID/resources | jq
 curl -s $BASE/compute-allocation-resources/$RES_ID/allocations | jq
diff --git a/internal/db/migrations/000006_compute_allocation_diffs.down.sql 
b/internal/db/migrations/000006_compute_allocation_diffs.down.sql
new file mode 100644
index 000000000..b0a79aecf
--- /dev/null
+++ b/internal/db/migrations/000006_compute_allocation_diffs.down.sql
@@ -0,0 +1,18 @@
+-- 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_diffs;
diff --git a/internal/db/migrations/000006_compute_allocation_diffs.up.sql 
b/internal/db/migrations/000006_compute_allocation_diffs.up.sql
new file mode 100644
index 000000000..f702cb8c1
--- /dev/null
+++ b/internal/db/migrations/000006_compute_allocation_diffs.up.sql
@@ -0,0 +1,33 @@
+-- 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_diffs
+(
+    id                    VARCHAR(255) NOT NULL,
+    compute_allocation_id VARCHAR(255) NOT NULL,
+    diff_type             VARCHAR(64)  NOT NULL,
+    new_su_amount         BIGINT       NOT NULL DEFAULT 0,
+    status                VARCHAR(64)  NOT NULL,
+    timestamp             TIMESTAMP(6) NOT NULL,
+    description           TEXT         NOT NULL,
+    created_at            TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
+    PRIMARY KEY (id),
+    KEY idx_compute_allocation_diffs_allocation (compute_allocation_id, 
timestamp),
+    KEY idx_compute_allocation_diffs_type (diff_type),
+    CONSTRAINT fk_compute_allocation_diffs_allocation FOREIGN KEY 
(compute_allocation_id)
+        REFERENCES compute_allocations (id) ON DELETE CASCADE
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
diff --git a/internal/server/server.go b/internal/server/server.go
index 89eb31946..3638c0751 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -80,6 +80,12 @@ func (s *Server) routes() {
        s.mux.HandleFunc("GET /compute-allocation-resource-rates/{id}", 
s.getComputeAllocationResourceRate)
        s.mux.HandleFunc("GET /compute-allocation-resources/{id}/rates", 
s.listRatesForResource)
        s.mux.HandleFunc("GET 
/compute-allocation-resources/{id}/rates/effective", 
s.getEffectiveRateForResource)
+
+       s.mux.HandleFunc("POST /compute-allocation-diffs", 
s.createComputeAllocationDiff)
+       s.mux.HandleFunc("GET /compute-allocation-diffs/{id}", 
s.getComputeAllocationDiff)
+       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)
 }
 
 func (s *Server) healthz(w http.ResponseWriter, _ *http.Request) {
@@ -336,6 +342,55 @@ func (s *Server) getEffectiveRateForResource(w 
http.ResponseWriter, r *http.Requ
        writeJSON(w, http.StatusOK, rate)
 }
 
+func (s *Server) createComputeAllocationDiff(w http.ResponseWriter, r 
*http.Request) {
+       var diff models.ComputeAllocationDiff
+       if err := decodeJSON(r, &diff); err != nil {
+               writeError(w, http.StatusBadRequest, err)
+               return
+       }
+       created, err := s.svc.CreateComputeAllocationDiff(r.Context(), &diff)
+       if err != nil {
+               writeServiceError(w, err)
+               return
+       }
+       writeJSON(w, http.StatusCreated, created)
+}
+
+func (s *Server) getComputeAllocationDiff(w http.ResponseWriter, r 
*http.Request) {
+       diff, err := s.svc.GetComputeAllocationDiff(r.Context(), 
r.PathValue("id"))
+       if err != nil {
+               writeServiceError(w, err)
+               return
+       }
+       writeJSON(w, http.StatusOK, diff)
+}
+
+func (s *Server) deleteComputeAllocationDiff(w http.ResponseWriter, r 
*http.Request) {
+       if err := s.svc.DeleteComputeAllocationDiff(r.Context(), 
r.PathValue("id")); err != nil {
+               writeServiceError(w, err)
+               return
+       }
+       w.WriteHeader(http.StatusNoContent)
+}
+
+func (s *Server) listDiffsForAllocation(w http.ResponseWriter, r 
*http.Request) {
+       diffs, err := s.svc.ListDiffsForAllocation(r.Context(), 
r.PathValue("id"))
+       if err != nil {
+               writeServiceError(w, err)
+               return
+       }
+       writeJSON(w, http.StatusOK, diffs)
+}
+
+func (s *Server) getLatestDiffForAllocation(w http.ResponseWriter, r 
*http.Request) {
+       diff, err := s.svc.GetLatestDiffForAllocation(r.Context(), 
r.PathValue("id"))
+       if err != nil {
+               writeServiceError(w, err)
+               return
+       }
+       writeJSON(w, http.StatusOK, diff)
+}
+
 // 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_diff_store.go 
b/internal/store/compute_allocation_diff_store.go
new file mode 100644
index 000000000..f4fd0f10a
--- /dev/null
+++ b/internal/store/compute_allocation_diff_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 computeAllocationDiffColumns = "id, compute_allocation_id, diff_type, 
new_su_amount, status, timestamp, description"
+
+type mysqlComputeAllocationDiffStore struct {
+       db *sqlx.DB
+}
+
+// NewComputeAllocationDiffStore returns a MySQL-backed
+// ComputeAllocationDiffStore.
+func NewComputeAllocationDiffStore(db *sqlx.DB) ComputeAllocationDiffStore {
+       return &mysqlComputeAllocationDiffStore{db: db}
+}
+
+func (s *mysqlComputeAllocationDiffStore) FindByID(ctx context.Context, id 
string) (*models.ComputeAllocationDiff, error) {
+       var d models.ComputeAllocationDiff
+       err := s.db.GetContext(ctx, &d,
+               `SELECT `+computeAllocationDiffColumns+` FROM 
compute_allocation_diffs WHERE id = ?`, id)
+       if err != nil {
+               if errors.Is(err, sql.ErrNoRows) {
+                       return nil, nil
+               }
+               return nil, err
+       }
+       return &d, nil
+}
+
+func (s *mysqlComputeAllocationDiffStore) FindByAllocation(ctx 
context.Context, allocationID string) ([]models.ComputeAllocationDiff, error) {
+       var diffs []models.ComputeAllocationDiff
+       err := s.db.SelectContext(ctx, &diffs,
+               `SELECT `+computeAllocationDiffColumns+`
+                FROM compute_allocation_diffs
+                WHERE compute_allocation_id = ?
+                ORDER BY timestamp`, allocationID)
+       if err != nil {
+               return nil, err
+       }
+       return diffs, nil
+}
+
+func (s *mysqlComputeAllocationDiffStore) FindLatestByAllocation(ctx 
context.Context, allocationID string) (*models.ComputeAllocationDiff, error) {
+       var d models.ComputeAllocationDiff
+       err := s.db.GetContext(ctx, &d,
+               `SELECT `+computeAllocationDiffColumns+`
+                FROM compute_allocation_diffs
+                WHERE compute_allocation_id = ?
+                ORDER BY timestamp DESC
+                LIMIT 1`, allocationID)
+       if err != nil {
+               if errors.Is(err, sql.ErrNoRows) {
+                       return nil, nil
+               }
+               return nil, err
+       }
+       return &d, nil
+}
+
+func (s *mysqlComputeAllocationDiffStore) Create(ctx context.Context, tx 
*sql.Tx, d *models.ComputeAllocationDiff) error {
+       _, err := tx.ExecContext(ctx,
+               `INSERT INTO compute_allocation_diffs
+                    (id, compute_allocation_id, diff_type, new_su_amount, 
status, timestamp, description)
+                VALUES (?, ?, ?, ?, ?, ?, ?)`,
+               d.ID, d.ComputeAllocationID, d.DiffType, d.NewSUAmount, 
string(d.Status), d.Timestamp, d.Description)
+       return err
+}
+
+func (s *mysqlComputeAllocationDiffStore) Delete(ctx context.Context, tx 
*sql.Tx, id string) error {
+       _, err := tx.ExecContext(ctx, `DELETE FROM compute_allocation_diffs 
WHERE id = ?`, id)
+       return err
+}
diff --git a/internal/store/store.go b/internal/store/store.go
index 2ad427c1f..6624d8f5a 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -152,3 +152,21 @@ type ComputeAllocationResourceRateStore interface {
        // Delete removes a rate by ID within the provided transaction.
        Delete(ctx context.Context, tx *sql.Tx, id string) error
 }
+
+// ComputeAllocationDiffStore defines persistence operations for the
+// append-only log of changes (SU updates, status changes, etc.) applied to a
+// compute allocation.
+type ComputeAllocationDiffStore interface {
+       // FindByID returns the diff with the given ID, or nil if it does not 
exist.
+       FindByID(ctx context.Context, id string) 
(*models.ComputeAllocationDiff, error)
+       // FindByAllocation returns every diff ever recorded for the given 
allocation,
+       // ordered by timestamp ascending.
+       FindByAllocation(ctx context.Context, allocationID string) 
([]models.ComputeAllocationDiff, error)
+       // FindLatestByAllocation returns the most recent diff for the given
+       // allocation (highest timestamp), or nil if none exist.
+       FindLatestByAllocation(ctx context.Context, allocationID string) 
(*models.ComputeAllocationDiff, error)
+       // Create inserts a new diff within the provided transaction.
+       Create(ctx context.Context, tx *sql.Tx, d 
*models.ComputeAllocationDiff) error
+       // Delete removes a diff 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 d532d0177..b3e284927 100644
--- a/pkg/models/allocation.go
+++ b/pkg/models/allocation.go
@@ -48,13 +48,13 @@ type ComputeAllocationResourceRate struct {
 }
 
 type ComputeAllocationDiff struct { // Diff will occur either through a change 
reqest or automated workflow like ACCESS AIME
-       ID                  string           `json:"id"`
-       ComputeAllocationID string           `json:"compute_allocation_id"`
-       DiffType            string           `json:"diff_type"`             // 
"USAGE_UPDATE", "ALLOCATION_STATUS_CHANGE", etc.
-       NewSUAmount         int64            `json:"new_su_amount"`         // 
New allocation amount in SUs, e.g., 900 SUs, etc.
-       Status              AllocationStatus `json:"status"`                // 
ACTIVE, INACTIVE, DELETED, etc.
-       Timestamp           time.Time        `json:"timestamp"`             // 
The time when the diff was generated.
-       Description         string           `json:"description,omitempty"` // 
Optional description of the diff, e.g., "SU usage updated based on job 
completion", "Allocation marked as INACTIVE due to end time reached", etc.
+       ID                  string           `json:"id"                    
db:"id"`
+       ComputeAllocationID string           `json:"compute_allocation_id" 
db:"compute_allocation_id"`
+       DiffType            string           `json:"diff_type"             
db:"diff_type"`     // "USAGE_UPDATE", "ALLOCATION_STATUS_CHANGE", etc.
+       NewSUAmount         int64            `json:"new_su_amount"         
db:"new_su_amount"` // New allocation amount in SUs, e.g., 900 SUs, etc.
+       Status              AllocationStatus `json:"status"                
db:"status"`        // ACTIVE, INACTIVE, DELETED, etc.
+       Timestamp           time.Time        `json:"timestamp"             
db:"timestamp"`     // The time when the diff was generated.
+       Description         string           `json:"description,omitempty" 
db:"description"`   // Optional description of the diff, e.g., "SU usage 
updated based on job completion", "Allocation marked as INACTIVE due to end 
time reached", etc.
 }
 
 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.
diff --git a/pkg/service/compute_allocation_diff.go 
b/pkg/service/compute_allocation_diff.go
new file mode 100644
index 000000000..49a414bf2
--- /dev/null
+++ b/pkg/service/compute_allocation_diff.go
@@ -0,0 +1,122 @@
+// 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"
+)
+
+// CreateComputeAllocationDiff records a new diff against a compute allocation.
+// The referenced allocation must exist. If Timestamp is zero the server's
+// current UTC time is used. Diff records are intended to be append-only and
+// have no update operation.
+func (s *Service) CreateComputeAllocationDiff(ctx context.Context, diff 
*models.ComputeAllocationDiff) (*models.ComputeAllocationDiff, error) {
+       if diff == nil {
+               return nil, fmt.Errorf("%w: compute allocation diff is nil", 
ErrInvalidInput)
+       }
+       if diff.ComputeAllocationID == "" {
+               return nil, fmt.Errorf("%w: compute_allocation_id is required", 
ErrInvalidInput)
+       }
+       if diff.DiffType == "" {
+               return nil, fmt.Errorf("%w: diff_type is required", 
ErrInvalidInput)
+       }
+       if diff.Status == "" {
+               return nil, fmt.Errorf("%w: status is required", 
ErrInvalidInput)
+       }
+
+       if alloc, err := s.allocs.FindByID(ctx, diff.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, diff.ComputeAllocationID)
+       }
+
+       if diff.ID == "" {
+               diff.ID = newID()
+       }
+       if diff.Timestamp.IsZero() {
+               diff.Timestamp = nowUTC()
+       }
+
+       if err := s.inTx(ctx, func(tx *sql.Tx) error {
+               return s.allocDiffs.Create(ctx, tx, diff)
+       }); err != nil {
+               return nil, fmt.Errorf("create compute allocation diff: %w", 
err)
+       }
+       return diff, nil
+}
+
+// GetComputeAllocationDiff retrieves a diff by its ID. Returns ErrNotFound
+// when no diff matches.
+func (s *Service) GetComputeAllocationDiff(ctx context.Context, id string) 
(*models.ComputeAllocationDiff, error) {
+       d, err := s.allocDiffs.FindByID(ctx, id)
+       if err != nil {
+               return nil, fmt.Errorf("get compute allocation diff: %w", err)
+       }
+       if d == nil {
+               return nil, ErrNotFound
+       }
+       return d, nil
+}
+
+// ListDiffsForAllocation returns every diff ever recorded against the given
+// compute allocation, ordered chronologically by timestamp.
+func (s *Service) ListDiffsForAllocation(ctx context.Context, allocationID 
string) ([]models.ComputeAllocationDiff, error) {
+       if allocationID == "" {
+               return nil, fmt.Errorf("%w: compute_allocation_id is required", 
ErrInvalidInput)
+       }
+       diffs, err := s.allocDiffs.FindByAllocation(ctx, allocationID)
+       if err != nil {
+               return nil, fmt.Errorf("list diffs for allocation: %w", err)
+       }
+       return diffs, nil
+}
+
+// GetLatestDiffForAllocation returns the most recent diff for the given
+// allocation. Returns ErrNotFound when the allocation has no diffs.
+func (s *Service) GetLatestDiffForAllocation(ctx context.Context, allocationID 
string) (*models.ComputeAllocationDiff, error) {
+       if allocationID == "" {
+               return nil, fmt.Errorf("%w: compute_allocation_id is required", 
ErrInvalidInput)
+       }
+       d, err := s.allocDiffs.FindLatestByAllocation(ctx, allocationID)
+       if err != nil {
+               return nil, fmt.Errorf("get latest diff for allocation: %w", 
err)
+       }
+       if d == nil {
+               return nil, ErrNotFound
+       }
+       return d, nil
+}
+
+// DeleteComputeAllocationDiff removes a diff record by ID. This is intended
+// for administrative cleanup of erroneous entries; the diff log is otherwise
+// append-only.
+func (s *Service) DeleteComputeAllocationDiff(ctx context.Context, id string) 
error {
+       if id == "" {
+               return fmt.Errorf("%w: compute allocation diff id is required", 
ErrInvalidInput)
+       }
+       if err := s.inTx(ctx, func(tx *sql.Tx) error {
+               return s.allocDiffs.Delete(ctx, tx, id)
+       }); err != nil {
+               return fmt.Errorf("delete compute allocation diff: %w", err)
+       }
+       return nil
+}
diff --git a/pkg/service/service.go b/pkg/service/service.go
index 119f12483..90de9e238 100644
--- a/pkg/service/service.go
+++ b/pkg/service/service.go
@@ -42,6 +42,7 @@ type Service struct {
        resources        store.ComputeAllocationResourceStore
        resourceMappings store.ComputeAllocationResourceMappingStore
        resourceRates    store.ComputeAllocationResourceRateStore
+       allocDiffs       store.ComputeAllocationDiffStore
 }
 
 // New constructs a Service backed by the supplied database handle.
@@ -57,6 +58,7 @@ func New(database *sqlx.DB) *Service {
                resources:        
store.NewComputeAllocationResourceStore(database),
                resourceMappings: 
store.NewComputeAllocationResourceMappingStore(database),
                resourceRates:    
store.NewComputeAllocationResourceRateStore(database),
+               allocDiffs:       store.NewComputeAllocationDiffStore(database),
        }
 }
 
@@ -73,6 +75,7 @@ func NewWithStores(
        resources store.ComputeAllocationResourceStore,
        resourceMappings store.ComputeAllocationResourceMappingStore,
        resourceRates store.ComputeAllocationResourceRateStore,
+       allocDiffs store.ComputeAllocationDiffStore,
 ) *Service {
        return &Service{
                db:               database,
@@ -84,6 +87,7 @@ func NewWithStores(
                resources:        resources,
                resourceMappings: resourceMappings,
                resourceRates:    resourceRates,
+               allocDiffs:       allocDiffs,
        }
 }
 

Reply via email to