aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--cost.go165
-rw-r--r--cost_test.go247
-rw-r--r--go.mod8
-rw-r--r--go.sum3
-rw-r--r--lulu.go3
-rw-r--r--print.go14
-rw-r--r--ship.go13
-rw-r--r--ship_test.go51
8 files changed, 431 insertions, 73 deletions
diff --git a/cost.go b/cost.go
new file mode 100644
index 0000000..202fcde
--- /dev/null
+++ b/cost.go
@@ -0,0 +1,165 @@
+package lulu
+
+// TODO: should we remove the redundant tax/subtotal fields or remain
+// true to their retarded api?
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/shopspring/decimal"
+)
+
+// printJobCostReq is the json body of a /print-job-cost-calculations/ request.
+type printJobCostReq struct {
+ LineItems []printJobCostLineItem `json:"line_items"`
+ ShipAddr ShippingAddress `json:"shipping_address"`
+ ShipOpt ShippingLevel `json:"shipping_option"`
+}
+
+type printJobCostLineItem struct {
+ NPages uint `json:"page_count"`
+ Mfg PkgId `json:"pod_package_id"`
+ Quantity uint `json:"quantity"`
+}
+
+// PrintJobCost is the response from /print-job-cost-calculations/.
+type PrintJobCost struct {
+ Addr, SuggestedAddr ShippingAddress
+ AddrWarning ShippingAddressWarning
+ Currency string
+ Fees []Fee
+ FulfillmentCost FulfillmentCost
+ LineItemCosts []LineItemCost
+ ShipCost FulfillmentCost
+ TotalCostExclTax, TotalCostInclTax, TotalDiscount, TotalTax decimal.Decimal
+}
+
+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"
+}
+
+type Fee struct {
+ Currency string `json:"currency"`
+ Type string `json:"fee_type"`
+ Sku string `json:"sku"`
+ TaxRate decimal.Decimal `json:"tax_rate"`
+ TotalCostExclTax decimal.Decimal `json:"total_cost_excl_tax"`
+ TotalCostInclTax decimal.Decimal `json:"total_cost_incl_tax"`
+ TotalTax decimal.Decimal `json:"total_tax"`
+}
+
+type FulfillmentCost struct {
+ TaxRate decimal.Decimal `json:"tax_rate"`
+ TotalCostExclTax decimal.Decimal `json:"total_cost_excl_tax"`
+ TotalCostInclTax decimal.Decimal `json:"total_cost_incl_tax"`
+ TotalTax decimal.Decimal `json:"total_tax"`
+}
+
+type LineItemCost struct {
+ CostExclDiscounts decimal.Decimal `json:"cost_excl_discounts"`
+ Discounts []Discount `json:"discounts"`
+ Quantity uint `json:"quantity"`
+ TaxRate decimal.Decimal `json:"tax_rate"`
+ TotalCostExclDiscounts decimal.Decimal `json:"total_cost_excl_discounts"`
+ TotalCostExclTax decimal.Decimal `json:"total_cost_excl_tax"`
+ TotalCostInclTax decimal.Decimal `json:"total_cost_incl_tax"`
+ TotalTax decimal.Decimal `json:"total_tax"`
+ UnitTierCost *decimal.Decimal `json:"unit_tier_cost"`
+}
+
+type Discount struct {
+ Amount decimal.Decimal `json:"amount"`
+ Descr string `json:"description"`
+}
+
+// printJobCostResp is the json body of a /print-job-cost-calculations/ response.
+type printJobCostResp struct {
+ Addr struct {
+ City string `json:"city"`
+ CountryCode string `json:"country_code"`
+ IsBusiness bool `json:"is_business"`
+ Name string `json:"name"`
+ Phone string `json:"phone_number"`
+ PostCode string `json:"postcode"`
+ StateCode string `json:"state_code"`
+ Street1 string `json:"street1"`
+ Street2 string `json:"street2"`
+ Warning ShippingAddressWarning `json:"warnings"`
+ Suggested suggestedShippingAddress `json:"suggested_address"`
+ } `json:"shipping_address"`
+ Currency string `json:"currency"`
+ Fees []Fee `json:"fees"`
+ FulfillmentCost FulfillmentCost `json:"fulfillment_cost"`
+ LineItemCosts []LineItemCost `json:"line_item_costs"`
+ ShipCost FulfillmentCost `json:"shipping_cost"`
+ TotalCostExclTax decimal.Decimal `json:"total_cost_excl_tax"`
+ TotalCostInclTax decimal.Decimal `json:"total_cost_incl_tax"`
+ TotalDiscount decimal.Decimal `json:"total_discount_amount"`
+ TotalTax decimal.Decimal `json:"total_tax"`
+}
+
+// Some blockhead that Lulu hired thought it would be a good idea to use
+// a string in one place and a number in another for the postal code.
+// That's why this special struct is needed.
+type suggestedShippingAddress struct {
+ CountryCode string `json:"country_code"`
+ StateCode string `json:"state_code"`
+ PostCode uint `json:"postcode"`
+ City string `json:"city"`
+ Street1 string `json:"street1"`
+ Street2 string `json:"street2"`
+}
+
+func (c *PrintJobCost) UnmarshalJSON(data []byte) error {
+ // Lulu decided to put warnings and suggested_address INSIDE
+ // shipping_address in the response. So we unmarshal into an
+ // alias struct and then map onto our own structs. The
+ // alternative is to add everything to our ShippingAddress
+ // struct, but that would be confusing to users of this library:
+ // ShippingAddress would have semantics because some fields would
+ // be "optional".
+
+ var resp printJobCostResp
+ if err := json.Unmarshal(data, &resp); err != nil {
+ return err
+ }
+
+ addr := resp.Addr
+ c.Addr = ShippingAddress{
+ City: addr.City,
+ CountryCode: addr.CountryCode,
+ IsBusiness: addr.IsBusiness,
+ Name: addr.Name,
+ Phone: addr.Phone,
+ PostCode: addr.PostCode,
+ StateCode: addr.StateCode,
+ Street1: addr.Street1,
+ Street2: addr.Street2,
+ }
+ sugad := addr.Suggested
+ c.SuggestedAddr = ShippingAddress{
+ CountryCode: sugad.CountryCode,
+ StateCode: sugad.StateCode,
+ PostCode: fmt.Sprint(sugad.PostCode), // idiot
+ City: sugad.City,
+ Street1: sugad.Street1,
+ Street2: sugad.Street2,
+ }
+ c.AddrWarning = addr.Warning
+
+ c.Currency = resp.Currency
+ c.Fees = resp.Fees
+ c.FulfillmentCost = resp.FulfillmentCost
+ c.LineItemCosts = resp.LineItemCosts
+ c.ShipCost = resp.ShipCost
+ c.TotalCostExclTax = resp.TotalCostExclTax
+ c.TotalCostInclTax = resp.TotalCostInclTax
+ c.TotalDiscount = resp.TotalDiscount
+ c.TotalTax = resp.TotalTax
+
+ return nil
+}
diff --git a/cost_test.go b/cost_test.go
new file mode 100644
index 0000000..a8aeb3d
--- /dev/null
+++ b/cost_test.go
@@ -0,0 +1,247 @@
+package lulu
+
+import (
+ "testing"
+
+ "github.com/shopspring/decimal"
+)
+
+func TestMarshalUnmarshalPrintJobCostReq(t *testing.T) {
+ j := `{
+ "line_items": [
+ {
+ "page_count": 32,
+ "pod_package_id": "0600X0900.BW.STD.PB.060UW444.MXX",
+ "quantity": 20
+ }, {
+ "page_count": 324,
+ "pod_package_id": "0425X0687.BW.STD.PB.060UW444.GXX",
+ "quantity": 200
+ }
+ ],
+ "shipping_address": {
+ "city": "Lübeck",
+ "country_code": "DE",
+ "postcode": "23552",
+ "state_code": "",
+ "street1": "Holstenstr. 40",
+ "phone_number": "844-212-0689",
+ "name": "",
+ "street2": "",
+ "is_business": false
+
+ },
+ "shipping_option": "EXPRESS"
+ }`
+ addr := printJobCostReq{
+ []printJobCostLineItem{
+ {
+ 32,
+ PkgId{UsTrade, Mono, Standard, Perfect, P60UncoatedWhite, Matte, NoLinen, NoFoil},
+ 20,
+ }, {
+ 324,
+ PkgId{Pocketbook, Mono, Standard, Perfect, P60UncoatedWhite, Gloss, NoLinen, NoFoil},
+ 200,
+ },
+ },
+ ShippingAddress{
+ City: "Lübeck",
+ CountryCode: "DE",
+ PostCode: "23552",
+ Street1: "Holstenstr. 40",
+ Phone: "844-212-0689",
+ // TODO: does the API mind the extra fields?
+ },
+ Express,
+ }
+ requireMarshalJsonEq(t, j, addr)
+ requireUnmarshalJsonEq(t, addr, j)
+}
+
+func TestUnmarshalPrintJobCost(t *testing.T) {
+ j := `{
+ "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
+ }
+ },
+ "currency": "USD",
+ "fees": [
+ {
+ "currency": "USD",
+ "fee_type": "HANDLING_FEE",
+ "sku": "HANDLING_FEE",
+ "tax_rate": "0.060000",
+ "total_cost_excl_tax": "14.00",
+ "total_cost_incl_tax": "14.84",
+ "total_tax": "0.84"
+ },
+ {
+ "currency": "USD",
+ "fee_type": "FULFILLMENT_FEE",
+ "sku": "FULFILLMENT_FEE",
+ "tax_rate": "0.060000",
+ "total_cost_excl_tax": "0.75",
+ "total_cost_incl_tax": "0.80",
+ "total_tax": "0.05"
+ }
+ ],
+ "fulfillment_cost": {
+ "tax_rate": "0.06",
+ "total_cost_excl_tax": "0.75",
+ "total_cost_incl_tax": "0.80",
+ "total_tax": "0.05"
+ },
+ "line_item_costs": [
+ {
+ "cost_excl_discounts": "2.55",
+ "discounts": [],
+ "quantity": 20,
+ "tax_rate": "0.060000",
+ "total_cost_excl_discounts": "51.00",
+ "total_cost_excl_tax": "51.00",
+ "total_cost_incl_tax": "54.06",
+ "total_tax": "3.06",
+ "unit_tier_cost": null
+ },
+ {
+ "cost_excl_discounts": "9.26",
+ "discounts": [
+ {
+ "amount": "92.60",
+ "description": "Volume Discount 5%"
+ }
+ ],
+ "quantity": 200,
+ "tax_rate": "0.060000",
+ "total_cost_excl_discounts": "1852.00",
+ "total_cost_excl_tax": "1759.40",
+ "total_cost_incl_tax": "1864.96",
+ "total_tax": "105.56",
+ "unit_tier_cost": null
+ }
+ ],
+ "shipping_cost": {
+ "tax_rate": "0.06",
+ "total_cost_excl_tax": "318.44",
+ "total_cost_incl_tax": "337.55",
+ "total_tax": "19.11"
+ },
+ "total_cost_excl_tax": "2129.59",
+ "total_cost_incl_tax": "2257.37",
+ "total_discount_amount": "92.60",
+ "total_tax": "127.78"
+ }`
+
+ cost := PrintJobCost{
+ Addr: ShippingAddress{
+ City: "Lübeck",
+ CountryCode: "DE",
+ IsBusiness: false,
+ Name: "Hans Dampf",
+ Phone: "844-212-0689",
+ PostCode: "23552",
+ StateCode: "",
+ Street1: "Holstenstr. 40",
+ Street2: "",
+ },
+ SuggestedAddr: ShippingAddress{
+ CountryCode: "DE",
+ StateCode: "",
+ PostCode: "23552",
+ City: "Lübeck",
+ Street1: "Holstenstraße 40\"",
+ Street2: "",
+ },
+ AddrWarning: ShippingAddressWarning{
+ "validation_warning",
+ "external",
+ "REPLACED",
+ "street1: Holstenstr. 40 -> Holstenstraße 40",
+ },
+ Currency: "USD",
+ Fees: []Fee{
+ {
+ Currency: "USD",
+ Type: "HANDLING_FEE",
+ Sku: "HANDLING_FEE",
+ TaxRate: decimal.RequireFromString("0.060000"),
+ TotalCostExclTax: decimal.RequireFromString("14.00"),
+ TotalCostInclTax: decimal.RequireFromString("14.84"),
+ TotalTax: decimal.RequireFromString("0.84"),
+ }, {
+ Currency: "USD",
+ Type: "FULFILLMENT_FEE",
+ Sku: "FULFILLMENT_FEE",
+ TaxRate: decimal.RequireFromString("0.060000"),
+ TotalCostExclTax: decimal.RequireFromString("0.75"),
+ TotalCostInclTax: decimal.RequireFromString("0.80"),
+ TotalTax: decimal.RequireFromString("0.05"),
+ },
+ },
+ FulfillmentCost: FulfillmentCost{
+ TaxRate: decimal.RequireFromString("0.06"),
+ TotalCostExclTax: decimal.RequireFromString("0.75"),
+ TotalCostInclTax: decimal.RequireFromString("0.80"),
+ TotalTax: decimal.RequireFromString("0.05"),
+ },
+ LineItemCosts: []LineItemCost{
+ {
+ CostExclDiscounts: decimal.RequireFromString("2.55"),
+ Discounts: []Discount{},
+ Quantity: 20,
+ TaxRate: decimal.RequireFromString("0.060000"),
+ TotalCostExclDiscounts: decimal.RequireFromString("51.00"),
+ TotalCostExclTax: decimal.RequireFromString("51.00"),
+ TotalCostInclTax: decimal.RequireFromString("54.06"),
+ TotalTax: decimal.RequireFromString("3.06"),
+ UnitTierCost: nil,
+ }, {
+ CostExclDiscounts: decimal.RequireFromString("9.26"),
+ Discounts: []Discount{
+ {decimal.RequireFromString("92.60"), "Volume Discount 5%"},
+ },
+ Quantity: 200,
+ TaxRate: decimal.RequireFromString("0.060000"),
+ TotalCostExclDiscounts: decimal.RequireFromString("1852.00"),
+ TotalCostExclTax: decimal.RequireFromString("1759.40"),
+ TotalCostInclTax: decimal.RequireFromString("1864.96"),
+ TotalTax: decimal.RequireFromString("105.56"),
+ UnitTierCost: nil,
+ },
+ },
+ ShipCost: FulfillmentCost{
+ TaxRate: decimal.RequireFromString("0.06"),
+ TotalCostExclTax: decimal.RequireFromString("318.44"),
+ TotalCostInclTax: decimal.RequireFromString("337.55"),
+ TotalTax: decimal.RequireFromString("19.11"),
+ },
+ TotalCostExclTax: decimal.RequireFromString("2129.59"),
+ TotalCostInclTax: decimal.RequireFromString("2257.37"),
+ TotalDiscount: decimal.RequireFromString("92.60"),
+ TotalTax: decimal.RequireFromString("127.78"),
+ }
+
+ requireUnmarshalJsonEq(t, cost, j)
+}
diff --git a/go.mod b/go.mod
index 8d3ada2..eda2d03 100644
--- a/go.mod
+++ b/go.mod
@@ -3,13 +3,17 @@ module git.samanthony.xyz/lulu
go 1.25.9
require (
+ github.com/shopspring/decimal v1.4.0
+ github.com/stretchr/testify v1.11.1
+ golang.org/x/oauth2 v0.36.0
+)
+
+require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
- github.com/stretchr/testify v1.11.1 // indirect
github.com/yawnak/string-enumer v0.0.0-20250330104602-f50db3525c45 // indirect
golang.org/x/mod v0.24.0 // indirect
- golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/tools v0.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
diff --git a/go.sum b/go.sum
index d678017..e9731b8 100644
--- a/go.sum
+++ b/go.sum
@@ -2,6 +2,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
@@ -16,6 +18,7 @@ golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/lulu.go b/lulu.go
index 74ef526..25f8c9c 100644
--- a/lulu.go
+++ b/lulu.go
@@ -165,7 +165,8 @@ func (c *Client) ValidateCover(srcUrl string, mfg PkgId, npages uint) (uint, err
return rec.Id, nil
}
-// GetCoverValidiation retrieves information about a cover file validation job that was started by ValidiateCover().
+// GetCoverValidiation retrieves information about a cover file
+// validation job that was started by ValidiateCover().
//
// https://api.lulu.com/docs/#tag/Files-validation/operation/Validate-Cover_read
func (c *Client) GetCoverValidation(id uint) (CoverValidationRecord, error) {
diff --git a/print.go b/print.go
deleted file mode 100644
index 829a5ef..0000000
--- a/print.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package lulu
-
-// printJobCostReq is the json body of a /print-job-cost-calculations/ request.
-type printJobCostReq struct {
- LineItems []CostCalcLineItem `json:"line_items"`
- ShipAddr ShippingAddress `json:"shipping_address"`
- ShipOpt ShippingLevel `json:"shipping_option"`
-}
-
-type CostCalcLineItem struct {
- NPages uint `json:"page_count"`
- Mfg PkgId `json:"pod_package_id"`
- Quantity uint
-}
diff --git a/ship.go b/ship.go
index a31724e..aef655e 100644
--- a/ship.go
+++ b/ship.go
@@ -14,10 +14,13 @@ const (
)
type ShippingAddress struct {
- City string `json:"city"` // Lübeck
- CountryCode string `json:"country_code"` // DE
- PostCode string `json:"postcode"` // 23552
+ Name string `json:"name"`
+ Phone string `json:"phone_number"`
+ CountryCode string `json:"country_code"`
+ PostCode string `json:"postcode"`
StateCode string `json:"state_code"`
- Street1 string `json:"street1"` // Holstenstr. 40
- Phone string `json:"phone_number"` // 844-212-0689
+ City string `json:"city"`
+ Street1 string `json:"street1"`
+ Street2 string `json:"street2"`
+ IsBusiness bool `json:"is_business"`
}
diff --git a/ship_test.go b/ship_test.go
deleted file mode 100644
index b91e671..0000000
--- a/ship_test.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package lulu
-
-import (
- "testing"
-)
-
-func TestMarshalUnmarshalShippingAddress(t *testing.T) {
- for _, pair := range []struct {
- j string
- addr ShippingAddress
- }{
- {
- `{
- "city": "Lübeck",
- "country_code": "DE",
- "postcode": "23552",
- "state_code": "",
- "street1": "Holstenstr. 40",
- "phone_number": "844-212-0689"
- }`,
- ShippingAddress{
- "Lübeck",
- "DE",
- "23552",
- "", // TODO: does the API mind if this is "" instead of null?
- "Holstenstr. 40",
- "844-212-0689",
- },
- }, {
- `{
- "city": "Anytown",
- "country_code": "CA",
- "postcode": "A1A 1A1",
- "state_code": "QC",
- "street1": "123 Fake Street",
- "phone_number": "123-456-7890"
- }`,
- ShippingAddress{
- "Anytown",
- "CA",
- "A1A 1A1",
- "QC",
- "123 Fake Street",
- "123-456-7890",
- },
- },
- } {
- requireMarshalJsonEq(t, pair.j, pair.addr)
- requireUnmarshalJsonEq(t, pair.addr, pair.j)
- }
-}