mirror of
https://github.com/marcusDenslow/terminalShop.git
synced 2026-10-25 10:02:21 +01:00
153 lines
4.0 KiB
Go
153 lines
4.0 KiB
Go
package bring
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const baseURL = "https://api.bring.com/address"
|
|
|
|
// Client handles communication with the Bring Address API.
|
|
type Client struct {
|
|
apiUID string
|
|
apiKey string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// NewClient creates a new Bring API client.
|
|
func NewClient(apiUID, apiKey string) *Client {
|
|
return &Client{
|
|
apiUID: apiUID,
|
|
apiKey: apiKey,
|
|
httpClient: &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Address represents a Norwegian shipping address
|
|
type Address struct {
|
|
Name string
|
|
Street1 string
|
|
Street2 string
|
|
City string
|
|
Zip string
|
|
Country string
|
|
Phone string
|
|
}
|
|
|
|
type validationMatch struct {
|
|
StreetName string `json:"street_name"`
|
|
HouseNumber int `json:"house_number"`
|
|
Letter string `json:"letter"`
|
|
PostalCode string `json:"postal_code"`
|
|
City string `json:"city"`
|
|
Country string `json:"country"`
|
|
}
|
|
|
|
// validationReason explains why an address was invalid
|
|
type validationReason struct {
|
|
Code string `json:"code"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
// validationResponse is the top-level response from Bring's validation endpoint
|
|
type validationResponse struct {
|
|
Valid bool `json:"valid"`
|
|
Match *validationMatch `json:"match,omitempty"`
|
|
Reasons []validationReason `json:"reasons,omitempty"`
|
|
Suggestions []validationMatch `json:"suggestions,omitempty"`
|
|
}
|
|
|
|
// streetNumberRegex splits "Storgata 5" into name="Storgata" number="5"
|
|
// and "Storgata 5B" into name="Storgata" number="5" (letter handled by Bring).
|
|
var streetNumberRegex = regexp.MustCompile(`^(.+?)\s+(\d+)\s*[A-Za-z]?\s*$`)
|
|
|
|
// parseStreet splits a street line like "Storgata 5" into name and number.
|
|
// Returns the full string as name and empty number if not match.
|
|
func parseStreet(street string) (name string, number string) {
|
|
matches := streetNumberRegex.FindStringSubmatch(strings.TrimSpace(street))
|
|
if len(matches) >= 3 {
|
|
return matches[1], matches[2]
|
|
}
|
|
return strings.TrimSpace(street), ""
|
|
}
|
|
|
|
func (c *Client) ValidateAddress(addr Address) (*Address, error) {
|
|
streetName, streetNumber := parseStreet(addr.Street1)
|
|
|
|
params := url.Values{}
|
|
params.Set("address_type", "street")
|
|
params.Set("street_or_place", streetName)
|
|
params.Set("postal_code", addr.Zip)
|
|
params.Set("city", addr.City)
|
|
if streetNumber != "" {
|
|
params.Set("street_number", streetNumber)
|
|
}
|
|
|
|
reqURL := fmt.Sprintf("%s/api/no/validation?%s", baseURL, params.Encode())
|
|
|
|
req, err := http.NewRequest("GET", reqURL, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("X-Mybring-API-Uid", c.apiUID)
|
|
req.Header.Set("X-Mybring-API-Key", c.apiKey)
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("bring request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("bring returned status %d: %s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
var result validationResponse
|
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
|
}
|
|
|
|
if !result.Valid {
|
|
reasons := make([]string, 0, len(result.Reasons))
|
|
for _, r := range result.Reasons {
|
|
reasons = append(reasons, r.Description)
|
|
}
|
|
return nil, fmt.Errorf("invalid address: %s", strings.Join(reasons, "; "))
|
|
}
|
|
|
|
normalizedStreet := result.Match.StreetName
|
|
if result.Match.HouseNumber > 0 {
|
|
normalizedStreet = fmt.Sprintf("%s %d", result.Match.StreetName, result.Match.HouseNumber)
|
|
}
|
|
|
|
if result.Match.Letter != "" {
|
|
normalizedStreet += result.Match.Letter
|
|
}
|
|
|
|
validated := &Address{
|
|
Name: addr.Name,
|
|
Street1: normalizedStreet,
|
|
Street2: addr.Street2,
|
|
City: result.Match.City,
|
|
Zip: result.Match.PostalCode,
|
|
Country: addr.Country,
|
|
Phone: addr.Phone,
|
|
}
|
|
return validated, nil
|
|
}
|