feat(tenant): per-tenant brand and question limit overrides

Add nullable brand_limit / question_limit columns on tenants so ops
admins can override the plan-derived brand-library quotas per account.
When set, these take precedence over config.yml plan defaults across the
brand-library summary, question materialization, and the daily
monitoring worker's primary-client plan resolution.

Ops side exposes them on admin-user create plus a new
PUT /admin-users/:id/limits endpoint, invalidating the brand-library
summary cache on change. The ops-web AdminUsers view gains matching
inputs on the create and edit modals.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-24 09:04:06 +08:00
parent d69510274c
commit 348f2e3451
16 changed files with 413 additions and 63 deletions
@@ -0,0 +1,39 @@
package app
import (
"testing"
"github.com/geo-platform/tenant-api/internal/shared/response"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestValidateOptionalAdminUserLimit(t *testing.T) {
positive := 3
zero := 0
negative := -1
tests := []struct {
name string
value *int
wantErr bool
}{
{name: "null inherits config", value: nil},
{name: "positive override", value: &positive},
{name: "zero rejected", value: &zero, wantErr: true},
{name: "negative rejected", value: &negative, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateOptionalAdminUserLimit("brand_limit", tt.value)
if !tt.wantErr {
require.NoError(t, err)
return
}
var appErr *response.AppError
require.ErrorAs(t, err, &appErr)
assert.Equal(t, "invalid_limit", appErr.Message)
})
}
}