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 d486a0c422e4b686304ea4b6e06f588bd35f75e2
Author: DImuthuUpe <[email protected]>
AuthorDate: Sat May 16 20:46:00 2026 -0400

    Implemented Resource Rate APIs
---
 docs/API-Docs.md                                   | 130 +++++++++++++++++++
 ...0005_compute_allocation_resource_rates.down.sql |  18 +++
 ...000005_compute_allocation_resource_rates.up.sql |  32 +++++
 internal/server/server.go                          |  55 ++++++++
 .../compute_allocation_resource_rate_store.go      | 110 ++++++++++++++++
 internal/store/store.go                            |  21 +++
 pkg/models/allocation.go                           |  10 +-
 pkg/service/compute_allocation_resource_rate.go    | 144 +++++++++++++++++++++
 pkg/service/service.go                             |   4 +
 9 files changed, 519 insertions(+), 5 deletions(-)

diff --git a/docs/API-Docs.md b/docs/API-Docs.md
index f509a3000..cec6e6331 100644
--- a/docs/API-Docs.md
+++ b/docs/API-Docs.md
@@ -549,6 +549,123 @@ List every compute allocation that has the given resource 
attached.
 
 ---
 
+## Compute Allocation Resource Rates
+
+A rate captures how many Service Units (SUs) are charged per unit of a
+compute allocation resource over a bounded time window. Multiple rates can
+exist for the same resource; usage at any instant is charged using the rate
+whose `[start_time, end_time)` window contains that instant.
+
+Rates are cascade-deleted when their parent resource is deleted.
+
+### `POST /compute-allocation-resource-rates`
+
+Create a new rate for a compute allocation resource.
+
+**Required fields:** `compute_allocation_resource_id`, `rate`, `start_time`, 
`end_time`
+**Optional fields:** `id`
+
+Validation:
+
+- `compute_allocation_resource_id` must reference an existing resource.
+- `rate` must be ≥ 0.
+- `start_time` must be strictly before `end_time`.
+
+**Request**
+
+```json
+{
+  "compute_allocation_resource_id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff",
+  "rate": 2.0,
+  "start_time": "2026-01-01T00:00:00Z",
+  "end_time":   "2026-12-31T23:59:59Z"
+}
+```
+
+**Response 201**
+
+```json
+{
+  "id": "55aa66bb-77cc-88dd-99ee-001122334455",
+  "compute_allocation_resource_id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff",
+  "rate": 2.0,
+  "start_time": "2026-01-01T00:00:00Z",
+  "end_time":   "2026-12-31T23:59:59Z"
+}
+```
+
+**Errors**
+
+- `400` — required field missing, invalid time window, negative `rate`, or 
unknown `compute_allocation_resource_id`.
+
+---
+
+### `GET /compute-allocation-resource-rates/{id}`
+
+Retrieve a rate by its ID.
+
+**Errors**
+
+- `404` — no rate matches the supplied ID.
+
+---
+
+### `GET /compute-allocation-resources/{id}/rates`
+
+List every rate ever defined for the given compute allocation resource,
+ordered by `start_time` ascending.
+
+**Response 200**
+
+```json
+[
+  {
+    "id": "55aa66bb-77cc-88dd-99ee-001122334455",
+    "compute_allocation_resource_id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff",
+    "rate": 2.0,
+    "start_time": "2026-01-01T00:00:00Z",
+    "end_time":   "2026-12-31T23:59:59Z"
+  }
+]
+```
+
+---
+
+### `GET /compute-allocation-resources/{id}/rates/effective`
+
+Return the rate currently in effect for the given resource. By default the
+server uses the current time; supply `?at=<RFC 3339 timestamp>` to query an
+arbitrary instant.
+
+A rate is "effective" at instant *t* when `start_time <= t < end_time`. If
+multiple rates overlap *t*, the one with the most recent `start_time` wins.
+
+**Examples**
+
+```http
+GET /compute-allocation-resources/c0a1.../rates/effective
+GET 
/compute-allocation-resources/c0a1.../rates/effective?at=2026-05-16T12:00:00Z
+```
+
+**Response 200**
+
+```json
+{
+  "id": "55aa66bb-77cc-88dd-99ee-001122334455",
+  "compute_allocation_resource_id": "c0a1b2c3-d4e5-46f7-8899-aabbccddeeff",
+  "rate": 2.0,
+  "start_time": "2026-01-01T00:00:00Z",
+  "end_time":   "2026-12-31T23:59:59Z"
+}
+```
+
+**Errors**
+
+- `400` — `at` query parameter is not a valid RFC 3339 timestamp.
+- `404` — no rate is effective for the resource at the supplied instant.
+
+---
+
 ## End-to-end example
 
 ```bash
@@ -587,6 +704,19 @@ curl -s -X POST 
$BASE/compute-allocations/$ALLOC_ID/resources \
   -H 'Content-Type: application/json' \
   -d "{\"compute_allocation_resource_id\":\"$RES_ID\"}" | jq
 
+# Define a rate for the resource.
+curl -s -X POST $BASE/compute-allocation-resource-rates \
+  -H 'Content-Type: application/json' \
+  -d "{
+        \"compute_allocation_resource_id\":\"$RES_ID\",
+        \"rate\":2.0,
+        \"start_time\":\"2026-01-01T00:00:00Z\",
+        \"end_time\":\"2026-12-31T23:59:59Z\"
+      }" | jq
+
+# Look up the currently-effective rate.
+curl -s $BASE/compute-allocation-resources/$RES_ID/rates/effective | 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/000005_compute_allocation_resource_rates.down.sql 
b/internal/db/migrations/000005_compute_allocation_resource_rates.down.sql
new file mode 100644
index 000000000..b6938f6ea
--- /dev/null
+++ b/internal/db/migrations/000005_compute_allocation_resource_rates.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_resource_rates;
diff --git 
a/internal/db/migrations/000005_compute_allocation_resource_rates.up.sql 
b/internal/db/migrations/000005_compute_allocation_resource_rates.up.sql
new file mode 100644
index 000000000..e49346c5e
--- /dev/null
+++ b/internal/db/migrations/000005_compute_allocation_resource_rates.up.sql
@@ -0,0 +1,32 @@
+-- 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_resource_rates
+(
+    id                             VARCHAR(255)     NOT NULL,
+    compute_allocation_resource_id VARCHAR(255)     NOT NULL,
+    rate                           DOUBLE           NOT NULL DEFAULT 0,
+    start_time                     TIMESTAMP(6)     NOT NULL,
+    end_time                       TIMESTAMP(6)     NOT NULL,
+    created_at                     TIMESTAMP(6)     NOT NULL DEFAULT 
CURRENT_TIMESTAMP(6),
+    updated_at                     TIMESTAMP(6)     NOT NULL DEFAULT 
CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
+    PRIMARY KEY (id),
+    KEY idx_carr_rates_resource (compute_allocation_resource_id),
+    KEY idx_carr_rates_window (compute_allocation_resource_id, start_time, 
end_time),
+    CONSTRAINT fk_carr_rates_resource FOREIGN KEY 
(compute_allocation_resource_id)
+        REFERENCES compute_allocation_resources (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 5207c724d..89eb31946 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -75,6 +75,11 @@ func (s *Server) routes() {
        s.mux.HandleFunc("POST /compute-allocations/{id}/resources", 
s.attachResourceToAllocation)
        s.mux.HandleFunc("DELETE 
/compute-allocations/{id}/resources/{resourceId}", 
s.detachResourceFromAllocation)
        s.mux.HandleFunc("GET /compute-allocation-resources/{id}/allocations", 
s.listAllocationsForResource)
+
+       s.mux.HandleFunc("POST /compute-allocation-resource-rates", 
s.createComputeAllocationResourceRate)
+       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)
 }
 
 func (s *Server) healthz(w http.ResponseWriter, _ *http.Request) {
@@ -281,6 +286,56 @@ func (s *Server) listAllocationsForResource(w 
http.ResponseWriter, r *http.Reque
        writeJSON(w, http.StatusOK, allocs)
 }
 
+func (s *Server) createComputeAllocationResourceRate(w http.ResponseWriter, r 
*http.Request) {
+       var rate models.ComputeAllocationResourceRate
+       if err := decodeJSON(r, &rate); err != nil {
+               writeError(w, http.StatusBadRequest, err)
+               return
+       }
+       created, err := s.svc.CreateComputeAllocationResourceRate(r.Context(), 
&rate)
+       if err != nil {
+               writeServiceError(w, err)
+               return
+       }
+       writeJSON(w, http.StatusCreated, created)
+}
+
+func (s *Server) getComputeAllocationResourceRate(w http.ResponseWriter, r 
*http.Request) {
+       rate, err := s.svc.GetComputeAllocationResourceRate(r.Context(), 
r.PathValue("id"))
+       if err != nil {
+               writeServiceError(w, err)
+               return
+       }
+       writeJSON(w, http.StatusOK, rate)
+}
+
+func (s *Server) listRatesForResource(w http.ResponseWriter, r *http.Request) {
+       rates, err := s.svc.ListRatesForResource(r.Context(), r.PathValue("id"))
+       if err != nil {
+               writeServiceError(w, err)
+               return
+       }
+       writeJSON(w, http.StatusOK, rates)
+}
+
+func (s *Server) getEffectiveRateForResource(w http.ResponseWriter, r 
*http.Request) {
+       var at time.Time
+       if raw := r.URL.Query().Get("at"); raw != "" {
+               parsed, err := time.Parse(time.RFC3339Nano, raw)
+               if err != nil {
+                       writeError(w, http.StatusBadRequest, 
errors.New("invalid 'at' query parameter; expected RFC 3339"))
+                       return
+               }
+               at = parsed
+       }
+       rate, err := s.svc.GetEffectiveRateForResource(r.Context(), 
r.PathValue("id"), at)
+       if err != nil {
+               writeServiceError(w, err)
+               return
+       }
+       writeJSON(w, http.StatusOK, rate)
+}
+
 // 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_resource_rate_store.go 
b/internal/store/compute_allocation_resource_rate_store.go
new file mode 100644
index 000000000..de9426797
--- /dev/null
+++ b/internal/store/compute_allocation_resource_rate_store.go
@@ -0,0 +1,110 @@
+// 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"
+       "time"
+
+       "github.com/jmoiron/sqlx"
+
+       "github.com/apache/airavata-custos/pkg/models"
+)
+
+const computeAllocationResourceRateColumns = "id, 
compute_allocation_resource_id, rate, start_time, end_time"
+
+type mysqlComputeAllocationResourceRateStore struct {
+       db *sqlx.DB
+}
+
+// NewComputeAllocationResourceRateStore returns a MySQL-backed
+// ComputeAllocationResourceRateStore.
+func NewComputeAllocationResourceRateStore(db *sqlx.DB) 
ComputeAllocationResourceRateStore {
+       return &mysqlComputeAllocationResourceRateStore{db: db}
+}
+
+func (s *mysqlComputeAllocationResourceRateStore) FindByID(ctx 
context.Context, id string) (*models.ComputeAllocationResourceRate, error) {
+       var r models.ComputeAllocationResourceRate
+       err := s.db.GetContext(ctx, &r,
+               `SELECT `+computeAllocationResourceRateColumns+`
+                FROM compute_allocation_resource_rates WHERE id = ?`, id)
+       if err != nil {
+               if errors.Is(err, sql.ErrNoRows) {
+                       return nil, nil
+               }
+               return nil, err
+       }
+       return &r, nil
+}
+
+func (s *mysqlComputeAllocationResourceRateStore) FindByResource(ctx 
context.Context, resourceID string) ([]models.ComputeAllocationResourceRate, 
error) {
+       var rates []models.ComputeAllocationResourceRate
+       err := s.db.SelectContext(ctx, &rates,
+               `SELECT `+computeAllocationResourceRateColumns+`
+                FROM compute_allocation_resource_rates
+                WHERE compute_allocation_resource_id = ?
+                ORDER BY start_time`, resourceID)
+       if err != nil {
+               return nil, err
+       }
+       return rates, nil
+}
+
+func (s *mysqlComputeAllocationResourceRateStore) FindEffective(ctx 
context.Context, resourceID string, at time.Time) 
(*models.ComputeAllocationResourceRate, error) {
+       var r models.ComputeAllocationResourceRate
+       err := s.db.GetContext(ctx, &r,
+               `SELECT `+computeAllocationResourceRateColumns+`
+                FROM compute_allocation_resource_rates
+                WHERE compute_allocation_resource_id = ?
+                  AND start_time <= ?
+                  AND end_time   >  ?
+                ORDER BY start_time DESC
+                LIMIT 1`, resourceID, at, at)
+       if err != nil {
+               if errors.Is(err, sql.ErrNoRows) {
+                       return nil, nil
+               }
+               return nil, err
+       }
+       return &r, nil
+}
+
+func (s *mysqlComputeAllocationResourceRateStore) Create(ctx context.Context, 
tx *sql.Tx, r *models.ComputeAllocationResourceRate) error {
+       _, err := tx.ExecContext(ctx,
+               `INSERT INTO compute_allocation_resource_rates
+                    (id, compute_allocation_resource_id, rate, start_time, 
end_time)
+                VALUES (?, ?, ?, ?, ?)`,
+               r.ID, r.ComputeAllocationResourceID, r.Rate, r.StartTime, 
r.EndTime)
+       return err
+}
+
+func (s *mysqlComputeAllocationResourceRateStore) Update(ctx context.Context, 
tx *sql.Tx, r *models.ComputeAllocationResourceRate) error {
+       _, err := tx.ExecContext(ctx,
+               `UPDATE compute_allocation_resource_rates
+                SET rate = ?, start_time = ?, end_time = ?
+                WHERE id = ?`,
+               r.Rate, r.StartTime, r.EndTime, r.ID)
+       return err
+}
+
+func (s *mysqlComputeAllocationResourceRateStore) Delete(ctx context.Context, 
tx *sql.Tx, id string) error {
+       _, err := tx.ExecContext(ctx, `DELETE FROM 
compute_allocation_resource_rates WHERE id = ?`, id)
+       return err
+}
diff --git a/internal/store/store.go b/internal/store/store.go
index 013f948d1..2ad427c1f 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -20,6 +20,7 @@ package store
 import (
        "context"
        "database/sql"
+       "time"
 
        "github.com/apache/airavata-custos/pkg/models"
 )
@@ -131,3 +132,23 @@ type ComputeAllocationResourceMappingStore interface {
        // DeleteByPair removes the mapping for a (allocation, resource) pair 
within the provided transaction.
        DeleteByPair(ctx context.Context, tx *sql.Tx, allocationID, resourceID 
string) error
 }
+
+// ComputeAllocationResourceRateStore defines persistence operations for
+// the time-windowed rate at which a compute allocation resource is charged
+// in Service Units.
+type ComputeAllocationResourceRateStore interface {
+       // FindByID returns the rate with the given ID, or nil if it does not 
exist.
+       FindByID(ctx context.Context, id string) 
(*models.ComputeAllocationResourceRate, error)
+       // FindByResource returns every rate ever defined for the given 
resource,
+       // ordered by start_time ascending.
+       FindByResource(ctx context.Context, resourceID string) 
([]models.ComputeAllocationResourceRate, error)
+       // FindEffective returns the rate effective for the given resource at 
the
+       // supplied instant (start_time <= at < end_time), or nil if none 
applies.
+       FindEffective(ctx context.Context, resourceID string, at time.Time) 
(*models.ComputeAllocationResourceRate, error)
+       // Create inserts a new rate within the provided transaction.
+       Create(ctx context.Context, tx *sql.Tx, r 
*models.ComputeAllocationResourceRate) error
+       // Update replaces mutable fields of an existing rate within the 
provided transaction.
+       Update(ctx context.Context, tx *sql.Tx, r 
*models.ComputeAllocationResourceRate) error
+       // Delete removes a rate 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 8d879af1a..d532d0177 100644
--- a/pkg/models/allocation.go
+++ b/pkg/models/allocation.go
@@ -40,11 +40,11 @@ type ComputeAllocationResourceMapping struct {
 }
 
 type ComputeAllocationResourceRate struct {
-       ID                          string    `json:"id"`
-       ComputeAllocationResourceID string    
`json:"compute_allocation_resource_id"`
-       Rate                        float64   `json:"rate"`       // The rate 
for the resource in SUs per unit, e.g., 0.5 SU per CPU hour, 2 SU per GPU hour, 
etc.
-       StartTime                   time.Time `json:"start_time"` // The time 
when this rate becomes effective.
-       EndTime                     time.Time `json:"end_time"`   // The time 
when this rate expires.
+       ID                          string    `json:"id"                        
     db:"id"`
+       ComputeAllocationResourceID string    
`json:"compute_allocation_resource_id" db:"compute_allocation_resource_id"`
+       Rate                        float64   `json:"rate"                      
     db:"rate"`       // The rate for the resource in SUs per unit, e.g., 0.5 
SU per CPU hour, 2 SU per GPU hour, etc.
+       StartTime                   time.Time `json:"start_time"                
     db:"start_time"` // The time when this rate becomes effective.
+       EndTime                     time.Time `json:"end_time"                  
     db:"end_time"`   // The time when this rate expires.
 }
 
 type ComputeAllocationDiff struct { // Diff will occur either through a change 
reqest or automated workflow like ACCESS AIME
diff --git a/pkg/service/compute_allocation_resource_rate.go 
b/pkg/service/compute_allocation_resource_rate.go
new file mode 100644
index 000000000..efd3a3c70
--- /dev/null
+++ b/pkg/service/compute_allocation_resource_rate.go
@@ -0,0 +1,144 @@
+// 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"
+       "time"
+
+       "github.com/apache/airavata-custos/pkg/models"
+)
+
+// CreateComputeAllocationResourceRate persists a new rate for a compute
+// allocation resource. The referenced resource must exist, start_time must be
+// strictly before end_time, and rate must be non-negative.
+func (s *Service) CreateComputeAllocationResourceRate(ctx context.Context, 
rate *models.ComputeAllocationResourceRate) 
(*models.ComputeAllocationResourceRate, error) {
+       if rate == nil {
+               return nil, fmt.Errorf("%w: compute allocation resource rate is 
nil", ErrInvalidInput)
+       }
+       if rate.ComputeAllocationResourceID == "" {
+               return nil, fmt.Errorf("%w: compute_allocation_resource_id is 
required", ErrInvalidInput)
+       }
+       if rate.Rate < 0 {
+               return nil, fmt.Errorf("%w: rate must be non-negative", 
ErrInvalidInput)
+       }
+       if rate.StartTime.IsZero() || rate.EndTime.IsZero() {
+               return nil, fmt.Errorf("%w: start_time and end_time are 
required", ErrInvalidInput)
+       }
+       if !rate.StartTime.Before(rate.EndTime) {
+               return nil, fmt.Errorf("%w: start_time must be before 
end_time", ErrInvalidInput)
+       }
+
+       if res, err := s.resources.FindByID(ctx, 
rate.ComputeAllocationResourceID); err != nil {
+               return nil, fmt.Errorf("lookup compute allocation resource: 
%w", err)
+       } else if res == nil {
+               return nil, fmt.Errorf("%w: compute allocation resource %q not 
found", ErrInvalidInput, rate.ComputeAllocationResourceID)
+       }
+
+       if rate.ID == "" {
+               rate.ID = newID()
+       }
+
+       if err := s.inTx(ctx, func(tx *sql.Tx) error {
+               return s.resourceRates.Create(ctx, tx, rate)
+       }); err != nil {
+               return nil, fmt.Errorf("create compute allocation resource 
rate: %w", err)
+       }
+       return rate, nil
+}
+
+// GetComputeAllocationResourceRate retrieves a rate by its ID. Returns
+// ErrNotFound when no rate matches.
+func (s *Service) GetComputeAllocationResourceRate(ctx context.Context, id 
string) (*models.ComputeAllocationResourceRate, error) {
+       r, err := s.resourceRates.FindByID(ctx, id)
+       if err != nil {
+               return nil, fmt.Errorf("get compute allocation resource rate: 
%w", err)
+       }
+       if r == nil {
+               return nil, ErrNotFound
+       }
+       return r, nil
+}
+
+// ListRatesForResource returns every rate ever defined for the given resource,
+// ordered chronologically by start_time.
+func (s *Service) ListRatesForResource(ctx context.Context, resourceID string) 
([]models.ComputeAllocationResourceRate, error) {
+       if resourceID == "" {
+               return nil, fmt.Errorf("%w: compute_allocation_resource_id is 
required", ErrInvalidInput)
+       }
+       rates, err := s.resourceRates.FindByResource(ctx, resourceID)
+       if err != nil {
+               return nil, fmt.Errorf("list rates for resource: %w", err)
+       }
+       return rates, nil
+}
+
+// GetEffectiveRateForResource returns the rate effective for the given 
resource
+// at the supplied instant. Returns ErrNotFound when no rate applies.
+func (s *Service) GetEffectiveRateForResource(ctx context.Context, resourceID 
string, at time.Time) (*models.ComputeAllocationResourceRate, error) {
+       if resourceID == "" {
+               return nil, fmt.Errorf("%w: compute_allocation_resource_id is 
required", ErrInvalidInput)
+       }
+       if at.IsZero() {
+               at = nowUTC()
+       }
+       r, err := s.resourceRates.FindEffective(ctx, resourceID, at)
+       if err != nil {
+               return nil, fmt.Errorf("get effective rate for resource: %w", 
err)
+       }
+       if r == nil {
+               return nil, ErrNotFound
+       }
+       return r, nil
+}
+
+// UpdateComputeAllocationResourceRate persists changes to an existing rate.
+// The referenced resource and ID are immutable from this call's perspective —
+// only rate, start_time, and end_time are updated.
+func (s *Service) UpdateComputeAllocationResourceRate(ctx context.Context, 
rate *models.ComputeAllocationResourceRate) error {
+       if rate == nil || rate.ID == "" {
+               return fmt.Errorf("%w: compute allocation resource rate id is 
required", ErrInvalidInput)
+       }
+       if rate.Rate < 0 {
+               return fmt.Errorf("%w: rate must be non-negative", 
ErrInvalidInput)
+       }
+       if !rate.StartTime.IsZero() && !rate.EndTime.IsZero() && 
!rate.StartTime.Before(rate.EndTime) {
+               return fmt.Errorf("%w: start_time must be before end_time", 
ErrInvalidInput)
+       }
+       if err := s.inTx(ctx, func(tx *sql.Tx) error {
+               return s.resourceRates.Update(ctx, tx, rate)
+       }); err != nil {
+               return fmt.Errorf("update compute allocation resource rate: 
%w", err)
+       }
+       return nil
+}
+
+// DeleteComputeAllocationResourceRate removes a rate by ID.
+func (s *Service) DeleteComputeAllocationResourceRate(ctx context.Context, id 
string) error {
+       if id == "" {
+               return fmt.Errorf("%w: compute allocation resource rate id is 
required", ErrInvalidInput)
+       }
+       if err := s.inTx(ctx, func(tx *sql.Tx) error {
+               return s.resourceRates.Delete(ctx, tx, id)
+       }); err != nil {
+               return fmt.Errorf("delete compute allocation resource rate: 
%w", err)
+       }
+       return nil
+}
diff --git a/pkg/service/service.go b/pkg/service/service.go
index e249fbe7e..119f12483 100644
--- a/pkg/service/service.go
+++ b/pkg/service/service.go
@@ -41,6 +41,7 @@ type Service struct {
        allocs           store.ComputeAllocationStore
        resources        store.ComputeAllocationResourceStore
        resourceMappings store.ComputeAllocationResourceMappingStore
+       resourceRates    store.ComputeAllocationResourceRateStore
 }
 
 // New constructs a Service backed by the supplied database handle.
@@ -55,6 +56,7 @@ func New(database *sqlx.DB) *Service {
                allocs:           store.NewComputeAllocationStore(database),
                resources:        
store.NewComputeAllocationResourceStore(database),
                resourceMappings: 
store.NewComputeAllocationResourceMappingStore(database),
+               resourceRates:    
store.NewComputeAllocationResourceRateStore(database),
        }
 }
 
@@ -70,6 +72,7 @@ func NewWithStores(
        allocs store.ComputeAllocationStore,
        resources store.ComputeAllocationResourceStore,
        resourceMappings store.ComputeAllocationResourceMappingStore,
+       resourceRates store.ComputeAllocationResourceRateStore,
 ) *Service {
        return &Service{
                db:               database,
@@ -80,6 +83,7 @@ func NewWithStores(
                allocs:           allocs,
                resources:        resources,
                resourceMappings: resourceMappings,
+               resourceRates:    resourceRates,
        }
 }
 

Reply via email to