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 } }