Files
geo/server/internal/tenant/repository/image_usage_repo.go
T
root 63667ed2d1 feat(membership): enforce tenant subscription plans with blocked UI
Add configurable membership plans (free/plus/pro) synced to DB on boot, a
subscription guard middleware that blocks tenant endpoints on expired or
missing plans, and a MembershipBlockedView that surfaces the reason so
the admin can contact the tenant owner. Quota and brand-library reads now
honor the active plan's policy JSON and expired subscriptions.
2026-04-18 20:56:05 +08:00

86 lines
2.3 KiB
Go

package repository
import (
"context"
"fmt"
"time"
"github.com/geo-platform/tenant-api/internal/tenant/repository/generated"
)
type ImageStorageUsage struct {
UsedBytes int64
UpdatedAt time.Time
}
type QuotaReserveResult struct {
UsedBytes int64
QuotaBytes int64
}
type ImageUsageRepository interface {
EnsureRow(ctx context.Context, tenantID int64) error
Get(ctx context.Context, tenantID int64) (*ImageStorageUsage, error)
Release(ctx context.Context, tenantID, releaseBytes int64) error
AtomicReserve(ctx context.Context, tenantID, sizeBytes int64) (*QuotaReserveResult, error)
}
type imageUsageRepository struct {
q generated.Querier
db generated.DBTX
}
func NewImageUsageRepository(db generated.DBTX) ImageUsageRepository {
return &imageUsageRepository{q: newQuerier(db), db: db}
}
func (r *imageUsageRepository) EnsureRow(ctx context.Context, tenantID int64) error {
return r.q.EnsureImageStorageUsageRow(ctx, tenantID)
}
func (r *imageUsageRepository) Get(ctx context.Context, tenantID int64) (*ImageStorageUsage, error) {
row, err := r.q.GetImageStorageUsage(ctx, tenantID)
if err != nil {
return nil, err
}
return &ImageStorageUsage{
UsedBytes: row.UsedBytes,
UpdatedAt: timeFromTimestamp(row.UpdatedAt),
}, nil
}
func (r *imageUsageRepository) Release(ctx context.Context, tenantID, releaseBytes int64) error {
return r.q.ReleaseImageStorageQuota(ctx, generated.ReleaseImageStorageQuotaParams{
ReleaseBytes: releaseBytes,
TenantID: tenantID,
})
}
func (r *imageUsageRepository) AtomicReserve(ctx context.Context, tenantID, sizeBytes int64) (*QuotaReserveResult, error) {
row := r.db.QueryRow(ctx, `
WITH plan AS (
SELECT COALESCE((p.quota_policy_json ->> 'image_storage_bytes')::BIGINT, 104857600) AS quota_bytes
FROM tenant_plan_subscriptions s
JOIN plans p ON p.id = s.plan_id
WHERE s.tenant_id = $1
AND s.status = 'active'
AND s.deleted_at IS NULL
AND s.end_at > NOW()
LIMIT 1
)
UPDATE tenant_image_storage_usage u
SET used_bytes = u.used_bytes + $2,
updated_at = NOW()
FROM plan
WHERE u.tenant_id = $1
AND u.used_bytes + $2 <= plan.quota_bytes
RETURNING u.used_bytes, plan.quota_bytes
`, tenantID, sizeBytes)
var result QuotaReserveResult
if err := row.Scan(&result.UsedBytes, &result.QuotaBytes); err != nil {
return nil, fmt.Errorf("reserve image storage quota: %w", err)
}
return &result, nil
}