63667ed2d1
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.
86 lines
2.3 KiB
Go
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
|
|
}
|