// Package notifyclient ist ein schlanker HTTP-Client für Core CFG-05 // (internal/notifyapi, POST /notify/enqueue). Archive ist ein physisch // getrenntes Go-Modul und kann Core CFG-02/CFG-04 daher nicht direkt // importieren — nur über die HTTP-Schnittstelle von CFG-05 aufrufen. // Gleiches Muster wie archive/internal/rbacclient (RET-08) für RBAC-06. package notifyclient import ( "bytes" "context" "encoding/json" "fmt" "net/http" ) type Client struct { BaseURL string ServiceToken string HTTPClient *http.Client } func New(baseURL, serviceToken string) *Client { return &Client{BaseURL: baseURL, ServiceToken: serviceToken, HTTPClient: http.DefaultClient} } type enqueueRequest struct { TenantSlug string `json:"tenant_slug"` UserID string `json:"user_id"` EventType string `json:"event_type"` Channel string `json:"channel"` Recipient string `json:"recipient"` Payload map[string]any `json:"payload"` } // EnqueueResult spiegelt CFG-05s enqueueResponse. type EnqueueResult struct { JobID string Skipped bool } // Enqueue löst ein Benachrichtigungs-Ereignis über CFG-05 aus. Jeder // Fehler (Transport, Timeout, unerwarteter Status) wird als Fehler // zurückgegeben — der Aufrufer entscheidet, ob/wie er das protokolliert // (Pflichtprüfung RET-07: fehlgeschlagener Versand wird protokolliert, // nicht stillschweigend verworfen). func (c *Client) Enqueue(ctx context.Context, tenantSlug, userID, eventType, channel, recipient string, payload map[string]any) (EnqueueResult, error) { body, err := json.Marshal(enqueueRequest{ TenantSlug: tenantSlug, UserID: userID, EventType: eventType, Channel: channel, Recipient: recipient, Payload: payload, }) if err != nil { return EnqueueResult{}, fmt.Errorf("notifyclient: request kodieren: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/notify/enqueue", bytes.NewReader(body)) if err != nil { return EnqueueResult{}, fmt.Errorf("notifyclient: request bauen: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Service-Token", c.ServiceToken) resp, err := c.httpClient().Do(req) if err != nil { return EnqueueResult{}, fmt.Errorf("notifyclient: aufruf fehlgeschlagen: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return EnqueueResult{}, fmt.Errorf("notifyclient: unerwarteter status %d", resp.StatusCode) } var out struct { JobID string `json:"job_id"` Skipped bool `json:"skipped"` } if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { return EnqueueResult{}, fmt.Errorf("notifyclient: antwort dekodieren: %w", err) } return EnqueueResult{JobID: out.JobID, Skipped: out.Skipped}, nil } func (c *Client) httpClient() *http.Client { if c.HTTPClient != nil { return c.HTTPClient } return http.DefaultClient }