Repository: redcardinal-io/metering
Pull request #24: Feat/plan crud
Author: login: manjunathsaivamshi
Created: 2025-05-14T16:40:24Z | Merged: 2025-05-15T02:29:08Z

plan crud completed, integration tests will be taken later

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced full support for managing plans, including creating, retrieving, updating, deleting, and listing plans via new API endpoints.
  - Added plan-related data models and integrated plan management into the user interface and API.
  - Enabled multi-tenant support for plan operations.
  - Implemented pagination for listing plans.
- **Database**
  - Added a new "plan" table with support for audit fields and multi-tenancy.
  - Included a migration for creating and rolling back the "plan" table.
- **Documentation**
  - Added example HTTP requests for creating, retrieving, updating, deleting, and listing plans.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/application/repositories/store.go b/application/repositories/store.go
index 6c818e9..e279ef6 100644
--- a/application/repositories/store.go
+++ b/application/repositories/store.go
@@ -22,3 +22,11 @@ type MeterStoreRepository interface {
 	DeleteMeterByIDorSlug(ctx context.Context, idOrSlug string) error
 	UpdateMeterByIDorSlug(ctx context.Context, idOrSlug string, arg models.UpdateMeterInput) (*models.Meter, error)
 }
+
+type PlanStoreRepository interface {
+	CreatePlan(ctx context.Context, arg models.CreatePlanInput) (*models.Plan, error)
+	GetPlanByID(ctx context.Context, id string) (*models.Plan, error)
+	ListPlans(ctx context.Context, pagination pagination.Pagination) (*pagination.PaginationView[models.Plan], error)
+	DeletePlanByID(ctx context.Context, id string) error
+	UpdatePlanByID(ctx context.Context, id string, arg models.UpdatePlanInput) (*models.Plan, error)
+}
diff --git a/application/services/plan.go b/application/services/plan.go
new file mode 100644
index 0000000..3f17664
--- /dev/null
+++ b/application/services/plan.go
@@ -0,0 +1,69 @@
+package services
+
+import (
+	"context"
+
+	"github.com/redcardinal-io/metering/application/repositories"
+	"github.com/redcardinal-io/metering/domain/models"
+	"github.com/redcardinal-io/metering/domain/pkg/pagination"
+)
+
+type PlanService struct {
+	store repositories.PlanStoreRepository
+}
+
+// NewPlanService creates a new PlanService with the provided plan store repositories.
+func NewPlanService(store repositories.PlanStoreRepository) *PlanService {
+	return &PlanService{
+		store: store,
+	}
+}
+
+func (s *PlanService) CreatePlan(ctx context.Context, arg models.CreatePlanInput) (*models.Plan, error) {
+	// Store the plan in the database
+	m, err := s.store.CreatePlan(ctx, arg)
+	if err != nil {
+		return nil, err
+	}
+
+	return m, nil
+}
+
+func (s *PlanService) GetPlanByID(ctx context.Context, ID string) (*models.Plan, error) {
+	// Call the repository to get the plan
+	m, err := s.store.GetPlanByID(ctx, ID)
+	if err != nil {
+		return nil, err
+	}
+	return m, nil
+}
+
+func (s *PlanService) DeletePlan(ctx context.Context, ID string) error {
+
+	err := s.store.DeletePlanByID(ctx, ID)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (s *PlanService) UpdatePlan(ctx context.Context, ID string, arg models.UpdatePlanInput) (*models.Plan, error) {
+	// Call the store repository to update the plan
+	m, err := s.store.UpdatePlanByID(ctx, ID, arg)
+	if err != nil {
+		return nil, err
+	}
+
+	return m, nil
+}
+
+func (s *PlanService) ListPlans(ctx context.Context, pagination pagination.Pagination) (*pagination.PaginationView[models.Plan], error) {
+	// Call the store repository to list the plans
+	m, err := s.store.ListPlans(ctx, pagination)
+	if err != nil {
+		return nil, err
+	}
+
+	return m, nil
+}
diff --git a/bruno/plans/create.bru b/bruno/plans/create.bru
new file mode 100644
index 0000000..5731f48
--- /dev/null
+++ b/bruno/plans/create.bru
@@ -0,0 +1,23 @@
+meta {
+  name: create
+  type: http
+  seq: 1
+}
+
+post {
+  url: {{base_url}}/v1/plans
+  body: json
+  auth: inherit
+}
+
+headers {
+  x-tenant-slug: {{tenant_slug}}
+}
+
+body:json {
+  {
+    "name": "RC_TENANT Free Plan",
+    "description": "Free Plan",
+    "created_by": "rc_tenant_admin_user"
+  }
+}
diff --git a/bruno/plans/delete.bru b/bruno/plans/delete.bru
new file mode 100644
index 0000000..fa667cb
--- /dev/null
+++ b/bruno/plans/delete.bru
@@ -0,0 +1,15 @@
+meta {
+  name: delete
+  type: http
+  seq: 5
+}
+
+delete {
+  url: {{base_url}}/v1/plans/027fd502-f98b-48ca-a6d5-2c6a133954c2
+  body: none
+  auth: inherit
+}
+
+headers {
+  x-tenant-slug: {{tenant_slug}}
+}
diff --git a/bruno/plans/folder.bru b/bruno/plans/folder.bru
new file mode 100644
index 0000000..d20a07b
--- /dev/null
+++ b/bruno/plans/folder.bru
@@ -0,0 +1,3 @@
+meta {
+  name: plans
+}
diff --git a/bruno/plans/get_by_id.bru b/bruno/plans/get_by_id.bru
new file mode 100644
index 0000000..10ce5b2
--- /dev/null
+++ b/bruno/plans/get_by_id.bru
@@ -0,0 +1,15 @@
+meta {
+  name: get_by_id
+  type: http
+  seq: 3
+}
+
+get {
+  url: {{base_url}}/v1/plans/027fd502-f98b-48ca-a6d5-2c6a133954c2
+  body: none
+  auth: inherit
+}
+
+headers {
+  x-tenant-slug: {{tenant_slug}}
+}
diff --git a/bruno/plans/list.bru b/bruno/plans/list.bru
new file mode 100644
index 0000000..f553f95
--- /dev/null
+++ b/bruno/plans/list.bru
@@ -0,0 +1,15 @@
+meta {
+  name: list
+  type: http
+  seq: 2
+}
+
+get {
+  url: {{base_url}}/v1/plans
+  body: none
+  auth: inherit
+}
+
+headers {
+  x-tenant-slug: {{tenant_slug}}
+}
diff --git a/bruno/plans/update.bru b/bruno/plans/update.bru
new file mode 100644
index 0000000..c799e1d
--- /dev/null
+++ b/bruno/plans/update.bru
@@ -0,0 +1,23 @@
+meta {
+  name: update
+  type: http
+  seq: 4
+}
+
+put {
+  url: {{base_url}}/v1/plans/027fd502-f98b-48ca-a6d5-2c6a133954c2
+  body: json
+  auth: inherit
+}
+
+headers {
+  x-tenant-slug: {{tenant_slug}}
+}
+
+body:json {
+  {
+    "name": "updated name",
+    "description":"updated description",
+    "updated_by":"test user"
+  }
+}
diff --git a/domain/models/plan.go b/domain/models/plan.go
new file mode 100644
index 0000000..9a50762
--- /dev/null
+++ b/domain/models/plan.go
@@ -0,0 +1,22 @@
+package models
+
+// Plan represents a plan entity from the database
+type Plan struct {
+	Base
+	Name        string `json:"name"`
+	Description string `json:"description,omitempty"`
+	TenantSlug  string `json:"tenant_slug"`
+}
+
+// CreatePlanInput represents the input for creating a new plan
+type CreatePlanInput struct {
+	Name        string
+	Description string
+	CreatedBy   string
+}
+
+type UpdatePlanInput struct {
+	Name        string
+	Description string
+	UpdatedBy   string
+}
diff --git a/infrastructure/postgres/gen/models.go b/infrastructure/postgres/gen/models.go
index 80e22df..29a613b 100644
--- a/infrastructure/postgres/gen/models.go
+++ b/infrastructure/postgres/gen/models.go
@@ -72,3 +72,14 @@ type Meter struct {
 	CreatedBy     string
 	UpdatedBy     string
 }
+
+type Plan struct {
+	ID          pgtype.UUID
+	Name        string
+	Description pgtype.Text
+	TenantSlug  string
+	CreatedAt   pgtype.Timestamptz
+	UpdatedAt   pgtype.Timestamptz
+	CreatedBy   string
+	UpdatedBy   string
+}
diff --git a/infrastructure/postgres/gen/plan.sql.go b/infrastructure/postgres/gen/plan.sql.go
new file mode 100644
index 0000000..2886059
--- /dev/null
+++ b/infrastructure/postgres/gen/plan.sql.go
@@ -0,0 +1,192 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.28.0
+// source: plan.sql
+
+package gen
+
+import (
+	"context"
+
+	"github.com/jackc/pgx/v5/pgtype"
+)
+
+const countPlans = `-- name: CountPlans :one
+SELECT count(*) FROM plan
+WHERE tenant_slug = $1
+`
+
+func (q *Queries) CountPlans(ctx context.Context, tenantSlug string) (int64, error) {
+	row := q.db.QueryRow(ctx, countPlans, tenantSlug)
+	var count int64
+	err := row.Scan(&count)
+	return count, err
+}
+
+const createPlan = `-- name: CreatePlan :one
+INSERT INTO plan (
+    name,
+    description,
+    tenant_slug,
+    created_by,
+    updated_by
+) VALUES (
+    $1, $2, $3, $4, $5
+) RETURNING id, name, description, tenant_slug, created_at, updated_at, created_by, updated_by
+`
+
+type CreatePlanParams struct {
+	Name        string
+	Description pgtype.Text
+	TenantSlug  string
+	CreatedBy   string
+	UpdatedBy   string
+}
+
+func (q *Queries) CreatePlan(ctx context.Context, arg CreatePlanParams) (Plan, error) {
+	row := q.db.QueryRow(ctx, createPlan,
+		arg.Name,
+		arg.Description,
+		arg.TenantSlug,
+		arg.CreatedBy,
+		arg.UpdatedBy,
+	)
+	var i Plan
+	err := row.Scan(
+		&i.ID,
+		&i.Name,
+		&i.Description,
+		&i.TenantSlug,
+		&i.CreatedAt,
+		&i.UpdatedAt,
+		&i.CreatedBy,
+		&i.UpdatedBy,
+	)
+	return i, err
+}
+
+const deletePlanByID = `-- name: DeletePlanByID :exec
+DELETE FROM plan
+WHERE id = $1
+AND tenant_slug = $2
+`
+
+type DeletePlanByIDParams struct {
+	ID         pgtype.UUID
+	TenantSlug string
+}
+
+func (q *Queries) DeletePlanByID(ctx context.Context, arg DeletePlanByIDParams) error {
+	_, err := q.db.Exec(ctx, deletePlanByID, arg.ID, arg.TenantSlug)
+	return err
+}
+
+const getPlanByID = `-- name: GetPlanByID :one
+SELECT id, name, description, tenant_slug, created_at, updated_at, created_by, updated_by FROM plan
+WHERE id = $1
+AND tenant_slug = $2
+`
+
+type GetPlanByIDParams struct {
+	ID         pgtype.UUID
+	TenantSlug string
+}
+
+func (q *Queries) GetPlanByID(ctx context.Context, arg GetPlanByIDParams) (Plan, error) {
+	row := q.db.QueryRow(ctx, getPlanByID, arg.ID, arg.TenantSlug)
+	var i Plan
+	err := row.Scan(
+		&i.ID,
+		&i.Name,
+		&i.Description,
+		&i.TenantSlug,
+		&i.CreatedAt,
+		&i.UpdatedAt,
+		&i.CreatedBy,
+		&i.UpdatedBy,
+	)
+	return i, err
+}
+
+const listPlansPaginated = `-- name: ListPlansPaginated :many
+SELECT id, name, description, tenant_slug, created_at, updated_at, created_by, updated_by FROM plan
+WHERE tenant_slug = $1
+ORDER BY created_at DESC
+LIMIT $2
+OFFSET $3
+`
+
+type ListPlansPaginatedParams struct {
+	TenantSlug string
+	Limit      int32
+	Offset     int32
+}
+
+func (q *Queries) ListPlansPaginated(ctx context.Context, arg ListPlansPaginatedParams) ([]Plan, error) {
+	rows, err := q.db.Query(ctx, listPlansPaginated, arg.TenantSlug, arg.Limit, arg.Offset)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	var items []Plan
+	for rows.Next() {
+		var i Plan
+		if err := rows.Scan(
+			&i.ID,
+			&i.Name,
+			&i.Description,
+			&i.TenantSlug,
+			&i.CreatedAt,
+			&i.UpdatedAt,
+			&i.CreatedBy,
+			&i.UpdatedBy,
+		); err != nil {
+			return nil, err
+		}
+		items = append(items, i)
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+	return items, nil
+}
+
+const updatePlanByID = `-- name: UpdatePlanByID :one
+UPDATE plan
+SET name = coalesce($5, name),
+    description = coalesce($1, description),
+    updated_by = $4
+WHERE id = $2
+AND tenant_slug = $3
+RETURNING id, name, description, tenant_slug, created_at, updated_at, created_by, updated_by
+`
+
+type UpdatePlanByIDParams struct {
+	Description pgtype.Text
+	ID          pgtype.UUID
+	TenantSlug  string
+	UpdatedBy   string
+	Name        pgtype.Text
+}
+
+func (q *Queries) UpdatePlanByID(ctx context.Context, arg UpdatePlanByIDParams) (Plan, error) {
+	row := q.db.QueryRow(ctx, updatePlanByID,
+		arg.Description,
+		arg.ID,
+		arg.TenantSlug,
+		arg.UpdatedBy,
+		arg.Name,
+	)
+	var i Plan
+	err := row.Scan(
+		&i.ID,
+		&i.Name,
+		&i.Description,
+		&i.TenantSlug,
+		&i.CreatedAt,
+		&i.UpdatedAt,
+		&i.CreatedBy,
+		&i.UpdatedBy,
+	)
+	return i, err
+}
diff --git a/infrastructure/postgres/gen/querier.go b/infrastructure/postgres/gen/querier.go
index fcb743e..e3da235 100644
--- a/infrastructure/postgres/gen/querier.go
+++ b/infrastructure/postgres/gen/querier.go
@@ -13,17 +13,23 @@ import (
 type Querier interface {
 	CountMeters(ctx context.Context, tenantSlug string) (int64, error)
 	CountMetersByEventType(ctx context.Context, arg CountMetersByEventTypeParams) (int64, error)
+	CountPlans(ctx context.Context, tenantSlug string) (int64, error)
 	CreateMeter(ctx context.Context, arg CreateMeterParams) (Meter, error)
+	CreatePlan(ctx context.Context, arg CreatePlanParams) (Plan, error)
 	DeleteMeterByID(ctx context.Context, arg DeleteMeterByIDParams) error
 	DeleteMeterBySlug(ctx context.Context, arg DeleteMeterBySlugParams) error
+	DeletePlanByID(ctx context.Context, arg DeletePlanByIDParams) error
 	GetMeterByID(ctx context.Context, arg GetMeterByIDParams) (Meter, error)
 	GetMeterBySlug(ctx context.Context, arg GetMeterBySlugParams) (Meter, error)
+	GetPlanByID(ctx context.Context, arg GetPlanByIDParams) (Plan, error)
 	GetPropertiesByEventType(ctx context.Context, arg GetPropertiesByEventTypeParams) ([]interface{}, error)
 	GetValuePropertiesByEventType(ctx context.Context, arg GetValuePropertiesByEventTypeParams) ([]pgtype.Text, error)
 	ListMetersByEventTypes(ctx context.Context, arg ListMetersByEventTypesParams) ([]Meter, error)
 	ListMetersPaginated(ctx context.Context, arg ListMetersPaginatedParams) ([]Meter, error)
+	ListPlansPaginated(ctx context.Context, arg ListPlansPaginatedParams) ([]Plan, error)
 	UpdateMeterByID(ctx context.Context, arg UpdateMeterByIDParams) (Meter, error)
 	UpdateMeterBySlug(ctx context.Context, arg UpdateMeterBySlugParams) (Meter, error)
+	UpdatePlanByID(ctx context.Context, arg UpdatePlanByIDParams) (Plan, error)
 }
 
 var _ Querier = (*Queries)(nil)
diff --git a/infrastructure/postgres/sql/queries/plan.sql b/infrastructure/postgres/sql/queries/plan.sql
new file mode 100644
index 0000000..f568911
--- /dev/null
+++ b/infrastructure/postgres/sql/queries/plan.sql
@@ -0,0 +1,42 @@
+-- name: CreatePlan :one
+INSERT INTO plan (
+    name,
+    description,
+    tenant_slug,
+    created_by,
+    updated_by
+) VALUES (
+    $1, $2, $3, $4, $5
+) RETURNING *;
+
+-- name: GetPlanByID :one
+SELECT * FROM plan
+WHERE id = $1
+AND tenant_slug = $2;
+
+-- name: ListPlansPaginated :many
+SELECT * FROM plan
+WHERE tenant_slug = $1
+ORDER BY created_at DESC
+LIMIT $2
+OFFSET $3;
+
+-- name: DeletePlanByID :exec
+DELETE FROM plan
+WHERE id = $1
+AND tenant_slug = $2;
+
+-- name: CountPlans :one
+SELECT count(*) FROM plan
+WHERE tenant_slug = $1;
+
+-- name: UpdatePlanByID :one
+UPDATE plan
+SET name = coalesce(sqlc.narg('name'), name),
+    description = coalesce($1, description),
+    updated_by = $4
+WHERE id = $2
+AND tenant_slug = $3
+RETURNING *;
+
+
diff --git a/infrastructure/postgres/sql/schema.sql b/infrastructure/postgres/sql/schema.sql
index 545d09a..8ac89ba 100644
--- a/infrastructure/postgres/sql/schema.sql
+++ b/infrastructure/postgres/sql/schema.sql
@@ -24,3 +24,14 @@ create table "meter" (
 
   unique (tenant_slug, slug)
 );
+
+create table if not exists plan (
+	id uuid primary key default uuid_generate_v4(),
+	name varchar not null,
+	description text,
+	tenant_slug varchar not null,
+	created_at timestamp with time zone not null default current_timestamp,
+	updated_at timestamp with time zone not null default current_timestamp,
+	created_by varchar not null,
+	updated_by varchar not null
+);
diff --git a/infrastructure/postgres/store/plans/create.go b/infrastructure/postgres/store/plans/create.go
new file mode 100644
index 0000000..ecc3e6f
--- /dev/null
+++ b/infrastructure/postgres/store/plans/create.go
@@ -0,0 +1,50 @@
+package plans
+
+import (
+	"context"
+
+	"github.com/google/uuid"
+	"github.com/jackc/pgx/v5/pgtype"
+	"github.com/redcardinal-io/metering/domain/models"
+	"github.com/redcardinal-io/metering/domain/pkg/constants"
+	"github.com/redcardinal-io/metering/infrastructure/postgres"
+	"github.com/redcardinal-io/metering/infrastructure/postgres/gen"
+	"go.uber.org/zap"
+)
+
+func (p *PgPlanStoreRepository) CreatePlan(ctx context.Context, arg models.CreatePlanInput) (*models.Plan, error) {
+	tenantSlug := ctx.Value(constants.TenantSlugKey).(string)
+	m, err := p.q.CreatePlan(ctx, gen.CreatePlanParams{
+		Name:        arg.Name,
+		Description: pgtype.Text{String: arg.Description, Valid: arg.Description != ""},
+		TenantSlug:  tenantSlug,
+		CreatedBy:   arg.CreatedBy,
+		UpdatedBy:   arg.CreatedBy,
+	})
+
+	if err != nil {
+		p.logger.Error("failed to create plan", zap.Error(err))
+		return nil, postgres.MapError(err, "Postgres.CreatePlan")
+	}
+
+	id, err := uuid.FromBytes(m.ID.Bytes[:])
+	if err != nil {
+		p.logger.Error("failed to parse UUID from bytes", zap.Error(err))
+		return nil, postgres.MapError(err, "Postgres.ParseUUID")
+	}
+
+	plan := &models.Plan{
+		Name:        m.Name,
+		Description: m.Description.String,
+		TenantSlug:  m.TenantSlug,
+		Base: models.Base{
+			ID:        id,
+			CreatedAt: m.CreatedAt,
+			CreatedBy: m.CreatedBy,
+			UpdatedBy: m.UpdatedBy,
+			UpdatedAt: m.UpdatedAt,
+		},
+	}
+
+	return plan, nil
+}
diff --git a/infrastructure/postgres/store/plans/delete.go b/infrastructure/postgres/store/plans/delete.go
new file mode 100644
index 0000000..ff142e6
--- /dev/null
+++ b/infrastructure/postgres/store/plans/delete.go
@@ -0,0 +1,33 @@
+package plans
+
+import (
+	"context"
+
+	"github.com/google/uuid"
+	"github.com/jackc/pgx/v5/pgtype"
+	"github.com/redcardinal-io/metering/domain/pkg/constants"
+	"github.com/redcardinal-io/metering/infrastructure/postgres"
+	"github.com/redcardinal-io/metering/infrastructure/postgres/gen"
+	"go.uber.org/zap"
+)
+
+func (p *PgPlanStoreRepository) DeletePlanByID(ctx context.Context, id string) error {
+	// Try to parse as UUID first
+	tenantSlug := ctx.Value(constants.TenantSlugKey).(string)
+	parsedId, err := uuid.Parse(id)
+	var deleteErr error
+	if err == nil {
+		// Valid UUID, delete by ID
+		deleteErr = p.q.DeletePlanByID(ctx, gen.DeletePlanByIDParams{
+			ID:         pgtype.UUID{Bytes: parsedId, Valid: true},
+			TenantSlug: tenantSlug,
+		})
+	}
+	// Handle errors from either delete operation
+	if deleteErr != nil {
+		p.logger.Error("failed to delete plan", zap.Error(deleteErr))
+		return postgres.MapError(deleteErr, "Postgres.DeletePlan")
+	}
+
+	return nil
+}
diff --git a/infrastructure/postgres/store/plans/details.go b/infrastructure/postgres/store/plans/details.go
new file mode 100644
index 0000000..9296fce
--- /dev/null
+++ b/infrastructure/postgres/store/plans/details.go
@@ -0,0 +1,50 @@
+package plans
+
+import (
+	"context"
+
+	"github.com/google/uuid"
+	"github.com/jackc/pgx/v5/pgtype"
+	"github.com/redcardinal-io/metering/domain/models"
+	"github.com/redcardinal-io/metering/domain/pkg/constants"
+	"github.com/redcardinal-io/metering/infrastructure/postgres"
+	"github.com/redcardinal-io/metering/infrastructure/postgres/gen"
+)
+
+func (p *PgPlanStoreRepository) GetPlanByID(ctx context.Context, id string) (*models.Plan, error) {
+	tenantSlug := ctx.Value(constants.TenantSlugKey).(string)
+	// Try to parse as UUID first
+	parsedId, err := uuid.Parse(id)
+	var detailsErr error
+	var m gen.Plan
+	if err == nil {
+		// Valid UUID, get details by ID
+		m, detailsErr = p.q.GetPlanByID(ctx, gen.GetPlanByIDParams{
+			ID:         pgtype.UUID{Bytes: parsedId, Valid: true},
+			TenantSlug: tenantSlug,
+		})
+	}
+
+	// Handle errors from either get operation
+	if detailsErr != nil {
+		return nil, postgres.MapError(detailsErr, "Postgres.GetPlanByID")
+	}
+
+	uuid, err := uuid.FromBytes(m.ID.Bytes[:])
+	if err != nil {
+		return nil, postgres.MapError(err, "Postgres.ParseUUID")
+	}
+
+	return &models.Plan{
+		Name:        m.Name,
+		Description: m.Description.String,
+		TenantSlug:  m.TenantSlug,
+		Base: models.Base{
+			ID:        uuid,
+			CreatedAt: m.CreatedAt,
+			CreatedBy: m.CreatedBy,
+			UpdatedBy: m.UpdatedBy,
+			UpdatedAt: m.UpdatedAt,
+		},
+	}, nil
+}
diff --git a/infrastructure/postgres/store/plans/list.go b/infrastructure/postgres/store/plans/list.go
new file mode 100644
index 0000000..71f2ffd
--- /dev/null
+++ b/infrastructure/postgres/store/plans/list.go
@@ -0,0 +1,58 @@
+package plans
+
+import (
+	"context"
+
+	"github.com/google/uuid"
+	"github.com/redcardinal-io/metering/domain/models"
+	"github.com/redcardinal-io/metering/domain/pkg/constants"
+	"github.com/redcardinal-io/metering/domain/pkg/pagination"
+	"github.com/redcardinal-io/metering/infrastructure/postgres"
+	"github.com/redcardinal-io/metering/infrastructure/postgres/gen"
+	"go.uber.org/zap"
+)
+
+func (p *PgPlanStoreRepository) ListPlans(ctx context.Context, page pagination.Pagination) (*pagination.PaginationView[models.Plan], error) {
+	tenantSlug := ctx.Value(constants.TenantSlugKey).(string)
+	m, err := p.q.ListPlansPaginated(ctx, gen.ListPlansPaginatedParams{
+		Limit:      int32(page.Limit),
+		Offset:     int32(page.GetOffset()),
+		TenantSlug: ctx.Value(constants.TenantSlugKey).(string),
+	})
+
+	if err != nil {
+		p.logger.Error("Error listing plans: ", zap.Error(err))
+		return nil, postgres.MapError(err, "Postgres.ListPlans")
+	}
+
+	plans := make([]models.Plan, 0, len(m))
+	for _, plan := range m {
+		id, err := uuid.FromBytes(plan.ID.Bytes[:])
+		if err != nil {
+			p.logger.Error("failed to parse UUID from bytes", zap.Error(err))
+			return nil, postgres.MapError(err, "Postgres.ParseUUID")
+		}
+		plans = append(plans, models.Plan{
+			Name:        plan.Name,
+			Description: plan.Description.String,
+			TenantSlug:  plan.TenantSlug,
+			Base: models.Base{
+				ID:        id,
+				CreatedAt: plan.CreatedAt,
+				CreatedBy: plan.CreatedBy,
+				UpdatedBy: plan.UpdatedBy,
+				UpdatedAt: plan.UpdatedAt,
+			},
+		})
+	}
+
+	count, err := p.q.CountPlans(ctx, tenantSlug)
+	if err != nil {
+		p.logger.Error("Error counting plans: ", zap.Error(err))
+		return nil, postgres.MapError(err, "Postgres.CountMeters")
+	}
+
+	result := pagination.FormatWith(page, int(count), plans)
+
+	return &result, nil
+}
diff --git a/infrastructure/postgres/store/plans/plans.go b/infrastructure/postgres/store/plans/plans.go
new file mode 100644
index 0000000..1c2f2ed
--- /dev/null
+++ b/infrastructure/postgres/store/plans/plans.go
@@ -0,0 +1,20 @@
+package plans
+
+import (
+	"github.com/jackc/pgx/v5/pgxpool"
+	"github.com/redcardinal-io/metering/application/repositories"
+	"github.com/redcardinal-io/metering/domain/pkg/logger"
+	"github.com/redcardinal-io/metering/infrastructure/postgres/gen"
+)
+
+type PgPlanStoreRepository struct {
+	q      *gen.Queries
+	logger *logger.Logger
+}
+
+func NewPostgresPlanStoreRepository(db any, logger *logger.Logger) repositories.PlanStoreRepository {
+	return &PgPlanStoreRepository{
+		q:      gen.New(db.(*pgxpool.Pool)),
+		logger: logger,
+	}
+}
diff --git a/infrastructure/postgres/store/plans/update.go b/infrastructure/postgres/store/plans/update.go
new file mode 100644
index 0000000..1ac0cdf
--- /dev/null
+++ b/infrastructure/postgres/store/plans/update.go
@@ -0,0 +1,52 @@
+package plans
+
+import (
+	"context"
+
+	"github.com/google/uuid"
+	"github.com/jackc/pgx/v5/pgtype"
+	"github.com/redcardinal-io/metering/domain/models"
+	"github.com/redcardinal-io/metering/domain/pkg/constants"
+	"github.com/redcardinal-io/metering/infrastructure/postgres"
+	"github.com/redcardinal-io/metering/infrastructure/postgres/gen"
+)
+
+func (s *PgPlanStoreRepository) UpdatePlanByID(ctx context.Context, id string, arg models.UpdatePlanInput) (*models.Plan, error) {
+	tenantSlug := ctx.Value(constants.TenantSlugKey).(string)
+	// Try to parse as UUID first
+	parsedId, err := uuid.Parse(id)
+	var updateErr error
+	var m gen.Plan
+	if err == nil {
+		m, updateErr = s.q.UpdatePlanByID(ctx, gen.UpdatePlanByIDParams{
+			Name:        pgtype.Text{String: arg.Name, Valid: arg.Name != ""},
+			Description: pgtype.Text{String: arg.Description, Valid: arg.Description != ""},
+			TenantSlug:  tenantSlug,
+			ID:          pgtype.UUID{Bytes: parsedId, Valid: true},
+			UpdatedBy:   arg.UpdatedBy,
+		})
+	}
+
+	if updateErr != nil {
+		return nil, postgres.MapError(updateErr, "Postgres.UpdatePlanByID")
+	}
+
+	uuid, err := uuid.FromBytes(m.ID.Bytes[:])
+	if err != nil {
+		return nil, postgres.MapError(err, "Postgres.ParseUUID")
+	}
+
+	// Valid UUID, delete by ID
+	return &models.Plan{
+		Name:        m.Name,
+		Description: m.Description.String,
+		TenantSlug:  m.TenantSlug,
+		Base: models.Base{
+			ID:        uuid,
+			CreatedAt: m.CreatedAt,
+			UpdatedBy: m.UpdatedBy,
+			UpdatedAt: m.UpdatedAt,
+			CreatedBy: m.CreatedBy,
+		},
+	}, nil
+}
diff --git a/interfaces/http/routes/v1/plans/create.go b/interfaces/http/routes/v1/plans/create.go
new file mode 100644
index 0000000..7c088da
--- /dev/null
+++ b/interfaces/http/routes/v1/plans/create.go
@@ -0,0 +1,50 @@
+package plans
+
+import (
+	"context"
+
+	"github.com/gofiber/fiber/v2"
+	domainerrors "github.com/redcardinal-io/metering/domain/errors"
+	"github.com/redcardinal-io/metering/domain/models"
+	"github.com/redcardinal-io/metering/domain/pkg/constants"
+	"go.uber.org/zap"
+)
+
+type createPlanRequest struct {
+	Name        string `json:"name" validate:"required"`
+	Description string `json:"description,omitempty"`
+	CreatedBy   string `json:"created_by" validate:"required"`
+}
+
+func (h *httpHandler) create(ctx *fiber.Ctx) error {
+	tenant_slug := ctx.Get(constants.TenantHeader)
+	var req createPlanRequest
+	if err := ctx.BodyParser(&req); err != nil {
+		errResp := domainerrors.NewErrorResponseWithOpts(err, domainerrors.EINVALID, "failed to parse request body")
+		h.logger.Error("failed to parse request body", zap.Reflect("error", errResp))
+		return ctx.Status(errResp.Status).JSON(errResp.ToJson())
+	}
+
+	// validate the request body
+	if err := h.validator.Struct(req); err != nil {
+		errResp := domainerrors.NewErrorResponseWithOpts(err, domainerrors.EINVALID, "invalid request body")
+		h.logger.Error("invalid request body", zap.Reflect("error", errResp))
+		return ctx.Status(errResp.Status).JSON(errResp.ToJson())
+	}
+
+	c := context.WithValue(ctx.UserContext(), constants.TenantSlugKey, tenant_slug)
+
+	plan, err := h.planSvc.CreatePlan(c, models.CreatePlanInput{
+		Name:        req.Name,
+		Description: req.Description,
+		CreatedBy:   req.CreatedBy,
+	})
+	if err != nil {
+		h.logger.Error("failed to create plan", zap.Reflect("error", err))
+		errResp := domainerrors.NewErrorResponse(err)
+		return ctx.Status(errResp.Status).JSON(errResp.ToJson())
+	}
+
+	return ctx.
+		Status(fiber.StatusCreated).JSON(models.NewHttpResponse(plan, "plan created successfully", fiber.StatusCreated))
+}
diff --git a/interfaces/http/routes/v1/plans/delete.go b/interfaces/http/routes/v1/plans/delete.go
new file mode 100644
index 0000000..673dcb1
--- /dev/null
+++ b/interfaces/http/routes/v1/plans/delete.go
@@ -0,0 +1,33 @@
+package plans
+
+import (
+	"context"
+
+	"github.com/gofiber/fiber/v2"
+	domainerrors "github.com/redcardinal-io/metering/domain/errors"
+	"github.com/redcardinal-io/metering/domain/pkg/constants"
+	"go.uber.org/zap"
+)
+
+func (h *httpHandler) deleteByID(ctx *fiber.Ctx) error {
+	tenantSlug := ctx.Get(constants.TenantHeader)
+	id := ctx.Params("id")
+
+	if id == "" {
+		errResp := domainerrors.NewErrorResponseWithOpts(nil, domainerrors.EINVALID, "plan ID is required")
+		h.logger.Error("plan ID is required", zap.Reflect("error", errResp))
+		return ctx.Status(errResp.Status).JSON(errResp.ToJson())
+	}
+
+	c := context.WithValue(ctx.UserContext(), constants.TenantSlugKey, tenantSlug)
+
+	err := h.planSvc.DeletePlan(c, id)
+	if err != nil {
+		h.logger.Error("failed to delete plan", zap.String("id", id), zap.Reflect("error", err))
+		errResp := domainerrors.NewErrorResponse(err)
+		return ctx.Status(errResp.Status).JSON(errResp.ToJson())
+	}
+
+	return ctx.
+		SendStatus(fiber.StatusNoContent)
+}
diff --git a/interfaces/http/routes/v1/plans/details.go b/interfaces/http/routes/v1/plans/details.go
new file mode 100644
index 0000000..6271e29
--- /dev/null
+++ b/interfaces/http/routes/v1/plans/details.go
@@ -0,0 +1,34 @@
+package plans
+
+import (
+	"context"
+
+	"github.com/gofiber/fiber/v2"
+	domainerrors "github.com/redcardinal-io/metering/domain/errors"
+	"github.com/redcardinal-io/metering/domain/models"
+	"github.com/redcardinal-io/metering/domain/pkg/constants"
+	"go.uber.org/zap"
+)
+
+func (h *httpHandler) getByID(ctx *fiber.Ctx) error {
+	tenantSlug := ctx.Get(constants.TenantHeader)
+	id := ctx.Params("id")
+
+	if id == "" {
+		errResp := domainerrors.NewErrorResponseWithOpts(nil, domainerrors.EINVALID, "plan ID is required")
+		h.logger.Error("plan ID is required", zap.Reflect("error", errResp))
+		return ctx.Status(errResp.Status).JSON(errResp.ToJson())
+	}
+
+	c := context.WithValue(ctx.UserContext(), constants.TenantSlugKey, tenantSlug)
+
+	plan, err := h.planSvc.GetPlanByID(c, id)
+	if err != nil {
+		h.logger.Error("failed to get plan", zap.String("id", id), zap.Reflect("error", err))
+		errResp := domainerrors.NewErrorResponse(err)
+		return ctx.Status(errResp.Status).JSON(errResp.ToJson())
+	}
+
+	return ctx.
+		Status(fiber.StatusOK).JSON(models.NewHttpResponse(plan, "plan retrieved successfully", fiber.StatusOK))
+}
diff --git a/interfaces/http/routes/v1/plans/list.go b/interfaces/http/routes/v1/plans/list.go
new file mode 100644
index 0000000..4d53bce
--- /dev/null
+++ b/interfaces/http/routes/v1/plans/list.go
@@ -0,0 +1,31 @@
+package plans
+
+import (
+	"context"
+
+	"github.com/gofiber/fiber/v2"
+	domainerrors "github.com/redcardinal-io/metering/domain/errors"
+	"github.com/redcardinal-io/metering/domain/models"
+	"github.com/redcardinal-io/metering/domain/pkg/constants"
+	"github.com/redcardinal-io/metering/domain/pkg/pagination"
+	"go.uber.org/zap"
+)
+
+func (h *httpHandler) list(ctx *fiber.Ctx) error {
+	tenantSlug := ctx.Get(constants.TenantHeader)
+
+	// Create pagination input
+	paginationInput := pagination.ExtractPaginationFromContext(ctx)
+	c := context.WithValue(ctx.UserContext(), constants.TenantSlugKey, tenantSlug)
+
+	// Call service to list plans
+	plans, err := h.planSvc.ListPlans(c, paginationInput)
+	if err != nil {
+		h.logger.Error("failed to list plans", zap.Reflect("error", err))
+		errResp := domainerrors.NewErrorResponse(err)
+		return ctx.Status(errResp.Status).JSON(errResp.ToJson())
+	}
+
+	return ctx.
+		Status(fiber.StatusOK).JSON(models.NewHttpResponse(plans, "plans retrieved successfully", fiber.StatusOK))
+}
diff --git a/interfaces/http/routes/v1/plans/routes.go b/interfaces/http/routes/v1/plans/routes.go
new file mode 100644
index 0000000..6b0d7be
--- /dev/null
+++ b/interfaces/http/routes/v1/plans/routes.go
@@ -0,0 +1,38 @@
+package plans
+
+import (
+	"github.com/go-playground/validator/v10"
+	"github.com/gofiber/fiber/v2"
+	"github.com/redcardinal-io/metering/application/services"
+	"github.com/redcardinal-io/metering/domain/pkg/logger"
+)
+
+type httpHandler struct {
+	logger    *logger.Logger
+	planSvc   *services.PlanService
+	validator *validator.Validate
+}
+
+// NewHTTPHandler creates and returns a new httpHandler for plan-related HTTP endpoints.
+func NewHTTPHandler(logger *logger.Logger, planSvc *services.PlanService) *httpHandler {
+	validator := validator.New()
+	return &httpHandler{
+		logger:    logger,
+		planSvc:   planSvc,
+		validator: validator,
+	}
+}
+
+func (h *httpHandler) RegisterRoutes(r fiber.Router) {
+	// Group all plans routes
+	plans := r.Group("/plans")
+
+	// Plan collection routes
+	plans.Post("/", h.create)
+	plans.Get("/", h.list)
+
+	// Single Plan routes with id parameter
+	plans.Get("/:id", h.getByID)
+	plans.Put("/:id", h.updateByID)
+	plans.Delete("/:id", h.deleteByID)
+}
diff --git a/interfaces/http/routes/v1/plans/update.go b/interfaces/http/routes/v1/plans/update.go
new file mode 100644
index 0000000..e6f2f6d
--- /dev/null
+++ b/interfaces/http/routes/v1/plans/update.go
@@ -0,0 +1,63 @@
+package plans
+
+import (
+	"context"
+
+	"github.com/gofiber/fiber/v2"
+	domainerrors "github.com/redcardinal-io/metering/domain/errors"
+	"github.com/redcardinal-io/metering/domain/models"
+	"github.com/redcardinal-io/metering/domain/pkg/constants"
+	"go.uber.org/zap"
+)
+
+type updatePlanRequest struct {
+	Name        string `json:"name,omitempty" validate:"omitempty,min=3,max=100"`
+	Description string `json:"description,omitempty" validate:"omitempty,min=3,max=255"`
+	UpdatedBy   string `json:"updated_by" validate:"required,min=3,max=255"`
+}
+
+func (h *httpHandler) updateByID(ctx *fiber.Ctx) error {
+	tenantSlug := ctx.Get(constants.TenantHeader)
+	id := ctx.Params("id")
+
+	if id == "" {
+		errResp := domainerrors.NewErrorResponseWithOpts(nil, domainerrors.EINVALID, "plan ID is required")
+		h.logger.Error("plan ID is required", zap.Reflect("error", errResp))
+		return ctx.Status(errResp.Status).JSON(errResp.ToJson())
+	}
+
+	var req updatePlanRequest
+	if err := ctx.BodyParser(&req); err != nil {
+		errResp := domainerrors.NewErrorResponseWithOpts(err, domainerrors.EINVALID, "failed to parse request body")
+		h.logger.Error("failed to parse request body", zap.Reflect("error", errResp))
+		return ctx.Status(errResp.Status).JSON(errResp.ToJson())
+	}
+
+	// validate the request body
+	if err := h.validator.Struct(req); err != nil {
+		errResp := domainerrors.NewErrorResponseWithOpts(err, domainerrors.EINVALID, "invalid request body")
+		h.logger.Error("invalid request body", zap.Reflect("error", errResp))
+		return ctx.Status(errResp.Status).JSON(errResp.ToJson())
+	}
+	if req.Name == "" && req.Description == "" {
+		errResp := domainerrors.NewErrorResponseWithOpts(nil, domainerrors.EINVALID, "at least one field (name or description) is required")
+		h.logger.Error("at least one field (name or description) is required", zap.Reflect("error", errResp))
+		return ctx.Status(errResp.Status).JSON(errResp.ToJson())
+	}
+
+	c := context.WithValue(ctx.UserContext(), constants.TenantSlugKey, tenantSlug)
+
+	plan, err := h.planSvc.UpdatePlan(c, id, models.UpdatePlanInput{
+		Name:        req.Name,
+		Description: req.Description,
+		UpdatedBy:   req.UpdatedBy,
+	})
+	if err != nil {
+		h.logger.Error("failed to update plan", zap.String("id", id), zap.Reflect("error", err))
+		errResp := domainerrors.NewErrorResponse(err)
+		return ctx.Status(errResp.Status).JSON(errResp.ToJson())
+	}
+
+	return ctx.
+		Status(fiber.StatusOK).JSON(models.NewHttpResponse(plan, "plan updated successfully", fiber.StatusOK))
+}
diff --git a/interfaces/http/server.go b/interfaces/http/server.go
index 979003e..86bbafe 100644
--- a/interfaces/http/server.go
+++ b/interfaces/http/server.go
@@ -13,10 +13,12 @@ import (
 	"github.com/redcardinal-io/metering/infrastructure/kafka"
 	"github.com/redcardinal-io/metering/infrastructure/postgres/store"
 	"github.com/redcardinal-io/metering/infrastructure/postgres/store/meters"
+	"github.com/redcardinal-io/metering/infrastructure/postgres/store/plans"
 	"github.com/redcardinal-io/metering/interfaces/http/routes"
 	"github.com/redcardinal-io/metering/interfaces/http/routes/middleware"
 	"github.com/redcardinal-io/metering/interfaces/http/routes/v1/events"
 	meterRoutes "github.com/redcardinal-io/metering/interfaces/http/routes/v1/meters"
+	planRoutes "github.com/redcardinal-io/metering/interfaces/http/routes/v1/plans"
 	"go.uber.org/zap"
 )
 
@@ -72,10 +74,12 @@ func ServeHttp() error {
 	}
 	defer store.Close()
 	meterStore := meters.NewPostgresMeterStoreRepository(store.GetDB(), logger)
+	planStore := plans.NewPostgresPlanStoreRepository(store.GetDB(), logger)
 
 	// intialize services
 	producerService := services.NewProducerService(producer, meterStore)
 	meterService := services.NewMeterService(olap, meterStore)
+	planService := services.NewPlanService(planStore)
 
 	// Register routes
 	routes := routes.NewHTTPHandler(logger)
@@ -95,6 +99,10 @@ func ServeHttp() error {
 	meterRoutes := meterRoutes.NewHTTPHandler(logger, meterService)
 	meterRoutes.RegisterRoutes(v1)
 
+	//plan routes
+	planRoutes := planRoutes.NewHTTPHandler(logger, planService)
+	planRoutes.RegisterRoutes(v1)
+
 	// Start server
 	return app.Listen(":" + config.Server.Port)
 }
diff --git a/migrations/postgres/20250513202626_plan.go b/migrations/postgres/20250513202626_plan.go
new file mode 100644
index 0000000..ac7ef65
--- /dev/null
+++ b/migrations/postgres/20250513202626_plan.go
@@ -0,0 +1,44 @@
+package postgres
+
+import (
+	"context"
+	"database/sql"
+
+	"github.com/pressly/goose/v3"
+)
+
+func init() {
+	goose.AddMigrationContext(upPlan, downPlan)
+}
+
+func upPlan(ctx context.Context, tx *sql.Tx) error {
+	_, err := tx.ExecContext(ctx, `
+		do $$
+		begin
+			-- create table and indexes
+			create table if not exists plan (
+				id uuid primary key default uuid_generate_v4(),
+				name varchar not null,
+				description text,
+				tenant_slug varchar not null,
+        created_at timestamp with time zone not null default current_timestamp,
+        updated_at timestamp with time zone not null default current_timestamp,
+        created_by varchar not null,
+        updated_by varchar not null
+			);
+		  
+      perform goose_manage_updated_at('plan');
+			create index if not exists idx_plan_tenant_slug on plan(tenant_slug);
+		end;
+		$$;
+	`)
+	return err
+}
+
+func downPlan(ctx context.Context, tx *sql.Tx) error {
+	// This code is executed when the migration is rolled back.
+	_, err := tx.ExecContext(ctx, `
+		drop table if exists plan;
+	`)
+	return err
+}


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 17**

<details>
<summary>🧹 Nitpick comments (31)</summary><blockquote>

<details>
<summary>bruno/plans/create.bru (1)</summary><blockquote>

`17-23`: **Consider using environment variables for request body fields**

The request body contains hardcoded values for `name`, `description`, and `created_by`. For better reusability across different test scenarios, consider using environment variables similar to how you're using `{{tenant_slug}}` for the header.


```diff
body:json {
  {
-    "name": "RC_TENANT Free Plan",
-    "description": "Free Plan",
-    "created_by": "rc_tenant_admin_user"
+    "name": "{{plan_name}}",
+    "description": "{{plan_description}}",
+    "created_by": "{{created_by}}"
  }
}
```

</blockquote></details>
<details>
<summary>infrastructure/postgres/store/plans/delete.go (1)</summary><blockquote>

`29-29`: **Fix incorrect error operation name**

The error mapping operation appears to be a copy-paste error. It references "DeleteMeter" instead of "DeletePlan".


```diff
- return postgres.MapError(deleteErr, "Postgres.DeleteMeter")
+ return postgres.MapError(deleteErr, "Postgres.DeletePlan")
```

</blockquote></details>
<details>
<summary>interfaces/http/routes/v1/plans/delete.go (2)</summary><blockquote>

`12-33`: **Consider validating the presence of the tenant slug**

The current implementation extracts the tenant slug from the header but doesn't validate if it's present or empty before using it. This could lead to operations being performed without proper tenant context.


```diff
func (h *httpHandler) deleteByID(ctx *fiber.Ctx) error {
	tenantSlug := ctx.Get(constants.TenantHeader)
+	if tenantSlug == "" {
+		errResp := domainerrors.NewErrorResponseWithOpts(nil, domainerrors.EINVALID, "tenant slug is required")
+		h.logger.Error("tenant slug is required", zap.Reflect("error", errResp))
+		return ctx.Status(errResp.Status).JSON(errResp.ToJson())
+	}
	
	id := ctx.Params("id")
```

---

`22-22`: **Use a middleware for tenant context**

Creating context with the tenant slug is duplicated across handler methods. Consider using a middleware to consistently add the tenant slug to the request context.


This logic could be moved to a shared middleware function that's applied to all tenant-specific routes.

</blockquote></details>
<details>
<summary>interfaces/http/routes/v1/plans/details.go (1)</summary><blockquote>

`13-21`: **Add validation for the tenant slug**

Similar to the delete handler, the tenant slug is extracted but not validated. Consider adding validation to ensure operations are performed in the correct tenant context.


```diff
func (h *httpHandler) getByID(ctx *fiber.Ctx) error {
	tenantSlug := ctx.Get(constants.TenantHeader)
+	if tenantSlug == "" {
+		errResp := domainerrors.NewErrorResponseWithOpts(nil, domainerrors.EINVALID, "tenant slug is required")
+		h.logger.Error("tenant slug is required", zap.Reflect("error", errResp))
+		return ctx.Status(errResp.Status).JSON(errResp.ToJson())
+	}
	
	id := ctx.Params("id")
```

</blockquote></details>
<details>
<summary>bruno/plans/update.bru (2)</summary><blockquote>

`8-8`: **Avoid hardcoded UUIDs in test requests**

Using a hardcoded UUID in the request URL makes the test less flexible. Consider using an environment variable instead.


```diff
-  url: {{base_url}}/v1/plans/027fd502-f98b-48ca-a6d5-2c6a133954c2
+  url: {{base_url}}/v1/plans/{{plan_id}}
```

---

`17-23`: **Consider documenting required vs. optional fields**

The request body doesn't indicate which fields are required and which are optional. Adding comments or structuring the file to make this clear would improve maintainability.


Consider adding a comment above the body section to document field requirements, or organize fields to group required and optional fields together.

</blockquote></details>
<details>
<summary>infrastructure/postgres/store/plans/plans.go (1)</summary><blockquote>

`15-20`: **Add nil checks for function parameters**

The function does not check if the provided `db` or `logger` parameters are nil. This could lead to nil pointer dereferences if invalid values are passed.


```diff
func NewPostgresPlanStoreRepository(db any, logger *logger.Logger) repositories.PlanStoreRepository {
+	if db == nil {
+		panic("database connection cannot be nil")
+	}
+	if logger == nil {
+		panic("logger cannot be nil")
+	}
	return &PgPlanStoreRepository{
		q:      gen.New(db.(*pgxpool.Pool)),
		logger: logger,
	}
}
```

</blockquote></details>
<details>
<summary>domain/models/plan.go (2)</summary><blockquote>

`11-16`: **Consider adding JSON tags to input structs**

If these input structs are used directly in HTTP handlers for request body parsing, consider adding JSON tags to ensure proper serialization/deserialization.

```diff
 type CreatePlanInput struct {
-	Name        string
-	Description string
-	CreatedBy   string
+	Name        string `json:"name"`
+	Description string `json:"description,omitempty"`
+	CreatedBy   string `json:"created_by"`
 }
```

---

`18-22`: **Consider adding validation rules**

The input structs lack validation rules. Consider adding validation tags (e.g., using a library like validator) to enforce constraints like required fields.

```diff
 type UpdatePlanInput struct {
-	Name        string
-	Description string
-	UpdatedBy   string
+	Name        string `json:"name" validate:"required"`
+	Description string `json:"description,omitempty"`
+	UpdatedBy   string `json:"updated_by" validate:"required"`
 }
```

</blockquote></details>
<details>
<summary>infrastructure/postgres/store/plans/create.go (3)</summary><blockquote>

`17-23`: **Reuse the extracted tenant slug**

Line 20 duplicates the tenant slug extraction that was already done in line 16. Reuse the extracted value.

```diff
 	m, err := p.q.CreatePlan(ctx, gen.CreatePlanParams{
 		Name:        arg.Name,
 		Description: pgtype.Text{String: arg.Description, Valid: arg.Description != ""},
-		TenantSlug:  ctx.Value(constants.TenantSlugKey).(string),
+		TenantSlug:  tenantSlug,
 		CreatedBy:   arg.CreatedBy,
 		UpdatedBy:   arg.CreatedBy,
 	})
```

---

`25-28`: **Fix error message in MapError call**

The error message refers to "Postgres.CreateMeter" instead of "Postgres.CreatePlan".

```diff
 	if err != nil {
 		p.logger.Error("failed to create plan", zap.Error(err))
-		return nil, postgres.MapError(err, "Postgres.CreateMeter")
+		return nil, postgres.MapError(err, "Postgres.CreatePlan")
 	}
```

---

`30-34`: **Add error check for UUID conversion**

Line 30 extracts the UUID from bytes and ignores the error on line 31, but then checks if the error is not nil on the next line. This is confusing logic. Either handle the error consistently or document why the check is needed.

```diff
-	id, err := uuid.FromBytes(m.ID.Bytes[:])
-	if err != nil {
+	id, parseErr := uuid.FromBytes(m.ID.Bytes[:])
+	if parseErr != nil {
 		p.logger.Error("failed to parse UUID from bytes", zap.Error(err))
 		return nil, postgres.MapError(err, "Postgres.ParseUUID")
 	}
```

</blockquote></details>
<details>
<summary>infrastructure/postgres/store/plans/list.go (2)</summary><blockquote>

`23-26`: **Fix error message in MapError call**

The error message refers to "Postgres.ListMeters" instead of "Postgres.ListPlans".

```diff
 	if err != nil {
 		p.logger.Error("Error listing plans: ", zap.Error(err))
-		return nil, postgres.MapError(err, "Postgres.ListMeters")
+		return nil, postgres.MapError(err, "Postgres.ListPlans")
 	}
```

---

`46-49`: **Fix error message in MapError call**

The error message refers to "Postgres.CountMeters" instead of "Postgres.CountPlans".

```diff
 	if err != nil {
 		p.logger.Error("Error counting plans: ", zap.Error(err))
-		return nil, postgres.MapError(err, "Postgres.CountMeters")
+		return nil, postgres.MapError(err, "Postgres.CountPlans")
 	}
```

</blockquote></details>
<details>
<summary>infrastructure/postgres/store/plans/details.go (1)</summary><blockquote>

`28-31`: **Update the error context message.**

The error context message refers to "GetMeterByIDorSlug" which appears to be a copy-paste error and should be updated to match the current function.

```diff
-		return nil, postgres.MapError(detailsErr, "Postgres.GetMeterByIDorSlug")
+		return nil, postgres.MapError(detailsErr, "Postgres.GetPlanByID")
```

</blockquote></details>
<details>
<summary>infrastructure/postgres/store/plans/update.go (2)</summary><blockquote>

`30-32`: **Update the error context message.**

The error context message refers to "UpdateMeterByIDorSlug" which appears to be a copy-paste error and should be updated to match the current function.

```diff
-		return nil, postgres.MapError(updateErr, "Postgres.UpdateMeterByIDorSlug")
+		return nil, postgres.MapError(updateErr, "Postgres.UpdatePlanByID")
```

---

`39-51`: **Update the comment to match the operation.**

The comment "Valid UUID, delete by ID" doesn't match the update operation being performed. Also, ensure the domain model construction includes all necessary fields.

```diff
-	// Valid UUID, delete by ID
+	// Construct domain model from database model
```

</blockquote></details>
<details>
<summary>migrations/postgres/20250513202626_plan.go (3)</summary><blockquote>

`19-28`: **Fix inconsistent indentation in table definition**

The indentation in the table definition is inconsistent. Lines 24-27 use more spaces than the rest of the columns, which affects readability.

```diff
create table if not exists plan (
	id uuid primary key default uuid_generate_v4(),
	name varchar not null,
	description text,
	tenant_slug varchar not null,
-        created_at timestamp with time zone not null default current_timestamp,
-        updated_at timestamp with time zone not null default current_timestamp,
-        created_by varchar not null,
-        updated_by varchar not null
+	created_at timestamp with time zone not null default current_timestamp,
+	updated_at timestamp with time zone not null default current_timestamp,
+	created_by varchar not null,
+	updated_by varchar not null
);
```

---

`19-28`: **Consider adding a unique constraint on (tenant_slug, name)**

If plan names should be unique within a tenant, consider adding a unique constraint. This would prevent duplicate plan names for the same tenant and create an index that can improve query performance.

```diff
create table if not exists plan (
	id uuid primary key default uuid_generate_v4(),
	name varchar not null,
	description text,
	tenant_slug varchar not null,
	created_at timestamp with time zone not null default current_timestamp,
	updated_at timestamp with time zone not null default current_timestamp,
	created_by varchar not null,
	updated_by varchar not null
+	, unique(tenant_slug, name)
);
```


Is uniqueness on plan names within a tenant a business requirement?

---

`21-21`: **Consider adding a length constraint to name column**

The `name` column is currently defined as `varchar not null` without a length limit. Consider adding a reasonable maximum length to prevent excessively long names and ensure consistent data.

```diff
-	name varchar not null,
+	name varchar(255) not null,
```

</blockquote></details>
<details>
<summary>interfaces/http/routes/v1/plans/create.go (3)</summary><blockquote>

`20-20`: **Use camelCase for variable naming to follow Go conventions**

Go conventionally uses camelCase for variable names. Consider changing `tenant_slug` to `tenantSlug`.

```diff
-	tenant_slug := ctx.Get(constants.TenantHeader)
+	tenantSlug := ctx.Get(constants.TenantHeader)
```

---

`35-35`: **Use tenantSlug variable with the updated context**

After converting to camelCase above, update the context creation to use the updated variable name:

```diff
-	c := context.WithValue(ctx.UserContext(), constants.TenantSlugKey, tenant_slug)
+	c := context.WithValue(ctx.UserContext(), constants.TenantSlugKey, tenantSlug)
```

---

`43-44`: **Add more context to error logs**

The current error log "failed to create plan" could provide more context to aid debugging. Include relevant request details, being careful not to log sensitive information.

```diff
-	h.logger.Error("failed to create plan", zap.Reflect("error", err))
+	h.logger.Error("failed to create plan", 
+		zap.String("name", req.Name),
+		zap.String("tenant", tenantSlug),
+		zap.Reflect("error", err))
```

</blockquote></details>
<details>
<summary>infrastructure/postgres/sql/queries/plan.sql (2)</summary><blockquote>

`17-22`: **Consider cursor-based pagination for better performance**

The current offset-based pagination (LIMIT/OFFSET) can become inefficient for large datasets. Consider implementing cursor-based pagination using the `created_at` and `id` fields that you're already ordering by:

```sql
-- For getting the next page:
-- name: ListPlansNextPage :many
SELECT * FROM plan
WHERE tenant_slug = $1
AND (created_at < $2 OR (created_at = $2 AND id < $3))
ORDER BY created_at DESC, id DESC
LIMIT $4;
```

This approach is more efficient and provides consistent results even when new records are added while paginating.

---

`42-43`: **Remove empty lines at the end of the file**

There are empty lines at the end of the file that should be removed to maintain cleanliness.

</blockquote></details>
<details>
<summary>application/services/plan.go (5)</summary><blockquote>

`22-30`: **CreatePlan method is a simple passthrough.**

The implementation is correct but could be enhanced with additional business logic validation before delegating to the repository. Consider adding validation for required fields and business rules at the service level.

---

`41-49`: **Simplify DeletePlan method.**

The method can be simplified since it just passes through the error from the repository without additional processing.

```diff
func (s *PlanService) DeletePlan(ctx context.Context, ID string) error {
-
-	err := s.store.DeletePlanByID(ctx, ID)
-	if err != nil {
-		return err
-	}
-
-	return nil
+	return s.store.DeletePlanByID(ctx, ID)
}
```

---

`51-59`: **UpdatePlan method implementation is correct.**

Similar to CreatePlan, consider adding input validation logic at this level before delegating to the repository.

---

`61-69`: **ListPlans method implementation is correct.**

The method properly handles pagination and delegates to the repository layer. Consider adding validation for pagination parameters to ensure they are within acceptable ranges.

---

`1-70`: **Add logging for operational visibility.**

The service currently lacks logging, which would be helpful for debugging and monitoring. Consider adding contextual logging for each operation, especially for errors.

```diff
import (
	"context"

	"github.com/redcardinal-io/metering/application/repositories"
	"github.com/redcardinal-io/metering/domain/models"
	"github.com/redcardinal-io/metering/domain/pkg/pagination"
+	"github.com/redcardinal-io/metering/domain/pkg/logging"
)

type PlanService struct {
	store repositories.PlanStoreRepository
+	logger logging.Logger
}

// NewPlanService creates a new PlanService with the provided plan store repositories.
-func NewPlanService(store repositories.PlanStoreRepository) *PlanService {
+func NewPlanService(store repositories.PlanStoreRepository, logger logging.Logger) *PlanService {
	return &PlanService{
		store: store,
+		logger: logger,
	}
}

func (s *PlanService) CreatePlan(ctx context.Context, arg models.CreatePlanInput) (*models.Plan, error) {
+	s.logger.Info(ctx, "Creating new plan", logging.Fields{"name": arg.Name})
	// Store the plan in the database
	m, err := s.store.CreatePlan(ctx, arg)
	if err != nil {
+		s.logger.Error(ctx, "Failed to create plan", logging.Fields{"error": err.Error(), "name": arg.Name})
		return nil, err
	}

+	s.logger.Info(ctx, "Plan created successfully", logging.Fields{"id": m.ID, "name": m.Name})
	return m, nil
}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between dbdbcf459b00ac59f31f5aa01cbe0599b99817e9 and 74bc89dba2c1b3c70f272dfdd8fa01804234193c.

</details>

<details>
<summary>⛔ Files ignored due to path filters (3)</summary>

* `infrastructure/postgres/gen/models.go` is excluded by `!**/gen/**`
* `infrastructure/postgres/gen/plan.sql.go` is excluded by `!**/gen/**`
* `infrastructure/postgres/gen/querier.go` is excluded by `!**/gen/**`

</details>

<details>
<summary>📒 Files selected for processing (25)</summary>

* `application/repositories/store.go` (1 hunks)
* `application/services/plan.go` (1 hunks)
* `bruno/plans/create.bru` (1 hunks)
* `bruno/plans/delete.bru` (1 hunks)
* `bruno/plans/folder.bru` (1 hunks)
* `bruno/plans/get_by_id.bru` (1 hunks)
* `bruno/plans/list.bru` (1 hunks)
* `bruno/plans/update.bru` (1 hunks)
* `domain/models/plan.go` (1 hunks)
* `infrastructure/postgres/sql/queries/plan.sql` (1 hunks)
* `infrastructure/postgres/sql/schema.sql` (1 hunks)
* `infrastructure/postgres/store/plans/create.go` (1 hunks)
* `infrastructure/postgres/store/plans/delete.go` (1 hunks)
* `infrastructure/postgres/store/plans/details.go` (1 hunks)
* `infrastructure/postgres/store/plans/list.go` (1 hunks)
* `infrastructure/postgres/store/plans/plans.go` (1 hunks)
* `infrastructure/postgres/store/plans/update.go` (1 hunks)
* `interfaces/http/routes/v1/plans/create.go` (1 hunks)
* `interfaces/http/routes/v1/plans/delete.go` (1 hunks)
* `interfaces/http/routes/v1/plans/details.go` (1 hunks)
* `interfaces/http/routes/v1/plans/list.go` (1 hunks)
* `interfaces/http/routes/v1/plans/routes.go` (1 hunks)
* `interfaces/http/routes/v1/plans/update.go` (1 hunks)
* `interfaces/http/server.go` (3 hunks)
* `migrations/postgres/20250513202626_plan.go` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (14)</summary>

<details>
<summary>infrastructure/postgres/store/plans/list.go (6)</summary><blockquote>

<details>
<summary>infrastructure/postgres/store/plans/plans.go (1)</summary>

* `PgPlanStoreRepository` (10-13)

</details>
<details>
<summary>domain/pkg/pagination/pagination.go (3)</summary>

* `Pagination` (8-14)
* `PaginationView` (17-22)
* `FormatWith` (69-76)

</details>
<details>
<summary>infrastructure/postgres/gen/models.go (1)</summary>

* `Plan` (76-85)

</details>
<details>
<summary>domain/pkg/constants/constants.go (1)</summary>

* `TenantSlugKey` (9-9)

</details>
<details>
<summary>infrastructure/postgres/gen/plan.sql.go (1)</summary>

* `ListPlansPaginatedParams` (119-123)

</details>
<details>
<summary>domain/models/base.go (1)</summary>

* `Base` (8-14)

</details>

</blockquote></details>
<details>
<summary>infrastructure/postgres/store/plans/details.go (5)</summary><blockquote>

<details>
<summary>infrastructure/postgres/store/plans/plans.go (1)</summary>

* `PgPlanStoreRepository` (10-13)

</details>
<details>
<summary>infrastructure/postgres/gen/models.go (1)</summary>

* `Plan` (76-85)

</details>
<details>
<summary>domain/pkg/constants/constants.go (1)</summary>

* `TenantSlugKey` (9-9)

</details>
<details>
<summary>infrastructure/postgres/gen/plan.sql.go (1)</summary>

* `GetPlanByIDParams` (90-93)

</details>
<details>
<summary>domain/models/base.go (1)</summary>

* `Base` (8-14)

</details>

</blockquote></details>
<details>
<summary>interfaces/http/routes/v1/plans/routes.go (2)</summary><blockquote>

<details>
<summary>domain/pkg/logger/logger.go (1)</summary>

* `Logger` (33-35)

</details>
<details>
<summary>application/services/plan.go (1)</summary>

* `PlanService` (11-13)

</details>

</blockquote></details>
<details>
<summary>infrastructure/postgres/store/plans/update.go (6)</summary><blockquote>

<details>
<summary>infrastructure/postgres/store/plans/plans.go (1)</summary>

* `PgPlanStoreRepository` (10-13)

</details>
<details>
<summary>domain/models/plan.go (1)</summary>

* `UpdatePlanInput` (18-22)

</details>
<details>
<summary>infrastructure/postgres/gen/models.go (1)</summary>

* `Plan` (76-85)

</details>
<details>
<summary>domain/pkg/constants/constants.go (1)</summary>

* `TenantSlugKey` (9-9)

</details>
<details>
<summary>infrastructure/postgres/gen/plan.sql.go (1)</summary>

* `UpdatePlanByIDParams` (164-170)

</details>
<details>
<summary>domain/models/base.go (1)</summary>

* `Base` (8-14)

</details>

</blockquote></details>
<details>
<summary>infrastructure/postgres/store/plans/delete.go (3)</summary><blockquote>

<details>
<summary>infrastructure/postgres/store/plans/plans.go (1)</summary>

* `PgPlanStoreRepository` (10-13)

</details>
<details>
<summary>domain/pkg/constants/constants.go (1)</summary>

* `TenantSlugKey` (9-9)

</details>
<details>
<summary>infrastructure/postgres/gen/plan.sql.go (1)</summary>

* `DeletePlanByIDParams` (74-77)

</details>

</blockquote></details>
<details>
<summary>domain/models/plan.go (1)</summary><blockquote>

<details>
<summary>domain/models/base.go (1)</summary>

* `Base` (8-14)

</details>

</blockquote></details>
<details>
<summary>interfaces/http/routes/v1/plans/delete.go (2)</summary><blockquote>

<details>
<summary>domain/pkg/constants/constants.go (2)</summary>

* `TenantHeader` (7-7)
* `TenantSlugKey` (9-9)

</details>
<details>
<summary>domain/errors/errors.go (3)</summary>

* `NewErrorResponseWithOpts` (300-303)
* `EINVALID` (50-50)
* `NewErrorResponse` (248-290)

</details>

</blockquote></details>
<details>
<summary>application/repositories/store.go (3)</summary><blockquote>

<details>
<summary>domain/models/plan.go (3)</summary>

* `CreatePlanInput` (12-16)
* `Plan` (4-9)
* `UpdatePlanInput` (18-22)

</details>
<details>
<summary>infrastructure/postgres/gen/models.go (1)</summary>

* `Plan` (76-85)

</details>
<details>
<summary>domain/pkg/pagination/pagination.go (2)</summary>

* `Pagination` (8-14)
* `PaginationView` (17-22)

</details>

</blockquote></details>
<details>
<summary>interfaces/http/routes/v1/plans/details.go (3)</summary><blockquote>

<details>
<summary>domain/pkg/constants/constants.go (2)</summary>

* `TenantHeader` (7-7)
* `TenantSlugKey` (9-9)

</details>
<details>
<summary>domain/errors/errors.go (3)</summary>

* `NewErrorResponseWithOpts` (300-303)
* `EINVALID` (50-50)
* `NewErrorResponse` (248-290)

</details>
<details>
<summary>domain/models/response.go (1)</summary>

* `NewHttpResponse` (10-16)

</details>

</blockquote></details>
<details>
<summary>infrastructure/postgres/store/plans/create.go (6)</summary><blockquote>

<details>
<summary>infrastructure/postgres/store/plans/plans.go (1)</summary>

* `PgPlanStoreRepository` (10-13)

</details>
<details>
<summary>domain/models/plan.go (1)</summary>

* `CreatePlanInput` (12-16)

</details>
<details>
<summary>infrastructure/postgres/gen/models.go (1)</summary>

* `Plan` (76-85)

</details>
<details>
<summary>domain/pkg/constants/constants.go (1)</summary>

* `TenantSlugKey` (9-9)

</details>
<details>
<summary>infrastructure/postgres/gen/plan.sql.go (1)</summary>

* `CreatePlanParams` (38-44)

</details>
<details>
<summary>domain/models/base.go (1)</summary>

* `Base` (8-14)

</details>

</blockquote></details>
<details>
<summary>application/services/plan.go (3)</summary><blockquote>

<details>
<summary>application/repositories/store.go (1)</summary>

* `PlanStoreRepository` (26-32)

</details>
<details>
<summary>domain/models/plan.go (3)</summary>

* `CreatePlanInput` (12-16)
* `Plan` (4-9)
* `UpdatePlanInput` (18-22)

</details>
<details>
<summary>domain/pkg/pagination/pagination.go (2)</summary>

* `Pagination` (8-14)
* `PaginationView` (17-22)

</details>

</blockquote></details>
<details>
<summary>interfaces/http/routes/v1/plans/update.go (4)</summary><blockquote>

<details>
<summary>domain/pkg/constants/constants.go (2)</summary>

* `TenantHeader` (7-7)
* `TenantSlugKey` (9-9)

</details>
<details>
<summary>domain/errors/errors.go (3)</summary>

* `NewErrorResponseWithOpts` (300-303)
* `EINVALID` (50-50)
* `NewErrorResponse` (248-290)

</details>
<details>
<summary>domain/models/plan.go (1)</summary>

* `UpdatePlanInput` (18-22)

</details>
<details>
<summary>domain/models/response.go (1)</summary>

* `NewHttpResponse` (10-16)

</details>

</blockquote></details>
<details>
<summary>interfaces/http/routes/v1/plans/create.go (4)</summary><blockquote>

<details>
<summary>domain/pkg/constants/constants.go (2)</summary>

* `TenantHeader` (7-7)
* `TenantSlugKey` (9-9)

</details>
<details>
<summary>domain/errors/errors.go (3)</summary>

* `NewErrorResponseWithOpts` (300-303)
* `EINVALID` (50-50)
* `NewErrorResponse` (248-290)

</details>
<details>
<summary>domain/models/plan.go (1)</summary>

* `CreatePlanInput` (12-16)

</details>
<details>
<summary>domain/models/response.go (1)</summary>

* `NewHttpResponse` (10-16)

</details>

</blockquote></details>
<details>
<summary>interfaces/http/routes/v1/plans/list.go (4)</summary><blockquote>

<details>
<summary>domain/pkg/constants/constants.go (2)</summary>

* `TenantHeader` (7-7)
* `TenantSlugKey` (9-9)

</details>
<details>
<summary>domain/pkg/pagination/extract.go (1)</summary>

* `ExtractPaginationFromContext` (26-63)

</details>
<details>
<summary>domain/errors/errors.go (1)</summary>

* `NewErrorResponse` (248-290)

</details>
<details>
<summary>domain/models/response.go (1)</summary>

* `NewHttpResponse` (10-16)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (27)</summary><blockquote>

<details>
<summary>bruno/plans/folder.bru (1)</summary>

`1-3`: **LGTM: Bruno folder metadata for plans is properly structured.**

The Bruno folder metadata for plans is correctly defined with a name attribute, which organizes the API request files for plan operations.

</details>
<details>
<summary>bruno/plans/list.bru (3)</summary>

`1-5`: **LGTM: Bruno metadata for list plans request is properly defined.**

The metadata contains name, type, and sequence number which allows proper organization within the Bruno collection.

---

`7-11`: **LGTM: HTTP GET request for plans listing is properly defined.**

The endpoint correctly uses the base URL variable and has no request body as expected for a GET operation.

---

`13-15`: **LGTM: Tenant slug header correctly implemented.**

The tenant header implementation follows the multi-tenancy pattern using the tenant_slug variable.

</details>
<details>
<summary>bruno/plans/get_by_id.bru (2)</summary>

`1-5`: **LGTM: Bruno metadata for get plan by ID is properly defined.**

The metadata contains appropriate name, type, and sequence number (3) which fits logically in the operation flow.

---

`13-15`: **LGTM: Tenant slug header correctly implemented.**

The tenant header implementation follows the multi-tenancy pattern using the tenant_slug variable.

</details>
<details>
<summary>interfaces/http/server.go (5)</summary>

`16-16`: **LGTM: Plans package imported correctly.**

The plan store import is properly added to include the plans functionality.

---

`21-21`: **LGTM: Plan routes package imported with alias.**

The plan routes import with the "planRoutes" alias follows the existing pattern for route imports.

---

`77-77`: **LGTM: Plan store repository properly initialized.**

The plan store repository is correctly initialized following the same pattern as the meter store repository.

---

`82-82`: **LGTM: Plan service properly initialized.**

The plan service is initialized correctly with its dependency on the plan store.

---

`102-105`: **LGTM: Plan routes properly registered.**

The plan routes are correctly registered under the v1 API group with the required dependencies.

</details>
<details>
<summary>interfaces/http/routes/v1/plans/delete.go (1)</summary>

`31-33`: **Good use of the NoContent status code**

The method correctly returns a 204 No Content status for successful deletion operations, following REST best practices.

</details>
<details>
<summary>interfaces/http/routes/v1/plans/details.go (1)</summary>

`32-34`: **Good use of structured response**

The code correctly uses the `NewHttpResponse` helper to create a consistent and well-structured API response, which helps maintain API consistency across the application.

</details>
<details>
<summary>domain/models/plan.go (1)</summary>

`1-9`: **LGTM: Clean domain model for Plan entity**

The Plan struct is well-designed with appropriate JSON tags and embedding of the Base struct for common fields.

</details>
<details>
<summary>application/repositories/store.go (1)</summary>

`26-32`: **LGTM! The PlanStoreRepository interface follows a consistent repository pattern.**

The interface provides all necessary CRUD operations for Plan entities with proper context support and domain model usage.

</details>
<details>
<summary>interfaces/http/routes/v1/plans/update.go (5)</summary>

`13-17`: **Request validation looks good.**

The validation rules for the request fields are appropriate and consistent with other parts of the codebase.

---

`19-27`: **LGTM! Good practice extracting tenant and ID.**

The handler correctly extracts the tenant identifier from the request header and validates the plan ID parameter.

---

`29-46`: **Request validation looks good.**

The handler properly validates both the structure and business rules for the update request, ensuring at least one updateable field is provided.

---

`48-59`: **LGTM! Good context handling and error management.**

The handler correctly passes tenant context to the service layer and provides detailed error responses.

---

`61-63`: **Good HTTP response formatting.**

The handler returns a well-structured success response with appropriate status code and message.

</details>
<details>
<summary>infrastructure/postgres/store/plans/details.go (2)</summary>

`17-26`: **LGTM! Good UUID parsing and query execution.**

The method correctly handles UUID parsing and executes the database query with appropriate parameters.

---

`33-49`: **LGTM! Clean domain model construction.**

The method correctly converts the database model to a domain model with all required fields.

</details>
<details>
<summary>infrastructure/postgres/store/plans/update.go (1)</summary>

`17-28`: **LGTM! Good UUID parsing and query parameter setup.**

The method correctly handles UUID parsing and sets up query parameters with appropriate valid flags for optional fields.

</details>
<details>
<summary>interfaces/http/routes/v1/plans/routes.go (1)</summary>

`26-38`: **LGTM! Code follows RESTful conventions.**

The route setup follows REST conventions properly and provides a clean organization for plan-related endpoints. The implementation uses dependency injection and separation of concerns.


Consider if these routes should have authentication or authorization middleware applied. While this might be handled at a higher level in the router hierarchy, it's good to ensure these endpoints are properly secured based on your requirements.

</details>
<details>
<summary>interfaces/http/routes/v1/plans/create.go (1)</summary>

`48-49`: **LGTM! Good response formatting.**

The success response formatting is clean and provides appropriate status, data, and message. The HTTP 201 Created status is correctly used for resource creation.

</details>
<details>
<summary>application/services/plan.go (2)</summary>

`11-20`: **Clean service structure with dependency injection.**

The service is properly designed with dependency injection through the constructor, making it testable and adhering to good separation of concerns. The repository interface is accepted as a parameter, allowing for different implementations to be used.

---

`32-39`: **GetPlanByID method looks good.**

This is a straightforward implementation that correctly delegates to the repository and handles errors appropriately.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T16:45:32Z
user: login: elliot14A
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-15T02:28:26Z

Review comments:
diff_hunk: @@ -0,0 +1,15 @@
+meta {
+  name: get_by_id
+  type: http
+  seq: 3
+}
+
+get {
+  url: {{base_url}}/v1/plans/027fd502-f98b-48ca-a6d5-2c6a133954c2
+  body: none
+  auth: inherit
+}
path: bruno/plans/get_by_id.bru
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Consider using a path parameter variable instead of hardcoded UUID.**

The URL contains a hardcoded UUID which limits reusability. Consider using a variable like `{{plan_id}}` instead.

```diff
-  url: {{base_url}}/v1/plans/027fd502-f98b-48ca-a6d5-2c6a133954c2
+  url: {{base_url}}/v1/plans/{{plan_id}}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
get {
  url: {{base_url}}/v1/plans/{{plan_id}}
  body: none
  auth: inherit
}
```

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T16:45:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,31 @@
+package plans
+
+import (
+	"context"
+
+	"github.com/gofiber/fiber/v2"
+	domainerrors "github.com/redcardinal-io/metering/domain/errors"
+	"github.com/redcardinal-io/metering/domain/models"
+	"github.com/redcardinal-io/metering/domain/pkg/constants"
+	"github.com/redcardinal-io/metering/domain/pkg/pagination"
+	"go.uber.org/zap"
+)
+
+func (h *httpHandler) list(ctx *fiber.Ctx) error {
+	tenantSlug := ctx.Get(constants.TenantHeader)
+
path: interfaces/http/routes/v1/plans/list.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add validation for tenant slug**

The handler extracts the tenant slug from the request header but doesn't validate if it's present or valid. If the header is missing or empty, this could lead to issues when using it in subsequent operations.


```diff
func (h *httpHandler) list(ctx *fiber.Ctx) error {
	tenantSlug := ctx.Get(constants.TenantHeader)
+	
+	if tenantSlug == "" {
+		errResp := domainerrors.NewErrorResponse(domainerrors.ErrInvalidRequest.WithMessage("Tenant slug is required"))
+		return ctx.Status(errResp.Status).JSON(errResp.ToJson())
+	}

	// Rest of the function...
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
func (h *httpHandler) list(ctx *fiber.Ctx) error {
	tenantSlug := ctx.Get(constants.TenantHeader)

	if tenantSlug == "" {
		errResp := domainerrors.NewErrorResponse(
			domainerrors.ErrInvalidRequest.
				WithMessage("Tenant slug is required"),
		)
		return ctx.Status(errResp.Status).JSON(errResp.ToJson())
	}

	// Rest of the function...
}
```

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T16:45:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,33 @@
+package plans
+
+import (
+	"context"
+
+	"github.com/google/uuid"
+	"github.com/jackc/pgx/v5/pgtype"
+	"github.com/redcardinal-io/metering/domain/pkg/constants"
+	"github.com/redcardinal-io/metering/infrastructure/postgres"
+	"github.com/redcardinal-io/metering/infrastructure/postgres/gen"
+	"go.uber.org/zap"
+)
+
+func (p *PgPlanStoreRepository) DeletePlanByID(ctx context.Context, id string) error {
+	// Try to parse as UUID first
+	tenantSlug := ctx.Value(constants.TenantSlugKey).(string)
+	parsedId, err := uuid.Parse(id)
+	var deleteErr error
+	if err == nil {
+		// Valid UUID, delete by ID
+		deleteErr = p.q.DeletePlanByID(ctx, gen.DeletePlanByIDParams{
+			ID:         pgtype.UUID{Bytes: parsedId, Valid: true},
+			TenantSlug: tenantSlug,
+		})
+	}
path: infrastructure/postgres/store/plans/delete.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Handle UUID parsing error properly**

The function tries to parse the ID as a UUID but doesn't handle the error case properly. If parsing fails, `deleteErr` remains `nil` and the function returns success, even though no deletion occurred.


```diff
parsedId, err := uuid.Parse(id)
var deleteErr error
if err == nil {
	// Valid UUID, delete by ID
	deleteErr = p.q.DeletePlanByID(ctx, gen.DeletePlanByIDParams{
		ID:         pgtype.UUID{Bytes: parsedId, Valid: true},
		TenantSlug: tenantSlug,
	})
+} else {
+	// Invalid UUID format
+	p.logger.Error("invalid UUID format", zap.String("id", id), zap.Error(err))
+	return postgres.ErrInvalidUUID
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
// … earlier in the function …
parsedId, err := uuid.Parse(id)
var deleteErr error
if err == nil {
    // Valid UUID, delete by ID
    deleteErr = p.q.DeletePlanByID(ctx, gen.DeletePlanByIDParams{
        ID:         pgtype.UUID{Bytes: parsedId, Valid: true},
        TenantSlug: tenantSlug,
    })
} else {
    // Invalid UUID format
    p.logger.Error("invalid UUID format", zap.String("id", id), zap.Error(err))
    return postgres.ErrInvalidUUID
}

// … rest of the function …
```

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T16:45:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,33 @@
+package plans
+
+import (
+	"context"
+
+	"github.com/google/uuid"
+	"github.com/jackc/pgx/v5/pgtype"
+	"github.com/redcardinal-io/metering/domain/pkg/constants"
+	"github.com/redcardinal-io/metering/infrastructure/postgres"
+	"github.com/redcardinal-io/metering/infrastructure/postgres/gen"
+	"go.uber.org/zap"
+)
+
+func (p *PgPlanStoreRepository) DeletePlanByID(ctx context.Context, id string) error {
+	// Try to parse as UUID first
+	tenantSlug := ctx.Value(constants.TenantSlugKey).(string)
+	parsedId, err := uuid.Parse(id)
path: infrastructure/postgres/store/plans/delete.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix unsafe type assertion**

The current code uses an unsafe type assertion that will panic if the value isn't a string or is missing from the context. Use a type assertion with an ok check to handle this gracefully.


```diff
- tenantSlug := ctx.Value(constants.TenantSlugKey).(string)
+ tenantSlugVal := ctx.Value(constants.TenantSlugKey)
+ if tenantSlugVal == nil {
+   return postgres.ErrInvalidTenantSlug
+ }
+ 
+ tenantSlug, ok := tenantSlugVal.(string)
+ if !ok || tenantSlug == "" {
+   return postgres.ErrInvalidTenantSlug
+ }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
	tenantSlugVal := ctx.Value(constants.TenantSlugKey)
	if tenantSlugVal == nil {
		return postgres.ErrInvalidTenantSlug
	}

	tenantSlug, ok := tenantSlugVal.(string)
	if !ok || tenantSlug == "" {
		return postgres.ErrInvalidTenantSlug
	}

	parsedId, err := uuid.Parse(id)
```

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T16:45:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,15 @@
+meta {
+  name: delete
+  type: http
+  seq: 5
+}
+
+delete {
+  url: {{base_url}}/v1/plans/027fd502-f98b-48ca-a6d5-2c6a133954c2
+  body: none
path: bruno/plans/delete.bru
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Use a variable for the plan ID**

The URL contains a hardcoded UUID. To make this request reusable for testing different plans, consider using a variable like `{{plan_id}}` instead.


```diff
delete {
-  url: {{base_url}}/v1/plans/027fd502-f98b-48ca-a6d5-2c6a133954c2
+  url: {{base_url}}/v1/plans/{{plan_id}}
  body: none
  auth: inherit
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
delete {
  url: {{base_url}}/v1/plans/{{plan_id}}
  body: none
  auth: inherit
}
```

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T16:45:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+package plans
+
+import (
+	"github.com/jackc/pgx/v5/pgxpool"
+	"github.com/redcardinal-io/metering/application/repositories"
+	"github.com/redcardinal-io/metering/domain/pkg/logger"
+	"github.com/redcardinal-io/metering/infrastructure/postgres/gen"
+)
+
+type PgPlanStoreRepository struct {
+	q      *gen.Queries
+	logger *logger.Logger
+}
+
+func NewPostgresPlanStoreRepository(db any, logger *logger.Logger) repositories.PlanStoreRepository {
+	return &PgPlanStoreRepository{
+		q:      gen.New(db.(*pgxpool.Pool)),
+		logger: logger,
+	}
+}
path: infrastructure/postgres/store/plans/plans.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling for type assertion**

The current implementation uses an unsafe type assertion `db.(*pgxpool.Pool)` which will panic if the provided database connection isn't of the expected type. Consider adding proper error handling.


```diff
func NewPostgresPlanStoreRepository(db any, logger *logger.Logger) repositories.PlanStoreRepository {
+	pool, ok := db.(*pgxpool.Pool)
+	if !ok {
+		panic("expected *pgxpool.Pool but got something else")
+	}
	return &PgPlanStoreRepository{
-		q:      gen.New(db.(*pgxpool.Pool)),
+		q:      gen.New(pool),
		logger: logger,
	}
}
```

Alternatively, you could change the function signature to directly accept the expected type:

```diff
-func NewPostgresPlanStoreRepository(db any, logger *logger.Logger) repositories.PlanStoreRepository {
+func NewPostgresPlanStoreRepository(db *pgxpool.Pool, logger *logger.Logger) repositories.PlanStoreRepository {
	return &PgPlanStoreRepository{
-		q:      gen.New(db.(*pgxpool.Pool)),
+		q:      gen.New(db),
		logger: logger,
	}
}
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T16:45:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,3 +24,14 @@ create table "meter" (
 
   unique (tenant_slug, slug)
 );
+
+create table if not exists plan (
+	id uuid primary key default uuid_generate_v4(),
+	name varchar not null,
+	description text,
+	tenant_slug varchar not null,
+	created_at timestamp with time zone not null default current_timestamp,
+	updated_at timestamp with time zone not null default current_timestamp,
+	created_by varchar not null,
+	updated_by varchar not null
+);
path: infrastructure/postgres/sql/schema.sql
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add index on tenant_slug for improved query performance**

Since this appears to be a multi-tenant application and queries will likely filter by tenant_slug, consider adding an index on this column to improve query performance.

```diff
 create table if not exists plan (
 	id uuid primary key default uuid_generate_v4(),
 	name varchar not null,
 	description text,
 	tenant_slug varchar not null,
 	created_at timestamp with time zone not null default current_timestamp,
 	updated_at timestamp with time zone not null default current_timestamp,
 	created_by varchar not null,
 	updated_by varchar not null
 );
+create index plan_tenant_slug_idx on plan(tenant_slug);
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
create table if not exists plan (
	id uuid primary key default uuid_generate_v4(),
	name varchar not null,
	description text,
	tenant_slug varchar not null,
	created_at timestamp with time zone not null default current_timestamp,
	updated_at timestamp with time zone not null default current_timestamp,
	created_by varchar not null,
	updated_by varchar not null
);
create index plan_tenant_slug_idx on plan(tenant_slug);
```

</details>

<!-- suggestion_end -->

---

_💡 Verification agent_

<details>
<summary>❓ Verification inconclusive</summary>

**Consider adding a unique constraint for plan names within a tenant**

The "meter" table has a unique constraint on (tenant_slug, slug), but the "plan" table doesn't have a similar constraint. Consider whether plan names should be unique within a tenant.

---

🏁 Script executed:

```shell
#!/bin/bash

# Check if unique constraints are enforced at the application level
rg -t go "CREATE TABLE.*plan|INSERT INTO.*plan.*ON CONFLICT" -A 10 -B 10
```

Length of output: 74

---



</details>

**Add a unique constraint on (tenant_slug, name) in the plan table**

The `plan` table currently allows duplicate names within the same tenant, unlike the `meter` table, which enforces uniqueness on `(tenant_slug, slug)`. To prevent name collisions and ensure consistency, add a unique constraint on `(tenant_slug, name)`.

• File: `infrastructure/postgres/sql/schema.sql`  
• Location: inside the `create table plan (…)` definition or as a subsequent `ALTER TABLE`

Suggested diff:
```sql
ALTER TABLE plan
  ADD CONSTRAINT plan_unique_tenant_name
    UNIQUE (tenant_slug, name);
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T16:45:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,50 @@
+package plans
+
+import (
+	"context"
+
+	"github.com/google/uuid"
+	"github.com/jackc/pgx/v5/pgtype"
+	"github.com/redcardinal-io/metering/domain/models"
+	"github.com/redcardinal-io/metering/domain/pkg/constants"
+	"github.com/redcardinal-io/metering/infrastructure/postgres"
+	"github.com/redcardinal-io/metering/infrastructure/postgres/gen"
+	"go.uber.org/zap"
+)
+
+func (p *PgPlanStoreRepository) CreatePlan(ctx context.Context, arg models.CreatePlanInput) (*models.Plan, error) {
+	tenantSlug := ctx.Value(constants.TenantSlugKey).(string)
+	m, err := p.q.CreatePlan(ctx, gen.CreatePlanParams{
path: infrastructure/postgres/store/plans/create.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Add type assertion check to prevent panic**

The type assertion on line 16 will panic if the context value is not a string or is nil. Add a check to safely extract the tenant slug.

```diff
-	tenantSlug := ctx.Value(constants.TenantSlugKey).(string)
+	tenantSlugValue := ctx.Value(constants.TenantSlugKey)
+	if tenantSlugValue == nil {
+		p.logger.Error("tenant slug not found in context")
+		return nil, postgres.ErrInvalidInput
+	}
+	tenantSlug, ok := tenantSlugValue.(string)
+	if !ok {
+		p.logger.Error("tenant slug is not a string")
+		return nil, postgres.ErrInvalidInput
+	}
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T16:45:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,54 @@
+package plans
+
+import (
+	"context"
+
+	"github.com/google/uuid"
+	"github.com/redcardinal-io/metering/domain/models"
+	"github.com/redcardinal-io/metering/domain/pkg/constants"
+	"github.com/redcardinal-io/metering/domain/pkg/pagination"
+	"github.com/redcardinal-io/metering/infrastructure/postgres"
+	"github.com/redcardinal-io/metering/infrastructure/postgres/gen"
+	"go.uber.org/zap"
+)
+
+func (p *PgPlanStoreRepository) ListPlans(ctx context.Context, page pagination.Pagination) (*pagination.PaginationView[models.Plan], error) {
+	tenantSlug := ctx.Value(constants.TenantSlugKey).(string)
+	m, err := p.q.ListPlansPaginated(ctx, gen.ListPlansPaginatedParams{
+		Limit:      int32(page.Limit),
+		Offset:     int32(page.GetOffset()),
+		TenantSlug: ctx.Value(constants.TenantSlugKey).(string),
+	})
+
+	if err != nil {
+		p.logger.Error("Error listing plans: ", zap.Error(err))
+		return nil, postgres.MapError(err, "Postgres.ListMeters")
+	}
+
+	plans := make([]models.Plan, 0, len(m))
+	for _, plan := range m {
+		id, _ := uuid.FromBytes(plan.ID.Bytes[:])
+		plans = append(plans, models.Plan{
+			Name:        plan.Name,
+			Description: plan.Description.String,
+			TenantSlug:  plan.TenantSlug,
+			Base: models.Base{
+				ID:        id,
+				CreatedAt: plan.CreatedAt,
+				CreatedBy: plan.CreatedBy,
+				UpdatedBy: plan.UpdatedBy,
+				UpdatedAt: plan.UpdatedAt,
+			},
+		})
+	}
path: infrastructure/postgres/store/plans/list.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Handle UUID conversion errors**

Line 30 ignores potential UUID conversion errors that could lead to incorrect data.

```diff
 	plans := make([]models.Plan, 0, len(m))
 	for _, plan := range m {
-		id, _ := uuid.FromBytes(plan.ID.Bytes[:])
+		id, err := uuid.FromBytes(plan.ID.Bytes[:])
+		if err != nil {
+			p.logger.Error("Error converting UUID from bytes", zap.Error(err))
+			return nil, postgres.MapError(err, "Postgres.ParseUUID")
+		}
 		plans = append(plans, models.Plan{
 			Name:        plan.Name,
 			Description: plan.Description.String,
 			TenantSlug:  plan.TenantSlug,
 			Base: models.Base{
 				ID:        id,
 				CreatedAt: plan.CreatedAt,
 				CreatedBy: plan.CreatedBy,
 				UpdatedBy: plan.UpdatedBy,
 				UpdatedAt: plan.UpdatedAt,
 			},
 		})
 	}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
	plans := make([]models.Plan, 0, len(m))
	for _, plan := range m {
		id, err := uuid.FromBytes(plan.ID.Bytes[:])
		if err != nil {
			p.logger.Error("Error converting UUID from bytes", zap.Error(err))
			return nil, postgres.MapError(err, "Postgres.ParseUUID")
		}
		plans = append(plans, models.Plan{
			Name:        plan.Name,
			Description: plan.Description.String,
			TenantSlug:  plan.TenantSlug,
			Base: models.Base{
				ID:        id,
				CreatedAt: plan.CreatedAt,
				CreatedBy: plan.CreatedBy,
				UpdatedBy: plan.UpdatedBy,
				UpdatedAt: plan.UpdatedAt,
			},
		})
	}
```

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T16:45:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,54 @@
+package plans
+
+import (
+	"context"
+
+	"github.com/google/uuid"
+	"github.com/redcardinal-io/metering/domain/models"
+	"github.com/redcardinal-io/metering/domain/pkg/constants"
+	"github.com/redcardinal-io/metering/domain/pkg/pagination"
+	"github.com/redcardinal-io/metering/infrastructure/postgres"
+	"github.com/redcardinal-io/metering/infrastructure/postgres/gen"
+	"go.uber.org/zap"
+)
+
+func (p *PgPlanStoreRepository) ListPlans(ctx context.Context, page pagination.Pagination) (*pagination.PaginationView[models.Plan], error) {
+	tenantSlug := ctx.Value(constants.TenantSlugKey).(string)
+	m, err := p.q.ListPlansPaginated(ctx, gen.ListPlansPaginatedParams{
+		Limit:      int32(page.Limit),
+		Offset:     int32(page.GetOffset()),
+		TenantSlug: ctx.Value(constants.TenantSlugKey).(string),
+	})
path: infrastructure/postgres/store/plans/list.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Reuse the extracted tenant slug and fix error handling**

The code extracts tenant slug twice and doesn't handle potential type assertion errors.

```diff
-	tenantSlug := ctx.Value(constants.TenantSlugKey).(string)
+	tenantSlugValue := ctx.Value(constants.TenantSlugKey)
+	if tenantSlugValue == nil {
+		p.logger.Error("tenant slug not found in context")
+		return nil, postgres.ErrInvalidInput
+	}
+	tenantSlug, ok := tenantSlugValue.(string)
+	if !ok {
+		p.logger.Error("tenant slug is not a string")
+		return nil, postgres.ErrInvalidInput
+	}
 	m, err := p.q.ListPlansPaginated(ctx, gen.ListPlansPaginatedParams{
 		Limit:      int32(page.Limit),
 		Offset:     int32(page.GetOffset()),
-		TenantSlug: ctx.Value(constants.TenantSlugKey).(string),
+		TenantSlug: tenantSlug,
 	})
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
func (p *PgPlanStoreRepository) ListPlans(ctx context.Context, page pagination.Pagination) (*pagination.PaginationView[models.Plan], error) {
	tenantSlugValue := ctx.Value(constants.TenantSlugKey)
	if tenantSlugValue == nil {
		p.logger.Error("tenant slug not found in context")
		return nil, postgres.ErrInvalidInput
	}
	tenantSlug, ok := tenantSlugValue.(string)
	if !ok {
		p.logger.Error("tenant slug is not a string")
		return nil, postgres.ErrInvalidInput
	}
	m, err := p.q.ListPlansPaginated(ctx, gen.ListPlansPaginatedParams{
		Limit:      int32(page.Limit),
		Offset:     int32(page.GetOffset()),
		TenantSlug: tenantSlug,
	})
    // …
}
```

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T16:45:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,50 @@
+package plans
+
+import (
+	"context"
+
+	"github.com/google/uuid"
+	"github.com/jackc/pgx/v5/pgtype"
+	"github.com/redcardinal-io/metering/domain/models"
+	"github.com/redcardinal-io/metering/domain/pkg/constants"
+	"github.com/redcardinal-io/metering/infrastructure/postgres"
+	"github.com/redcardinal-io/metering/infrastructure/postgres/gen"
+)
+
+func (p *PgPlanStoreRepository) GetPlanByID(ctx context.Context, id string) (*models.Plan, error) {
+	tenantSlug := ctx.Value(constants.TenantSlugKey).(string)
+	// Try to parse as UUID first
path: infrastructure/postgres/store/plans/details.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling for the tenant slug context value.**

The current code assumes the tenant slug is always present in the context and may panic if it's missing or not a string.

```diff
-	tenantSlug := ctx.Value(constants.TenantSlugKey).(string)
+	tenantSlugValue := ctx.Value(constants.TenantSlugKey)
+	if tenantSlugValue == nil {
+		return nil, postgres.MapError(fmt.Errorf("tenant slug not found in context"), "Postgres.GetPlanByID")
+	}
+	tenantSlug, ok := tenantSlugValue.(string)
+	if !ok {
+		return nil, postgres.MapError(fmt.Errorf("tenant slug in context is not a string"), "Postgres.GetPlanByID")
+	}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
func (p *PgPlanStoreRepository) GetPlanByID(ctx context.Context, id string) (*models.Plan, error) {
	tenantSlugValue := ctx.Value(constants.TenantSlugKey)
	if tenantSlugValue == nil {
		return nil, postgres.MapError(fmt.Errorf("tenant slug not found in context"), "Postgres.GetPlanByID")
	}
	tenantSlug, ok := tenantSlugValue.(string)
	if !ok {
		return nil, postgres.MapError(fmt.Errorf("tenant slug in context is not a string"), "Postgres.GetPlanByID")
	}
	// Try to parse as UUID first
```

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T16:45:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,52 @@
+package plans
+
+import (
+	"context"
+
+	"github.com/google/uuid"
+	"github.com/jackc/pgx/v5/pgtype"
+	"github.com/redcardinal-io/metering/domain/models"
+	"github.com/redcardinal-io/metering/domain/pkg/constants"
+	"github.com/redcardinal-io/metering/infrastructure/postgres"
+	"github.com/redcardinal-io/metering/infrastructure/postgres/gen"
+)
+
+func (s *PgPlanStoreRepository) UpdatePlanByID(ctx context.Context, id string, arg models.UpdatePlanInput) (*models.Plan, error) {
+	tenantSlug := ctx.Value(constants.TenantSlugKey).(string)
+	// Try to parse as UUID first
path: infrastructure/postgres/store/plans/update.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling for the tenant slug context value.**

The current code assumes the tenant slug is always present in the context and may panic if it's missing or not a string.

```diff
-	tenantSlug := ctx.Value(constants.TenantSlugKey).(string)
+	tenantSlugValue := ctx.Value(constants.TenantSlugKey)
+	if tenantSlugValue == nil {
+		return nil, postgres.MapError(fmt.Errorf("tenant slug not found in context"), "Postgres.UpdatePlanByID")
+	}
+	tenantSlug, ok := tenantSlugValue.(string)
+	if !ok {
+		return nil, postgres.MapError(fmt.Errorf("tenant slug in context is not a string"), "Postgres.UpdatePlanByID")
+	}
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T16:45:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,50 @@
+package plans
+
+import (
+	"context"
+
+	"github.com/gofiber/fiber/v2"
+	domainerrors "github.com/redcardinal-io/metering/domain/errors"
+	"github.com/redcardinal-io/metering/domain/models"
+	"github.com/redcardinal-io/metering/domain/pkg/constants"
+	"go.uber.org/zap"
+)
+
+type createPlanRequest struct {
+	Name        string `json:"name" validate:"required"`
+	Description string `json:"description,omitempty"`
+	CreatedBy   string `json:"created_by" validate:"required"`
+}
+
+func (h *httpHandler) create(ctx *fiber.Ctx) error {
+	tenant_slug := ctx.Get(constants.TenantHeader)
path: interfaces/http/routes/v1/plans/create.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add validation for empty tenant slug**

The tenant slug is extracted from the header but there's no validation to ensure it's not empty before using it. This could lead to unexpected behavior if the header is missing.

```diff
	tenantSlug := ctx.Get(constants.TenantHeader)
+	if tenantSlug == "" {
+		errResp := domainerrors.NewErrorResponseWithOpts(
+			nil, 
+			domainerrors.EINVALID, 
+			"missing tenant identifier")
+		h.logger.Error("missing tenant identifier")
+		return ctx.Status(errResp.Status).JSON(errResp.ToJson())
+	}
```



> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T16:45:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,42 @@
+-- name: CreatePlan :one
+INSERT INTO plan (
+    name,
+    description,
+    tenant_slug,
+    created_by,
+    updated_by
+) VALUES (
+    $1, $2, $3, $4, $5
+) RETURNING *;
+
+-- name: GetPlanByID :one
+SELECT * FROM plan
+WHERE id = $1
+AND tenant_slug = $2;
+
+-- name: ListPlansPaginated :many
+SELECT * FROM plan
+WHERE tenant_slug = $1
+ORDER BY created_at DESC
+LIMIT $2
+OFFSET $3;
+
+-- name: DeletePlanByID :exec
+DELETE FROM plan
+WHERE id = $1
+AND tenant_slug = $2;
+
+-- name: CountPlans :one
+SELECT count(*) FROM plan
+WHERE tenant_slug = $1;
+
+-- name: UpdatePlanByID :one
+UPDATE plan
+SET name = coalesce(sqlc.narg('name'), name),
+    description = coalesce($1, description),
+    updated_by = $4
+WHERE id = $2
+AND tenant_slug = $3
+RETURNING *;
path: infrastructure/postgres/sql/queries/plan.sql
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Ensure parameter numbering in WHERE clause matches SET clause**

The parameter numbering in the WHERE clause needs to be adjusted to match the changes in the SET clause:

```diff
-WHERE id = $2
-AND tenant_slug = $3
+WHERE id = $1
+AND tenant_slug = $2
```

Or if using the alternative numbered parameters approach:

```diff
-WHERE id = $2
-AND tenant_slug = $3
+WHERE id = $4
+AND tenant_slug = $5
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T16:45:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,42 @@
+-- name: CreatePlan :one
+INSERT INTO plan (
+    name,
+    description,
+    tenant_slug,
+    created_by,
+    updated_by
+) VALUES (
+    $1, $2, $3, $4, $5
+) RETURNING *;
+
+-- name: GetPlanByID :one
+SELECT * FROM plan
+WHERE id = $1
+AND tenant_slug = $2;
+
+-- name: ListPlansPaginated :many
+SELECT * FROM plan
+WHERE tenant_slug = $1
+ORDER BY created_at DESC
+LIMIT $2
+OFFSET $3;
+
+-- name: DeletePlanByID :exec
+DELETE FROM plan
+WHERE id = $1
+AND tenant_slug = $2;
+
+-- name: CountPlans :one
+SELECT count(*) FROM plan
+WHERE tenant_slug = $1;
+
+-- name: UpdatePlanByID :one
+UPDATE plan
+SET name = coalesce(sqlc.narg('name'), name),
+    description = coalesce($1, description),
+    updated_by = $4
path: infrastructure/postgres/sql/queries/plan.sql
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix parameter inconsistency in UpdatePlanByID query**

There's an inconsistency in how parameters are defined. Line 35 uses `sqlc.narg('name')` while line 36 uses `$1` for description. The parameters should be consistent in style and properly numbered.

```diff
-SET name = coalesce(sqlc.narg('name'), name),
-    description = coalesce($1, description),
-    updated_by = $4
+SET name = coalesce(sqlc.narg('name'), name),
+    description = coalesce(sqlc.narg('description'), description),
+    updated_by = $3
```

Alternatively, if you prefer numbered parameters:

```diff
-SET name = coalesce(sqlc.narg('name'), name),
-    description = coalesce($1, description),
-    updated_by = $4
+SET name = coalesce($1, name),
+    description = coalesce($2, description),
+    updated_by = $3
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T16:45:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,69 @@
+package services
+
+import (
+	"context"
+
+	"github.com/redcardinal-io/metering/application/repositories"
+	"github.com/redcardinal-io/metering/domain/models"
+	"github.com/redcardinal-io/metering/domain/pkg/pagination"
+)
path: application/services/plan.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Consider adding tenant isolation checks.**

From the model definition in the snippets, I can see that plans have a TenantSlug field, but there's no explicit tenant isolation in the service methods. Consider extracting tenant information from the context and ensuring that operations are scoped to the correct tenant.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-14T16:45:32Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-14T16:40:31Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This change introduces a new "Plan" domain to the application, encompassing database schema and migrations, domain models, repository interfaces and implementations, service logic, HTTP route handlers, and API endpoint definitions for CRUD operations. All layers from storage to HTTP routing are implemented, enabling full lifecycle management of plan entities with multi-tenancy support.

## Changes

| File(s)                                                                                   | Change Summary                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
|-------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| application/repositories/store.go                                                         | Added the `PlanStoreRepository` interface for plan CRUD and listing operations.                                                                                                                                                                                                                                                                                                                                                                                                                        |
| application/services/plan.go                                                              | Introduced `PlanService` struct and methods for plan management, delegating to the repository.                                                                                                                                                                                                                                                                                                                                                                                                        |
| domain/models/plan.go                                                                     | Defined `Plan`, `CreatePlanInput`, and `UpdatePlanInput` structs for the plan domain model.                                                                                                                                                                                                                                                                                                                                                                                                           |
| infrastructure/postgres/sql/schema.sql<br>migrations/postgres/20250513202626_plan.go      | Added the `plan` table to the schema and provided migration scripts for creation and rollback, including index and timestamp management.                                                                                                                                                                                                                                                                                                                                                               |
| infrastructure/postgres/sql/queries/plan.sql                                              | Added SQL queries for creating, retrieving, listing, updating, and deleting plans, all scoped by tenant slug and supporting pagination.                                                                                                                                                                                                                                                                                                                                                               |
| infrastructure/postgres/store/plans/plans.go                                              | Implemented the `PgPlanStoreRepository` struct and constructor for PostgreSQL-backed plan storage.                                                                                                                                                                                                                                                                                                                                                                                                    |
| infrastructure/postgres/store/plans/create.go<br>delete.go<br>details.go<br>list.go<br>update.go | Added methods to `PgPlanStoreRepository` for creating, retrieving by ID, listing with pagination, updating, and deleting plans.                                                                                                                                                                                                                                                                                                                                                                       |
| interfaces/http/routes/v1/plans/routes.go                                                 | Added the `httpHandler` struct, constructor, and route registration for plan endpoints under `/plans`.                                                                                                                                                                                                                                                                                                                                                                                                |
| interfaces/http/routes/v1/plans/create.go<br>delete.go<br>details.go<br>list.go<br>update.go | Implemented HTTP handlers for creating, retrieving, updating, deleting, and listing plans, including request validation and error handling.                                                                                                                                                                                                                                                                                                                                                            |
| interfaces/http/server.go                                                                 | Integrated plan repository, service, and HTTP handler into the server setup and routing.                                                                                                                                                                                                                                                                                                                                                                                                               |
| bruno/plans/create.bru<br>delete.bru<br>get_by_id.bru<br>list.bru<br>update.bru           | Added Bruno API definitions for plan creation, deletion, retrieval by ID, listing, and updating endpoints with headers and payloads.                                                                                                                                                                                                                                                                                                                                                                  |
| bruno/plans/folder.bru                                                                    | Added a folder metadata file for organizing plan-related Bruno API definitions.                                                                                                                                                                                                                                                                                                                                                                                                                       |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Client
    participant HTTPHandler
    participant PlanService
    participant PlanRepository
    participant PostgresDB

    Client->>HTTPHandler: HTTP Request (Create/List/Get/Update/Delete Plan)
    HTTPHandler->>PlanService: Call corresponding method with context & input
    PlanService->>PlanRepository: Delegate CRUD/list operation
    PlanRepository->>PostgresDB: Execute SQL query (CRUD/list)
    PostgresDB-->>PlanRepository: Return result/error
    PlanRepository-->>PlanService: Return domain model/error
    PlanService-->>HTTPHandler: Return domain model/error
    HTTPHandler-->>Client: HTTP Response (JSON, status code)
```

## Suggested reviewers

- elliot14A

## Poem

> In fields of code where new plans grow,  
> A schema sprouts, and APIs flow.  
> CRUD hops along with every call,  
> Tenants safe behind each wall.  
> From models to routes, the journey’s grand—  
> A rabbit’s dream, well-planned and spanned!  
> 🐇✨

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> - CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> 
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 74bc89dba2c1b3c70f272dfdd8fa01804234193c and 9846f6d6e9cee68915c3f6fede2f83fc20856716.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `infrastructure/postgres/store/plans/create.go` (1 hunks)
* `infrastructure/postgres/store/plans/delete.go` (1 hunks)
* `infrastructure/postgres/store/plans/details.go` (1 hunks)
* `infrastructure/postgres/store/plans/list.go` (1 hunks)
* `infrastructure/postgres/store/plans/update.go` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (5)</summary>

* infrastructure/postgres/store/plans/delete.go
* infrastructure/postgres/store/plans/create.go
* infrastructure/postgres/store/plans/list.go
* infrastructure/postgres/store/plans/update.go
* infrastructure/postgres/store/plans/details.go

</details>

</details>
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Docstrings were successfully generated. (🔄  Check again to generate docstrings again)

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=redcardinal-io/metering&utm_content=24):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-05-15T02:30:19Z
body: > [!NOTE]
> Generated docstrings for this pull request at https://github.com/redcardinal-io/metering/pull/25
author_association: CONTRIBUTOR