diff options
| -rw-r--r-- | cost.go | 165 | ||||
| -rw-r--r-- | cost_test.go | 247 | ||||
| -rw-r--r-- | go.mod | 8 | ||||
| -rw-r--r-- | go.sum | 3 | ||||
| -rw-r--r-- | lulu.go | 3 | ||||
| -rw-r--r-- | print.go | 14 | ||||
| -rw-r--r-- | ship.go | 13 | ||||
| -rw-r--r-- | ship_test.go | 51 |
8 files changed, 431 insertions, 73 deletions
@@ -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) +} @@ -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 @@ -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= @@ -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 -} @@ -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) - } -} |