Files
go-cart-actor/pkg/order/email_preferences.go
T
mats 83986e7a35
Build and Publish / BuildAndDeployArm64 (push) Failing after 6s
Build and Publish / BuildAndDeployAmd64 (push) Failing after 3s
actor
2026-07-01 10:47:28 +02:00

108 lines
3.5 KiB
Go

package order
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// ProfilePreferencesClient is the subset of the profile service's UCP customer
// API that returns email preferences for a given email address.
type ProfilePreferencesClient struct {
baseURL string
httpClient *http.Client
timeout time.Duration
}
// NewProfilePreferencesClient creates a client that talks to the profile
// service's UCP customer endpoint at baseURL (e.g. "http://profile:8080/ucp/v1/customers").
func NewProfilePreferencesClient(baseURL string) *ProfilePreferencesClient {
return &ProfilePreferencesClient{
baseURL: strings.TrimRight(baseURL, "/"),
httpClient: &http.Client{Timeout: 5 * time.Second},
timeout: 5 * time.Second,
}
}
// emailPreferencesResponse mirrors ucp.EmailPreferencesResponse for decoding.
type emailPreferencesResponse struct {
OrderEmails *bool `json:"orderEmails,omitempty"`
MarketingEmails *bool `json:"marketingEmails,omitempty"`
}
// customerLookupResponse mirrors the UCP customer response, but we only need
// email preferences.
type customerLookupResponse struct {
EmailPreferences *emailPreferencesResponse `json:"emailPreferences,omitempty"`
}
// fetchPreferences calls GET /by-email/{email} on the profile service and
// returns the parsed email preferences. Returns nil, nil when the profile
// is not found (unknown email -> allow all).
func (c *ProfilePreferencesClient) fetchPreferences(ctx context.Context, email string) (*emailPreferencesResponse, error) {
url := fmt.Sprintf("%s/by-email/%s", c.baseURL, url.PathEscape(email))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("profile prefs: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, nil // unknown email -> no preferences
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("profile prefs: unexpected status %d: %s", resp.StatusCode, string(body))
}
var data customerLookupResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return nil, fmt.Errorf("profile prefs: decode: %w", err)
}
return data.EmailPreferences, nil
}
// PreferencesChecker wraps ProfilePreferencesClient to implement
// EmailPreferenceChecker.
type PreferencesChecker struct {
client *ProfilePreferencesClient
}
// NewPreferencesChecker creates an EmailPreferenceChecker that looks up
// preferences from the profile service at the given base URL.
func NewPreferencesChecker(profileServiceURL string) *PreferencesChecker {
return &PreferencesChecker{
client: NewProfilePreferencesClient(profileServiceURL),
}
}
// Allowed returns true when the customer has not opted out of the given
// category. Categories are "order" (order confirmations, shipping updates,
// receipts) and "marketing" (promotional offers, newsletters). Returns true
// when preferences are unknown (profile not found) — the safe default.
func (c *PreferencesChecker) Allowed(ctx context.Context, email string, category string) bool {
prefs, err := c.client.fetchPreferences(ctx, email)
if err != nil || prefs == nil {
return true // safe default: allow when unknown
}
switch category {
case "order":
return prefs.OrderEmails == nil || *prefs.OrderEmails
case "marketing":
return prefs.MarketingEmails == nil || *prefs.MarketingEmails
default:
return true // unknown category -> allow
}
}