aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--cost.go118
-rw-r--r--cost_test.go66
-rw-r--r--date.go39
-rw-r--r--email.go32
-rw-r--r--json.go15
-rw-r--r--lulu.go24
-rw-r--r--lulu_test.go8
-rw-r--r--notes9
-rw-r--r--order.go18
-rw-r--r--order_gen.go51
-rw-r--r--phone.go36
-rw-r--r--print.go193
-rw-r--r--print_test.go115
-rw-r--r--query.go76
-rw-r--r--ship.go263
-rw-r--r--ship_test.go39
-rw-r--r--testdata/getprintjobsresp.json119
-rw-r--r--testdata/shipaddrresp.json27
19 files changed, 1021 insertions, 229 deletions
diff --git a/Makefile b/Makefile
index 3f144ca..079f77d 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-GEN = $(addsuffix _gen.go, cover interior pkgid ship)
+GEN = $(addsuffix _gen.go, cover interior order pkgid ship)
TEST = $(wildcard *_test.go)
SRC = $(filter-out ${GEN} ${TEST}, $(wildcard *.go))
diff --git a/cost.go b/cost.go
index c0d340a..3d3c3dd 100644
--- a/cost.go
+++ b/cost.go
@@ -1,16 +1,6 @@
package lulu
-import (
- "encoding/json"
-
- "github.com/shopspring/decimal"
-)
-
-type printJobCostReq struct {
- LineItems []PrintJobCostLineItem `json:"line_items"`
- ShipAddr printJobCostReqShipAddr `json:"shipping_address"`
- ShipOpt ShippingLevel `json:"shipping_option"`
-}
+import "github.com/shopspring/decimal"
type PrintJobCostLineItem struct {
NPages uint `json:"page_count"`
@@ -18,32 +8,16 @@ type PrintJobCostLineItem struct {
Quantity uint `json:"quantity"`
}
-type printJobCostReqShipAddr struct {
- City string `json:"city"`
- Country string `json:"country_code"`
- PostCode string `json:"postcode"`
- State string `json:"state_code"`
- Street1 string `json:"street1"`
- Phone PhoneNumber `json:"phone_number"`
-}
-
-// PrintJobCost is the response from /print-job-cost-calculations/.
type PrintJobCost struct {
- Addr, SuggestedAddr ShippingAddress
- AddrWarnings []ShippingAddressWarning
- Fees []Fee
- LineItemCosts []LineItemCost
- ShipCost FulfillmentCost
- FulfillmentCost FulfillmentCost
- TotalTax, TotalCostExclTax, TotalCostInclTax, TotalDiscount decimal.Decimal
- Currency string
-}
-
-type ShippingAddressWarning struct {
- Type string `json:"type"` // eg "validation_warning"
- Path string `json:"path"` // eg "external"
- Code string `json:"code"` // eg "REPLACED"
- Msg string `json:"message"` // eg "street1: Holstenstr. 40 -> Holstenstraße 40"
+ Fees []Fee `json:"fees"`
+ LineItemCosts []LineItemCost `json:"line_item_costs"`
+ ShipCost FulfillmentCost `json:"shipping_cost"`
+ FulfillmentCost FulfillmentCost `json:"fulfillment_cost"`
+ TotalTax decimal.Decimal `json:"total_tax"`
+ TotalCostExclTax decimal.Decimal `json:"total_cost_excl_tax"`
+ TotalCostInclTax decimal.Decimal `json:"total_cost_incl_tax"`
+ TotalDiscount decimal.Decimal `json:"total_discount"`
+ Currency string `json:"currency"`
}
type Fee struct {
@@ -80,44 +54,44 @@ type Discount struct {
Description string `json:"description"`
}
+type printJobCostReq struct {
+ LineItems []PrintJobCostLineItem `json:"line_items"`
+ ShipAddr printJobCostReqShipAddr `json:"shipping_address"`
+ ShipOpt ShippingLevel `json:"shipping_option"`
+}
+
+type printJobCostReqShipAddr struct {
+ City string `json:"city"`
+ Country string `json:"country_code"`
+ PostCode string `json:"postcode"`
+ State string `json:"state_code"`
+ Street1 string `json:"street1"`
+ Phone PhoneNumber `json:"phone_number"`
+}
+
type printJobCostResp struct {
- Addr json.RawMessage `json:"shipping_address"`
- Fees []Fee `json:"fees"`
- LineItemCosts []LineItemCost `json:"line_item_costs"`
- ShipCost FulfillmentCost `json:"shipping_cost"`
- FulfillmentCost FulfillmentCost `json:"fulfillment_cost"`
- TotalTax decimal.Decimal `json:"total_tax"`
- TotalCostExclTax decimal.Decimal `json:"total_cost_excl_tax"`
- TotalCostInclTax decimal.Decimal `json:"total_cost_incl_tax"`
- TotalDiscount decimal.Decimal `json:"total_discount_amount"`
- Currency string `json:"currency"`
+ AddressValidation ShippingAddressValidation `json:"shipping_address"`
+ Fees []Fee `json:"fees"`
+ LineItemCosts []LineItemCost `json:"line_item_costs"`
+ ShipCost FulfillmentCost `json:"shipping_cost"`
+ FulfillmentCost FulfillmentCost `json:"fulfillment_cost"`
+ TotalTax decimal.Decimal `json:"total_tax"`
+ TotalCostExclTax decimal.Decimal `json:"total_cost_excl_tax"`
+ TotalCostInclTax decimal.Decimal `json:"total_cost_incl_tax"`
+ TotalDiscount decimal.Decimal `json:"total_discount_amount"`
+ Currency string `json:"currency"`
}
-func (c *PrintJobCost) UnmarshalJSON(data []byte) error {
- var resp printJobCostResp
- if err := json.Unmarshal(data, &resp); err != nil {
- return err
- }
- if err := json.Unmarshal(resp.Addr, &c.Addr); err != nil {
- return err
- }
- var warnsAndSugg struct {
- Warnings []ShippingAddressWarning `json:"warnings"`
- Suggested ShippingAddress `json:"suggested_address"`
- }
- if err := json.Unmarshal(resp.Addr, &warnsAndSugg); err != nil {
- return err
+func (resp printJobCostResp) cost() PrintJobCost {
+ return PrintJobCost{
+ Fees: resp.Fees,
+ LineItemCosts: resp.LineItemCosts,
+ ShipCost: resp.ShipCost,
+ FulfillmentCost: resp.FulfillmentCost,
+ TotalTax: resp.TotalTax,
+ TotalCostExclTax: resp.TotalCostExclTax,
+ TotalCostInclTax: resp.TotalCostInclTax,
+ TotalDiscount: resp.TotalDiscount,
+ Currency: resp.Currency,
}
- c.SuggestedAddr = warnsAndSugg.Suggested
- c.AddrWarnings = warnsAndSugg.Warnings
- c.Fees = resp.Fees
- c.LineItemCosts = resp.LineItemCosts
- c.ShipCost = resp.ShipCost
- c.FulfillmentCost = resp.FulfillmentCost
- c.TotalTax = resp.TotalTax
- c.TotalCostExclTax = resp.TotalCostExclTax
- c.TotalCostInclTax = resp.TotalCostInclTax
- c.TotalDiscount = resp.TotalDiscount
- c.Currency = resp.Currency
- return nil
}
diff --git a/cost_test.go b/cost_test.go
index 15b2fe1..446440d 100644
--- a/cost_test.go
+++ b/cost_test.go
@@ -40,31 +40,33 @@ var printJobCostReqSample = printJobCostReq{
Express,
}
-var printJobCostSample = PrintJobCost{
- Addr: ShippingAddress{
- City: "Lübeck",
- PostCode: "23552",
- Street1: "Holstenstr. 40",
- Phone: MustParsePhoneNumber("844-212-0689"),
- State: "",
- Country: "DE",
- IsBusiness: false,
- Name: ". .",
- },
- SuggestedAddr: ShippingAddress{
- Country: "DE",
- State: "",
- PostCode: "23552",
- City: "Lübeck",
- Street1: "Holstenstraße 40",
- Street2: "",
+var printJobCostRespSample = printJobCostResp{
+ AddressValidation: ShippingAddressValidation{
+ Address: ShippingAddress{
+ City: "Lübeck",
+ PostCode: "23552",
+ Street1: "Holstenstr. 40",
+ Phone: MustParsePhoneNumber("844-212-0689"),
+ State: "",
+ Country: "DE",
+ IsBusiness: false,
+ Name: ". .",
+ },
+ Suggested: ShippingAddress{
+ Country: "DE",
+ State: "",
+ PostCode: "23552",
+ City: "Lübeck",
+ Street1: "Holstenstraße 40",
+ Street2: "",
+ },
+ Warnings: []ShippingAddressWarning{{
+ "validation_warning",
+ "external",
+ "REPLACED",
+ "street1: Holstenstr. 40 -> Holstenstraße 40",
+ }},
},
- AddrWarnings: []ShippingAddressWarning{{
- "validation_warning",
- "external",
- "REPLACED",
- "street1: Holstenstr. 40 -> Holstenstraße 40",
- }},
Fees: []Fee{
{
Currency: "USD",
@@ -130,8 +132,8 @@ func TestMarshalPrintJobCostReq(t *testing.T) {
requireMarshalJsonEq(t, printJobCostReqJson, printJobCostReqSample)
}
-func TestUnmarshalPrintJobCost(t *testing.T) {
- requireUnmarshalJsonEq(t, printJobCostSample, printJobCostRespJson)
+func TestUnmarshalPrintJobCostResp(t *testing.T) {
+ requireUnmarshalJsonEq(t, printJobCostRespSample, printJobCostRespJson)
}
func TestPrintJobCost(t *testing.T) {
@@ -146,17 +148,9 @@ func TestPrintJobCost(t *testing.T) {
Phone: MustParsePhoneNumber("844-212-0689"),
}
shiplvl := printJobCostReqSample.ShipOpt
- cost, err := c.PrintJobCost(items, addr, shiplvl)
+ cost, av, err := c.PrintJobCost(items, addr, shiplvl)
require.NoError(t, err)
- require.Equal(t, "Lübeck", cost.Addr.City)
- require.Equal(t, "23552", cost.Addr.PostCode)
- require.Equal(t, "Holstenstr. 40", cost.Addr.Street1)
- require.Equal(t, MustParsePhoneNumber("844-212-0689"), cost.Addr.Phone)
- require.Empty(t, cost.Addr.State)
- require.Equal(t, "DE", cost.Addr.Country)
- require.False(t, cost.Addr.IsBusiness)
- require.Equal(t, printJobCostSample.SuggestedAddr, cost.SuggestedAddr)
- require.Equal(t, printJobCostSample.AddrWarnings, cost.AddrWarnings)
+ require.Equal(t, printJobCostRespSample.AddressValidation, av)
requireCurrency(t, cost.Currency)
for _, fee := range cost.Fees {
requireCurrency(t, fee.Currency)
diff --git a/date.go b/date.go
new file mode 100644
index 0000000..c82769f
--- /dev/null
+++ b/date.go
@@ -0,0 +1,39 @@
+package lulu
+
+import (
+ "fmt"
+ "time"
+)
+
+type Date time.Time
+
+func ParseDate(s string) (Date, error) {
+ t, err := time.Parse(time.DateOnly, s)
+ return Date(t), err
+}
+
+func MustParseDate(s string) Date {
+ d, err := ParseDate(s)
+ if err != nil {
+ panic(fmt.Sprintf("lulu.ParseDate(%q): %v", s, err))
+ }
+ return d
+}
+
+func (d Date) String() string {
+ return time.Time(d).Format(time.DateOnly)
+}
+
+func (d *Date) UnmarshalText(text []byte) error {
+ nd, err := ParseDate(string(text))
+ if err != nil {
+ return err
+ }
+ *d = nd
+ return nil
+}
+
+func (d Date) MarshalText() ([]byte, error) {
+ s := time.Time(d).Format(time.DateOnly)
+ return []byte(s), nil
+}
diff --git a/email.go b/email.go
new file mode 100644
index 0000000..bdf4dce
--- /dev/null
+++ b/email.go
@@ -0,0 +1,32 @@
+package lulu
+
+import (
+ "fmt"
+ "net/mail"
+)
+
+type EmailAddress struct {
+ *mail.Address
+}
+
+func (a *EmailAddress) UnmarshalText(text []byte) error {
+ addr, err := mail.ParseAddress(string(text))
+ if err != nil {
+ return err
+ }
+ a.Address = addr
+ return nil
+}
+
+func ParseEmailAddress(address string) (EmailAddress, error) {
+ addr, err := mail.ParseAddress(address)
+ return EmailAddress{addr}, err
+}
+
+func MustParseEmailAddress(s string) EmailAddress {
+ addr, err := mail.ParseAddress(s)
+ if err != nil {
+ panic(fmt.Sprintf("lulu: %v", err))
+ }
+ return EmailAddress{addr}
+}
diff --git a/json.go b/json.go
new file mode 100644
index 0000000..e200770
--- /dev/null
+++ b/json.go
@@ -0,0 +1,15 @@
+package lulu
+
+import "encoding/json"
+
+func unmarshalSliceOrVal[T any](data []byte) ([]T, error) {
+ var vs []T
+ if err := json.Unmarshal(data, &vs); err != nil {
+ var v T
+ if err := json.Unmarshal(data, &v); err != nil {
+ return nil, err
+ }
+ return []T{v}, nil
+ }
+ return vs, nil
+}
diff --git a/lulu.go b/lulu.go
index 302bfc5..a7a6cdd 100644
--- a/lulu.go
+++ b/lulu.go
@@ -24,6 +24,7 @@ const (
coverDimensionsPath = "/cover-dimensions"
validateCoverPath = "/validate-cover"
printJobCostPath = "/print-job-cost-calculations"
+ printJobPath = " /print-jobs"
)
// ApiUrl is the location of the API server. It is set to the sandbox
@@ -155,7 +156,7 @@ func (c *Client) GetCoverValidation(id uint) (CoverValidationRecord, error) {
// actually creating a print job.
//
// https://api.lulu.com/docs/#tag/Print-Job-Cost-Calculations/operation/Print-Job-cost-calculations_create
-func (c *Client) PrintJobCost(items []PrintJobCostLineItem, addr ShippingAddress, shipOpt ShippingLevel) (PrintJobCost, error) {
+func (c *Client) PrintJobCost(items []PrintJobCostLineItem, addr ShippingAddress, shipOpt ShippingLevel) (PrintJobCost, ShippingAddressValidation, error) {
reqAddr := printJobCostReqShipAddr{
City: addr.City,
Country: addr.Country,
@@ -165,12 +166,25 @@ func (c *Client) PrintJobCost(items []PrintJobCostLineItem, addr ShippingAddress
Phone: addr.Phone,
}
payload := printJobCostReq{items, reqAddr, shipOpt}
- var cost PrintJobCost
- err := c.postDecode(printJobCostPath, payload, http.StatusCreated, &cost)
+
+ var resp printJobCostResp
+ err := c.postDecode(printJobCostPath, payload, http.StatusCreated, &resp)
if err != nil {
- return cost, pkgErr(err)
+ return PrintJobCost{}, ShippingAddressValidation{}, pkgErr(err)
}
- return cost, nil
+ return resp.cost(), resp.AddressValidation, nil
+}
+
+// PrintJobs retrieves a list of all print jobs that have been created,
+// filtered by a set of optional query parameters.
+//
+// https://api.lulu.com/docs/#tag/Print-Jobs/operation/Print-Jobs_list
+func (c *Client) PrintJobs(queries ...PrintJobQuery) ([]PrintJob, error) {
+ //q := parsePrintJobQueries(queries)
+ //page := 1
+
+ // TODO
+ return nil, fmt.Errorf("not implemented")
}
// getDecode sends a GET request and unmarshals the response.
diff --git a/lulu_test.go b/lulu_test.go
index 648ede7..613cc4b 100644
--- a/lulu_test.go
+++ b/lulu_test.go
@@ -88,3 +88,11 @@ func poll(t *testing.T, f func() bool) {
}
}
}
+
+func mustParseTime(layout, value string) time.Time {
+ t, err := time.Parse(layout, value)
+ if err != nil {
+ panic(err)
+ }
+ return t
+}
diff --git a/notes b/notes
index 331cabe..ce427d0 100644
--- a/notes
+++ b/notes
@@ -9,3 +9,12 @@ real response did not; thus "fees" is included in the testdata. The
real response had "first_name" and "last_name" rather than just
"name", so ShippingAddress has a custom UnmarshalJSON() function to
account for both. And so on...
+
+
+-- Print Jobs --
+
+I made some corrections to testdata/getprintjobsresp.json. These
+include changing "source_md5sum" to "source_md5_sum" and changing the
+postcode field from integer to string, etc., as per the schema. It
+remains to be seen if the errors are just in the documentation example
+or if they (sometimes) appear in real server responses too...
diff --git a/order.go b/order.go
new file mode 100644
index 0000000..ec53071
--- /dev/null
+++ b/order.go
@@ -0,0 +1,18 @@
+package lulu
+
+//go:generate go run github.com/yawnak/string-enumer -t OrderStatus --text -o ./order_gen.go .
+
+type OrderStatus string
+
+const (
+ OrderCreated OrderStatus = "CREATED" // Print-Job created.
+ OrderUnpaid OrderStatus = "UNPAID" // Print-Job can be paid.
+ OrderPaymentInProgress OrderStatus = "PAYMENT_IN_PROGRESS" // Payment is in Progress.
+ OrderProductionDelayed OrderStatus = "PRODUCTION_DELAYED" // Print-Job is paid and will move to production after the mandatory production delay.
+ OrderProductionReady OrderStatus = "PRODUCTION_READY" // Production delay has ended and the Print-Job will move to "in production" shortly.
+ OrderInProduction OrderStatus = "IN_PRODUCTION" // Print-Job submitted to printer.
+ OrderShipped OrderStatus = "SHIPPED" // Print-Job is fully shipped.
+ OrderDelivered OrderStatus = "DELIVERED" // Print-Job has been delivered by the carrier. This is supported by USPS, FedEx, and UPS.
+ OrderRejected OrderStatus = "REJECTED" // When there is a problem with the input data or the file, Lulu will reject a Print-Job with a detailed error message. Please contact our experts if you need help in resolving this issue.
+ OrderCanceled OrderStatus = "CANCELED" // You can cancel a Print-Job as long as it is “unpaid” using an API request to the status endpoint. In rare cases, Lulu might also cancel a Print-Job if a problem has surfaced in production and the order cannot be fulfilled.
+)
diff --git a/order_gen.go b/order_gen.go
new file mode 100644
index 0000000..5b17f05
--- /dev/null
+++ b/order_gen.go
@@ -0,0 +1,51 @@
+// Code generated by "string-enumer -t OrderStatus --text -o ./order_gen.go ."; DO NOT EDIT.
+package lulu
+
+import (
+ "fmt"
+)
+
+// validOrderStatusValues contains a map of all valid OrderStatus values for easy lookup
+var validOrderStatusValues = map[OrderStatus]struct{}{
+ OrderCreated: {},
+ OrderUnpaid: {},
+ OrderPaymentInProgress: {},
+ OrderProductionDelayed: {},
+ OrderProductionReady: {},
+ OrderInProduction: {},
+ OrderShipped: {},
+ OrderDelivered: {},
+ OrderRejected: {},
+ OrderCanceled: {},
+}
+
+// Valid validates if a value is a valid OrderStatus
+func (v OrderStatus) Valid() bool {
+ _, ok := validOrderStatusValues[v]
+ return ok
+}
+
+// OrderStatusValues returns a list of all (valid) OrderStatus values
+func OrderStatusValues() []OrderStatus {
+ return []OrderStatus{
+ OrderCreated,
+ OrderUnpaid,
+ OrderPaymentInProgress,
+ OrderProductionDelayed,
+ OrderProductionReady,
+ OrderInProduction,
+ OrderShipped,
+ OrderDelivered,
+ OrderRejected,
+ OrderCanceled,
+ }
+}
+
+// UnmarshalText takes a text, verifies that it is a correct OrderStatus and unmarshals it
+func (v *OrderStatus) UnmarshalText(text []byte) error {
+ if valid := OrderStatus(text).Valid(); !valid {
+ return fmt.Errorf("not valid value for OrderStatus: %s", text)
+ }
+ *v = OrderStatus(text)
+ return nil
+}
diff --git a/phone.go b/phone.go
new file mode 100644
index 0000000..ff92cef
--- /dev/null
+++ b/phone.go
@@ -0,0 +1,36 @@
+package lulu
+
+import (
+ "fmt"
+ "regexp"
+)
+
+var phoneExpr = regexp.MustCompile(`^\+?[\d\s\-.\/()]{8,20}$`)
+
+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
+}
diff --git a/print.go b/print.go
new file mode 100644
index 0000000..4302e6e
--- /dev/null
+++ b/print.go
@@ -0,0 +1,193 @@
+package lulu
+
+import (
+ "encoding/json"
+ "fmt"
+ "time"
+)
+
+const (
+ MinProductionDelay = 60 * time.Minute
+ MaxProductionDelay = 48 * time.Hour
+)
+
+type getPrintJobsResp struct {
+ Count uint `json:"count"`
+ Next string `json:"next"`
+ Prev string `json:"previous"`
+ Results []PrintJob `json:"results"`
+}
+
+type PrintJob struct {
+ // An email address for questions regarding the print
+ // job—normally, you want to use the email address of a
+ // developer or shop owner, not the end customer.
+ Contact EmailAddress `json:"contact_email"`
+
+ Cost PrintJobCost `json:"costs"`
+
+ Created time.Time `json:"date_created"`
+ Modified time.Time `json:"date_modified"`
+
+ EstimatedShippingDates EstimatedShippingDates `json:"estimated_shipping_dates"`
+
+ // Arbitrary string to identify and connect a print job to your
+ // systems. Set it to an order number, a purchase order or
+ // whatever else works for your particular use case.
+ ExternalId string `json:"external_id"`
+
+ Id uint64 `json:"id"`
+
+ LineItems []LineItem `json:"line_items"`
+
+ OrderId string `json:"order_id"`
+
+ // Delay before a newly created Print-Job is sent to production.
+ // Minimum is 60 minutes, maximum is 2880 minutes (=48 hours). As
+ // most cancellation requests occur right after an order has been
+ // placed, it makes sense to wait for some time before sending an
+ // order to production. Once production has started, orders
+ // cannot be canceled anymore.
+ ProductionDelay time.Duration
+
+ // Target timestamp of when this job will move into production.
+ ProductionDue time.Time `json:"production_due_time"`
+
+ AddressValidation ShippingAddressValidation `json:"shipping_address"`
+
+ // The shipping level that this Print-Job is shipped with.
+ ShipOpt ShippingLevel `json:"shipping_level"`
+
+ // ISO 3166-1 alpha-2 country code of the tax country determined for this job
+ TaxCountry string `json:"tax_country"`
+}
+
+func (pj *PrintJob) UnmarshalJSON(data []byte) error {
+ type alias PrintJob // prevent infinite recursion
+ if err := json.Unmarshal(data, (*alias)(pj)); err != nil {
+ return err
+ }
+ var extra struct {
+ ProductionDelay uint `json:"production_delay"`
+ }
+ if err := json.Unmarshal(data, &extra); err != nil {
+ return fmt.Errorf("error unmarshaling %T.ProductionDelay: %w", *pj, err)
+ }
+ pj.ProductionDelay = time.Minute * time.Duration(extra.ProductionDelay)
+ return nil
+}
+
+type EstimatedShippingDates struct {
+ ArrivalMax Date `json:"arrival_max"`
+ ArrivalMin Date `json:"arrival_min"`
+ DispatchMax Date `json:"dispatch_max"`
+ DispatchMin Date `json:"dispatch_min"`
+}
+
+type LineItem struct {
+ // Arbitrary string to identify and connect a print job to your
+ // systems. Set it to an order number, a purchase order or
+ // whatever else works for your particular use case.
+ ExternalId string `json:"external_id"`
+
+ Id uint64 `json:"id"`
+
+ NPages uint `json:"page_count"`
+
+ // Manufacturing options.
+ Mfg PkgId `json:"pod_package_id"`
+
+ // Id of the printable. It can be used instead of PrintableNormalization.
+ PrintableId string `json:"printable_id"`
+
+ // Normalization process of the cover and interior source files.
+ PrintableNormalization PrintableNormalization `json:"printable_normalization"`
+
+ // Quantity of printed books.
+ Quantity uint `json:"quantity"`
+
+ Status LineItemStatus `json:"status"`
+
+ // Title of the line item. Should be on the cover.
+ Title string `json:"title"`
+
+ // Tracking id for this line item's shipment.
+ TrackingId string `json:"tracking_id"`
+
+ // A list of tracking urls for this line item's shipment.
+ TrackingUrls []string `json:"tracking_urls"`
+
+ // Name of the carrier handling the shipment.
+ Carrier string `json:"carrier_name"`
+}
+
+// PrintableNormalization represents the normalization processes of the
+// interior and cover source files.
+type PrintableNormalization struct {
+ Cover NormalizationJob `json:"cover"`
+ Interior NormalizationJob `json:"interior"`
+}
+
+// NormalizationJob represents the normalization process of an interior
+// or cover source file.
+type NormalizationJob struct {
+ JobId uint `json:"job_id"`
+ NormalizedFile NormalizedFile `json:"normalized_file"`
+ SrcMd5Sum string `json:"source_md5_sum"` // md5 hash of the source file.
+ SrcUrl string `json:"source_url"` // URL of the source file.
+}
+
+// NormalizedFile represents a file on the server that was created by
+// normalizing a cover or interior source file.
+type NormalizedFile struct {
+ Id uint64 `json:"file_id"`
+ Name string `json:"filename"`
+}
+
+type LineItemStatus struct {
+ Messages LineItemStatusMessages `json:"messages"`
+ Status OrderStatus `json:"name"`
+}
+
+type LineItemStatusMessages struct {
+ Delay time.Duration // Expected delay due to the error, if present.
+ Error string `json:"error"`
+ Info string `json:"info"`
+ PrintableNormalization PrintableNormalization `json:"printable_normalization"`
+ Timestamp time.Time `json:"timestamp"` // Timestamp of the last status change.
+ TrackingUrls []string
+ TrackingId string `json:"tracking_id"` // Tracking ID for this line item's shipment.
+ Carrier string `json:"carrier_name"` // Name of the carrier handling the shipment.
+}
+
+func (msgs *LineItemStatusMessages) UnmarshalJSON(data []byte) error {
+ type alias LineItemStatusMessages // prevent infinite recursion
+ if err := json.Unmarshal(data, (*alias)(msgs)); err != nil {
+ return err
+ }
+
+ var extra struct {
+ Delay string `json:"delay"`
+ TrackingUrls json.RawMessage `json:"tracking_urls"`
+ }
+ if err := json.Unmarshal(data, &extra); err != nil {
+ return err
+ }
+
+ var err error
+ if len(extra.Delay) > 0 {
+ msgs.Delay, err = time.ParseDuration(fmt.Sprintf("%sh", extra.Delay))
+ if err != nil {
+ return fmt.Errorf("error unmarshaling %T.Delay: %w", *msgs, err)
+ }
+ }
+
+ if len(extra.TrackingUrls) > 0 {
+ msgs.TrackingUrls, err = unmarshalSliceOrVal[string](extra.TrackingUrls)
+ if err != nil {
+ return fmt.Errorf("error unmarshaling %T.TrackingUrls: %w", *msgs, err)
+ }
+ }
+
+ return nil
+}
diff --git a/print_test.go b/print_test.go
new file mode 100644
index 0000000..9e51095
--- /dev/null
+++ b/print_test.go
@@ -0,0 +1,115 @@
+package lulu
+
+import (
+ _ "embed"
+ "testing"
+ "time"
+
+ "github.com/shopspring/decimal"
+)
+
+//go:embed testdata/getprintjobsresp.json
+var getPrintJobsRespJson string
+
+func TestUnmarshalGetPrintJobsResp(t *testing.T) {
+ want := getPrintJobsResp{
+ Count: 1,
+ Next: "https://api.lulu.com/resources/?page=1&page_size=1",
+ Prev: "https://api.lulu.com/resources/?page=1&page_size=1",
+ Results: []PrintJob{{
+ Contact: MustParseEmailAddress("test@test.com"),
+ Cost: PrintJobCost{
+ LineItemCosts: nil,
+ ShipCost: FulfillmentCost{
+ TotalCostExclTax: decimal.RequireFromString("132.74"),
+ TotalCostInclTax: decimal.RequireFromString("132.74"),
+ TotalTax: decimal.RequireFromString("0.00"),
+ TaxRate: decimal.RequireFromString("0.000000"),
+ },
+ FulfillmentCost: FulfillmentCost{
+ TotalCostExclTax: decimal.RequireFromString("0.75"),
+ TotalCostInclTax: decimal.RequireFromString("0.81"),
+ TotalTax: decimal.RequireFromString("0.06"),
+ TaxRate: decimal.RequireFromString("0.080000"),
+ },
+ Fees: []Fee{{
+ Currency: "USD",
+ Type: "HANDLING_FEE",
+ Sku: "HANDLING_FEE_6",
+ TaxRate: decimal.RequireFromString("0.088750"),
+ TotalCostExclTax: decimal.RequireFromString("4.00"),
+ TotalCostInclTax: decimal.RequireFromString("4.36"),
+ TotalTax: decimal.RequireFromString("0.36"),
+ }},
+ TotalCostExclTax: decimal.RequireFromString("123.45"),
+ TotalCostInclTax: decimal.RequireFromString("678.00"),
+ TotalTax: decimal.RequireFromString("0.123400"),
+ },
+ Created: mustParseTime(time.RFC3339, "2017-08-07T08:47:26.485456Z"),
+ Modified: mustParseTime(time.RFC3339, "2017-08-07T08:47:26.485490Z"),
+ EstimatedShippingDates: EstimatedShippingDates{
+ ArrivalMax: MustParseDate("2017-08-12"),
+ ArrivalMin: MustParseDate("2017-08-10"),
+ DispatchMax: MustParseDate("2017-08-09"),
+ DispatchMin: MustParseDate("2017-08-07"),
+ },
+ ExternalId: "demo-time",
+ Id: 1,
+ LineItems: []LineItem{{
+ ExternalId: "item-reference-1",
+ Id: 1,
+ PrintableId: "",
+ PrintableNormalization: PrintableNormalization{
+ Cover: NormalizationJob{
+ SrcMd5Sum: "e78512c777e7f5841fe8f1992cefb898",
+ SrcUrl: "https://www.dropbox.com/sh/p3zh22vzsaegiri/AADP367j0bTWlt8fCu-_tm2ia/161025/139056_cover.pdf?dl=1",
+ },
+ Interior: NormalizationJob{
+ SrcMd5Sum: "7f8af20c296747689756f8e310135d79",
+ SrcUrl: "https://www.dropbox.com/sh/p3zh22vzsaegiri/AACOUn3LFKsITDzylh13bQpsa/161025/thesis2.pdf?dl=1",
+ },
+ },
+ Quantity: 20,
+ Status: LineItemStatus{
+ Messages: LineItemStatusMessages{
+ Info: "Line-item is currently being validated",
+ },
+ Status: OrderCreated,
+ },
+ Title: "My Book",
+ }},
+ ProductionDelay: 120 * time.Minute,
+ ProductionDue: time.Time{},
+ AddressValidation: ShippingAddressValidation{
+ Address: ShippingAddress{
+ City: "Lübeck",
+ Country: "DE",
+ IsBusiness: false,
+ Name: "Hans Dampf",
+ Phone: "844-212-0689",
+ PostCode: "23552",
+ State: "",
+ Street1: "Holstenstr. 40",
+ Street2: "",
+ },
+ Suggested: ShippingAddress{
+ Country: "DE",
+ State: "",
+ PostCode: "23552",
+ City: "Lübeck",
+ Street1: "Holstenstraße 40",
+ Street2: "",
+ },
+ Warnings: []ShippingAddressWarning{{
+ Type: "validation_warning",
+ Path: "external",
+ Code: "REPLACED",
+ Msg: "street1: Holstenstr. 40 -> Holstenstraße 40",
+ }},
+ },
+ ShipOpt: Mail,
+ }},
+ }
+
+ requireUnmarshalJsonEq(t, want, getPrintJobsRespJson)
+}
diff --git a/query.go b/query.go
new file mode 100644
index 0000000..cf56bdd
--- /dev/null
+++ b/query.go
@@ -0,0 +1,76 @@
+package lulu
+
+import "time"
+
+type printJobQueries struct {
+ creatAfter, creatBefore, modAfter, modBefore *time.Time
+ status *OrderStatus
+ id, orderId *uint
+ excludeLineItems bool
+}
+
+func parsePrintJobQueries(qs []PrintJobQuery) printJobQueries {
+ var q printJobQueries
+ for i := range qs {
+ qs[i](&q)
+ }
+ return q
+}
+
+type PrintJobQuery func(*printJobQueries)
+
+// Include only print jobs created after t.
+func FilterCreatedAfter(t time.Time) PrintJobQuery {
+ return func(q *printJobQueries) {
+ q.creatAfter = &t
+ }
+}
+
+// Include only print jobs created before t.
+func FilterCreatedBefore(t time.Time) PrintJobQuery {
+ return func(q *printJobQueries) {
+ q.creatBefore = &t
+ }
+}
+
+// Include only print jobs modified after t.
+func FilterModifiedAfter(t time.Time) PrintJobQuery {
+ return func(q *printJobQueries) {
+ q.modAfter = &t
+ }
+}
+
+// Include only print jobs modified before t.
+func FilterModifiedBefore(t time.Time) PrintJobQuery {
+ return func(q *printJobQueries) {
+ q.modBefore = &t
+ }
+}
+
+// Include only print jobs with status s.
+func FilterStatus(s OrderStatus) PrintJobQuery {
+ return func(q *printJobQueries) {
+ q.status = &s
+ }
+}
+
+// Include only print jobs with the given id.
+func FilterId(id uint) PrintJobQuery {
+ return func(q *printJobQueries) {
+ q.id = &id
+ }
+}
+
+// Include only print jobs with the given order_id.
+func FilterOrderId(orderId uint) PrintJobQuery {
+ return func(q *printJobQueries) {
+ q.orderId = &orderId
+ }
+}
+
+// Leave the list of line items out of the print jobs response.
+func ExcludeLineItems() PrintJobQuery {
+ return func(q *printJobQueries) {
+ q.excludeLineItems = true
+ }
+}
diff --git a/ship.go b/ship.go
index d76661a..0cfd733 100644
--- a/ship.go
+++ b/ship.go
@@ -3,14 +3,10 @@ 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
@@ -22,99 +18,174 @@ const (
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.
-}
+type Title string
+
+const (
+ Mr Title = "MR"
+ Miss Title = "MISS"
+ Mrs Title = "MRS"
+ Ms Title = "MS"
+ Dr Title = "DR"
+)
-// 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"`
+type ShippingAddress struct {
+ Country string // ISO 3166-2 country code
+
+ // 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).
+ State string
+
+ City string
+ Street1 string // First address line
+ Street2 string // Second address line
+ PostCode string // Required for most countries
+
+ // Only relevant for US addresses. Some US carriers don't deliver
+ // to business-addresses on Saturday.
+ IsBusiness bool
+
+ // Full name of the person, including first and last name.
+ Name string
+ Title Title
+
+ // Name of an organization. Required if no person name is given.
+ Organization string
+
+ // 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.
+ Email EmailAddress
+
+ // 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.
+ Phone PhoneNumber
+
+ // The recipient's tax identification number. Required for
+ // shipping addresses to Brazil, Chile, and Mexico.
+ TaxId string
}
func (a *ShippingAddress) UnmarshalJSON(data []byte) error {
- s := string(data)
- var alias shippingAddress
+ var alias extShippingAddress
if err := json.Unmarshal(data, &alias); err != nil {
return err
}
+ addr, err := alias.addr()
+ if err != nil {
+ return err
+ }
+ *a = addr
+ return nil
+}
+
+type ShippingAddressValidation struct {
+ Address ShippingAddress // The original address that was given to the server.
+ Suggested ShippingAddress // The address that the server recommends.
+ Warnings []ShippingAddressWarning
+}
- 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)
+func (av *ShippingAddressValidation) UnmarshalJSON(data []byte) error {
+ var alias extShippingAddress
+ if err := json.Unmarshal(data, &alias); err != nil {
+ return err
}
+ addr, err := alias.addr()
+ if err != nil {
+ return err
+ }
+ av.Address = addr
+ av.Suggested = alias.Suggested
+ av.Warnings = alias.Warnings
+ return nil
+}
+
+type ShippingAddressWarning struct {
+ Type string `json:"type"` // eg "validation_warning"
+ Path string `json:"path"` // eg "external"
+ Code string `json:"code"` // eg "REPLACED"
+ Msg string `json:"message"` // eg "street1: Holstenstr. 40 -> Holstenstraße 40"
+}
+
+// extShippingAddress is the extended shipping address that is seen in
+// some responses. It includes "warnings" and "suggested_address". It
+// also unmarshals either "country" or "country_code" etc. to account for
+// the irregularities in the API.
+type extShippingAddress 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 EmailAddress `json:"email"`
+ Phone PhoneNumber `json:"phone_number"`
+ TaxId string `json:"recipient_tax_id"`
+
+ Warnings []ShippingAddressWarning `json:"warnings"`
+ Suggested ShippingAddress `json:"suggested_address"`
+}
- 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)
+// addr takes the disjunctive union of the API's irregular fields
+// ("country"/"country_code", etc.) and returns only those fields of the
+// actual shipping address: excluding the suggested address and warnings.
+// It returns error if two or more of the "same field" are present, eg.
+// both "country" and "country_code".
+func (ext extShippingAddress) addr() (ShippingAddress, error) {
+ country, both := either(ext.Country, ext.CountryCode)
+ if both {
+ return ShippingAddress{}, fmt.Errorf(`address contains both "country" and "country_code"`)
}
- a.City = alias.City
- a.Street1 = alias.Street1
- a.Street2 = alias.Street2
- a.PostCode = alias.PostCode
- a.IsBusiness = alias.IsBusiness
+ state, both := either(ext.State, ext.StateCode)
+ if both {
+ return ShippingAddress{}, fmt.Errorf(`address contains both "state" and "state_code"`)
+ }
- hasName := alias.Name != ""
- hasFirst := alias.FirstName != ""
- hasLast := alias.LastName != ""
+ var name string
+ hasName := ext.Name != ""
+ hasFirst := ext.FirstName != ""
+ hasLast := ext.LastName != ""
if hasName && (hasFirst || hasLast) {
- return fmt.Errorf(`address contains both "name" and {"first_name", "last_name"}: %q`, s)
+ return ShippingAddress{}, fmt.Errorf(`address contains both "name" and {"first_name", "last_name"}`)
} else if hasName {
- a.Name = alias.Name
+ name = ext.Name
} else if hasFirst && hasLast {
- a.Name = fmt.Sprintf("%s %s", alias.FirstName, alias.LastName)
+ name = fmt.Sprintf("%s %s", ext.FirstName, ext.LastName)
} else if hasLast {
- a.Name = alias.LastName
+ name = ext.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
+ name = ext.FirstName
}
- a.Phone = alias.Phone
- a.TaxId = alias.TaxId
-
- return nil
+ return ShippingAddress{
+ Country: country,
+ State: state,
+ City: ext.City,
+ Street1: ext.Street1,
+ Street2: ext.Street2,
+ PostCode: ext.PostCode,
+ IsBusiness: ext.IsBusiness,
+ Name: name,
+ Title: ext.Title,
+ Organization: ext.Organization,
+ Email: ext.Email,
+ Phone: ext.Phone,
+ TaxId: ext.TaxId,
+ }, nil
}
func either(a, b string) (string, bool) {
@@ -125,41 +196,3 @@ func either(a, b string) (string, bool) {
}
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
-}
diff --git a/ship_test.go b/ship_test.go
new file mode 100644
index 0000000..7643cb3
--- /dev/null
+++ b/ship_test.go
@@ -0,0 +1,39 @@
+package lulu
+
+import (
+ _ "embed"
+ "testing"
+)
+
+//go:embed testdata/shipaddrresp.json
+var shipAddrRespJson string
+
+func TestUnmarshalShippingAddressValidation(t *testing.T) {
+ want := ShippingAddressValidation{
+ Address: ShippingAddress{
+ City: "Lübeck",
+ Country: "DE",
+ IsBusiness: false,
+ Name: "Hans Dampf",
+ Phone: MustParsePhoneNumber("844-212-0689"),
+ PostCode: "23552",
+ State: "",
+ Street1: "Holstenstr. 40",
+ Street2: "",
+ },
+ Warnings: []ShippingAddressWarning{{
+ Type: "validation_warning",
+ Path: "external",
+ Code: "REPLACED",
+ Msg: "street1: Holstenstr. 40 -> Holstenstraße 40",
+ }},
+ Suggested: ShippingAddress{
+ Country: "DE",
+ State: "",
+ PostCode: "23552",
+ City: "Lübeck",
+ Street1: "Holstenstraße 40",
+ },
+ }
+ requireUnmarshalJsonEq(t, want, shipAddrRespJson)
+}
diff --git a/testdata/getprintjobsresp.json b/testdata/getprintjobsresp.json
new file mode 100644
index 0000000..3f43d2c
--- /dev/null
+++ b/testdata/getprintjobsresp.json
@@ -0,0 +1,119 @@
+{
+ "count": 1,
+ "next": "https://api.lulu.com/resources/?page=1&page_size=1",
+ "previous": "https://api.lulu.com/resources/?page=1&page_size=1",
+ "results": [
+ {
+ "contact_email": "test@test.com",
+ "costs": {
+ "line_item_costs": null,
+ "shipping_cost": {
+ "total_cost_excl_tax": "132.74",
+ "total_cost_incl_tax": "132.74",
+ "total_tax": "0.00",
+ "tax_rate": "0.000000"
+ },
+ "fulfillment_cost": {
+ "total_cost_excl_tax": "0.75",
+ "total_cost_incl_tax": "0.81",
+ "total_tax": "0.06",
+ "tax_rate": "0.080000"
+ },
+ "fees": [
+ {
+ "currency": "USD",
+ "fee_type": "HANDLING_FEE",
+ "sku": "HANDLING_FEE_6",
+ "tax_rate": "0.088750",
+ "total_cost_excl_tax": "4.00",
+ "total_cost_incl_tax": "4.36",
+ "total_tax": "0.36"
+ }
+ ],
+ "total_cost_excl_tax": "123.45",
+ "total_cost_incl_tax": "678.00",
+ "total_tax": "0.123400"
+ },
+ "date_created": "2017-08-07T08:47:26.485456Z",
+ "date_modified": "2017-08-07T08:47:26.485490Z",
+ "estimated_shipping_dates": {
+ "arrival_max": "2017-08-12",
+ "arrival_min": "2017-08-10",
+ "dispatch_max": "2017-08-09",
+ "dispatch_min": "2017-08-07"
+ },
+ "external_id": "demo-time",
+ "id": 1,
+ "line_items": [
+ {
+ "external_id": "item-reference-1",
+ "id": 1,
+ "printable_id": null,
+ "printable_normalization": {
+ "cover": {
+ "job_id": null,
+ "normalized_file": null,
+ "page_count": null,
+ "source_file": null,
+ "source_md5_sum": "e78512c777e7f5841fe8f1992cefb898",
+ "source_url": "https://www.dropbox.com/sh/p3zh22vzsaegiri/AADP367j0bTWlt8fCu-_tm2ia/161025/139056_cover.pdf?dl=1"
+ },
+ "interior": {
+ "job_id": null,
+ "normalized_file": null,
+ "page_count": null,
+ "source_file": null,
+ "source_md5_sum": "7f8af20c296747689756f8e310135d79",
+ "source_url": "https://www.dropbox.com/sh/p3zh22vzsaegiri/AACOUn3LFKsITDzylh13bQpsa/161025/thesis2.pdf?dl=1"
+ },
+ "pod_package_id": "0600X0900.BW.STD.PB.060UW444.MXX"
+ },
+ "quantity": 20,
+ "status": {
+ "messages": {
+ "info": "Line-item is currently being validated"
+ },
+ "name": "CREATED"
+ },
+ "title": "My Book"
+ }
+ ],
+ "production_delay": 120,
+ "production_due_time": null,
+ "shipping_address": {
+ "city": "Lübeck",
+ "country_code": "DE",
+ "is_business": false,
+ "name": "Hans Dampf",
+ "phone_number": "844-212-0689",
+ "postcode": "23552",
+ "state_code": "",
+ "street1": "Holstenstr. 40",
+ "street2": "",
+ "warnings": [
+ {
+ "type": "validation_warning",
+ "path": "external",
+ "code": "REPLACED",
+ "message": "street1: Holstenstr. 40 -> Holstenstraße 40"
+ }
+ ],
+ "suggested_address": {
+ "country_code": "DE",
+ "state_code": null,
+ "postcode": "23552",
+ "city": "Lübeck",
+ "street1": "Holstenstraße 40",
+ "street2": null
+ }
+ },
+ "shipping_level": "MAIL",
+ "shipping_option_level": "MAIL",
+ "status": {
+ "changed": "2017-08-07T08:47:26.480493Z",
+ "message": "Print-job is currently being validated",
+ "name": "CREATED"
+ }
+ }
+ ]
+}
diff --git a/testdata/shipaddrresp.json b/testdata/shipaddrresp.json
new file mode 100644
index 0000000..510831d
--- /dev/null
+++ b/testdata/shipaddrresp.json
@@ -0,0 +1,27 @@
+{
+ "city": "Lübeck",
+ "country_code": "DE",
+ "is_business": false,
+ "name": "Hans Dampf",
+ "phone_number": "844-212-0689",
+ "postcode": "23552",
+ "state_code": "",
+ "street1": "Holstenstr. 40",
+ "street2": "",
+ "warnings": [
+ {
+ "type": "validation_warning",
+ "path": "external",
+ "code": "REPLACED",
+ "message": "street1: Holstenstr. 40 -> Holstenstraße 40"
+ }
+ ],
+ "suggested_address": {
+ "country_code": "DE",
+ "state_code": null,
+ "postcode": "23552",
+ "city": "Lübeck",
+ "street1": "Holstenstraße 40",
+ "street2": null
+ }
+}