aboutsummaryrefslogtreecommitdiffstats
path: root/ship.go
blob: d76661a8f34e6ef71a2be5401b2c7b7f41619ce7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package lulu

import (
	"encoding/json"
	"fmt"
	email "net/mail"
	"regexp"
)

//go:generate go run github.com/yawnak/string-enumer -t ShippingLevel -t Title --text -o ./ship_gen.go .

var phoneExpr = regexp.MustCompile(`^\+?[\d\s\-.\/()]{8,20}$`)

// ShippingLevel is the quality/speed with which a package is shipped.
type ShippingLevel string

const (
	Mail         ShippingLevel = "MAIL"          // Slowest ship method. Depending on the destination, tracking might not be available.
	PriorityMail ShippingLevel = "PRIORITY_MAIL" // Priority mail shipping
	Ground       ShippingLevel = "GROUND"        // Courier based shipping using ground transportation in the US.
	Expedited    ShippingLevel = "EXPEDITED"     // Expedited (2nd day) delivery via air mail or equivalent.
	Express      ShippingLevel = "EXPRESS"       // Overnight delivery. Fastest shipping available.
)

type ShippingAddress struct {
	Country      string // ISO 3166-2 country code
	State        string // 2 or 3 letter state codes (officially called ISO-3166-2 subdivision codes). They are required for some countries (e.g. US, MX, CA, AU)
	City         string
	Street1      string // First address line
	Street2      string // Second address line
	PostCode     string // Required for most countries
	IsBusiness   bool   // Only relevant for US addresses. Some US carriers don't deliver to business-addresses on Saturday.
	Name         string // Full name of the person, including first and last name.
	Title        Title
	Organization string         // Name of an organization. Required if no person name is given.
	Email        *email.Address // Shipping carriers require an email address for notifications or handling delivery issues. If no email is given, the default email in the user profile will be used.
	Phone        PhoneNumber    // Shipping carriers require a phone number for handling delivery issues. If no phone number is given, the default in the API user profile will be used.
	TaxId        string         // The recipient’s tax identification number. Required for shipping addresses to Brazil, Chile, and Mexico.
}

// They use "country" in some places, "country_code" in others, etc.
// This is the union of all of them.
type shippingAddress struct {
	Country      string      `json:"country"`
	CountryCode  string      `json:"country_code"`
	State        string      `json:"state"`
	StateCode    string      `json:"state_code"`
	City         string      `json:"city"`
	Street1      string      `json:"street1"`
	Street2      string      `json:"street2"`
	PostCode     string      `json:"postcode"`
	IsBusiness   bool        `json:"is_business"`
	Name         string      `json:"name"`
	FirstName    string      `json:"first_name"`
	LastName     string      `json:"last_name"`
	Title        Title       `json:"title"`
	Organization string      `json:"organization"`
	Email        string      `json:"email"`
	Phone        PhoneNumber `json:"phone_number"`
	TaxId        string      `json:"recipient_tax_id"`
}

func (a *ShippingAddress) UnmarshalJSON(data []byte) error {
	s := string(data)
	var alias shippingAddress
	if err := json.Unmarshal(data, &alias); err != nil {
		return err
	}

	if country, both := either(alias.Country, alias.CountryCode); !both {
		a.Country = country
	} else {
		return fmt.Errorf(`address contains both "country" and "country_code": %q`, s)
	}

	if state, both := either(alias.State, alias.StateCode); !both {
		a.State = state
	} else {
		return fmt.Errorf(`address contains both "state" and "state_code": %q`, s)
	}

	a.City = alias.City
	a.Street1 = alias.Street1
	a.Street2 = alias.Street2
	a.PostCode = alias.PostCode
	a.IsBusiness = alias.IsBusiness

	hasName := alias.Name != ""
	hasFirst := alias.FirstName != ""
	hasLast := alias.LastName != ""
	if hasName && (hasFirst || hasLast) {
		return fmt.Errorf(`address contains both "name" and {"first_name", "last_name"}: %q`, s)
	} else if hasName {
		a.Name = alias.Name
	} else if hasFirst && hasLast {
		a.Name = fmt.Sprintf("%s %s", alias.FirstName, alias.LastName)
	} else if hasLast {
		a.Name = alias.LastName
	} else if hasFirst {
		a.Name = alias.FirstName
	}

	a.Title = alias.Title
	a.Organization = alias.Organization

	if alias.Email != "" {
		addr, err := email.ParseAddress(alias.Email)
		if err != nil {
			return err
		}
		a.Email = addr
	}

	a.Phone = alias.Phone
	a.TaxId = alias.TaxId

	return nil
}

func either(a, b string) (string, bool) {
	if a != "" && b != "" {
		return a, true
	} else if a != "" {
		return a, false
	}
	return b, false
}

type Title string

const (
	Mr   Title = "MR"
	Miss Title = "MISS"
	Mrs  Title = "MRS"
	Ms   Title = "MS"
	Dr   Title = "DR"
)

type PhoneNumber string

func ParsePhoneNumber(s string) (PhoneNumber, error) {
	if phoneExpr.MatchString(s) {
		return PhoneNumber(s), nil
	}
	return "", fmt.Errorf("malformed phone number %q; must fit pattern `%s`", s, phoneExpr.String())
}

// MustParsePhoneNumber is like ParsePhoneNumber but panics if the phone
// number cannot be parsed.
func MustParsePhoneNumber(s string) PhoneNumber {
	n, err := ParsePhoneNumber(s)
	if err != nil {
		panic(fmt.Sprintf("lulu: ParsePhoneNumber(%q): %v", s, err))
	}
	return n
}

func (n *PhoneNumber) UnmarshalText(text []byte) error {
	pn, err := ParsePhoneNumber(string(text))
	if err != nil {
		return err
	}
	*n = pn
	return nil
}