// Package identityclient calls identity-svc's internal endpoints for render-charge // consume/refund (service-to-service, shared service token). package identityclient import ( "bytes" "context" "encoding/json" "fmt" "net/http" "time" "github.com/google/uuid" ) type Client struct { baseURL string token string http *http.Client } func New(baseURL, token string) *Client { return &Client{baseURL: baseURL, token: token, http: &http.Client{Timeout: 8 * time.Second}} } func (c *Client) configured() bool { return c.baseURL != "" } type consumeResp struct { Allowed bool `json:"allowed"` Remaining int `json:"remaining"` } func (c *Client) post(ctx context.Context, path string, userID uuid.UUID) (*http.Response, error) { body, _ := json.Marshal(map[string]string{"user_id": userID.String()}) req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Service-Token", c.token) return c.http.Do(req) } // Consume decrements the user's daily render count. Returns whether the render is // allowed. Fails OPEN (allowed=true) if identity is unreachable, to avoid blocking // renders on a transient outage. func (c *Client) Consume(ctx context.Context, userID uuid.UUID) (bool, error) { if !c.configured() { return true, nil } resp, err := c.post(ctx, "/v1/internal/render-charge/consume", userID) if err != nil { return true, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return true, fmt.Errorf("consume status %d", resp.StatusCode) } var r consumeResp if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { return true, err } return r.Allowed, nil } // Refund returns one render to the user's daily count (admin-stop only). func (c *Client) Refund(ctx context.Context, userID uuid.UUID) error { if !c.configured() { return nil } resp, err := c.post(ctx, "/v1/internal/render-charge/refund", userID) if err != nil { return err } resp.Body.Close() return nil }