diff --git a/browse.go b/browse.go index c09d9c8..c56731d 100644 --- a/browse.go +++ b/browse.go @@ -375,3 +375,175 @@ func (s *BrowseService) GetItem(ctx context.Context, itemID string, opts ...Opt) var it Item return it, s.client.Do(ctx, req, &it) } + +// ItemsByGroup represents eBay items by group. +type ItemsByGroup struct { + Items []struct { + ItemID string `json:"itemId"` + SellerItemRevision string `json:"sellerItemRevision"` + Title string `json:"title"` + ShortDescription string `json:"shortDescription"` + Price struct { + Value string `json:"value"` + Currency string `json:"currency"` + } `json:"price"` + CategoryPath string `json:"categoryPath"` + Condition string `json:"condition"` + ConditionID string `json:"conditionId"` + ItemLocation struct { + City string `json:"city"` + Country string `json:"country"` + } `json:"itemLocation"` + Image struct { + ImageURL string `json:"imageUrl"` + } `json:"image"` + Color string `json:"color"` + Material string `json:"material"` + Pattern string `json:"pattern"` + SizeType string `json:"sizeType"` + Brand string `json:"brand"` + ItemEndDate time.Time `json:"itemEndDate"` + Seller struct { + Username string `json:"username"` + FeedbackPercentage string `json:"feedbackPercentage"` + FeedbackScore int `json:"feedbackScore"` + } `json:"seller"` + EstimatedAvailabilities []struct { + DeliveryOptions []string `json:"deliveryOptions"` + AvailabilityThresholdType string `json:"availabilityThresholdType"` + AvailabilityThreshold int `json:"availabilityThreshold"` + EstimatedAvailabilityStatus string `json:"estimatedAvailabilityStatus"` + EstimatedSoldQuantity int `json:"estimatedSoldQuantity"` + } `json:"estimatedAvailabilities"` + ShippingOptions []struct { + ShippingServiceCode string `json:"shippingServiceCode"` + TrademarkSymbol string `json:"trademarkSymbol,omitempty"` + ShippingCarrierCode string `json:"shippingCarrierCode,omitempty"` + Type string `json:"type"` + ShippingCost struct { + Value string `json:"value"` + Currency string `json:"currency"` + } `json:"shippingCost"` + QuantityUsedForEstimate int `json:"quantityUsedForEstimate"` + MinEstimatedDeliveryDate time.Time `json:"minEstimatedDeliveryDate"` + MaxEstimatedDeliveryDate time.Time `json:"maxEstimatedDeliveryDate"` + ShipToLocationUsedForEstimate struct { + Country string `json:"country"` + } `json:"shipToLocationUsedForEstimate"` + AdditionalShippingCostPerUnit struct { + Value string `json:"value"` + Currency string `json:"currency"` + } `json:"additionalShippingCostPerUnit"` + ShippingCostType string `json:"shippingCostType"` + } `json:"shippingOptions"` + ShipToLocations struct { + RegionIncluded []struct { + RegionName string `json:"regionName"` + RegionType string `json:"regionType"` + } `json:"regionIncluded"` + RegionExcluded []struct { + RegionName string `json:"regionName"` + RegionType string `json:"regionType"` + } `json:"regionExcluded"` + } `json:"shipToLocations"` + ReturnTerms struct { + ReturnsAccepted bool `json:"returnsAccepted"` + RefundMethod string `json:"refundMethod"` + ReturnMethod string `json:"returnMethod"` + ReturnShippingCostPayer string `json:"returnShippingCostPayer"` + ReturnPeriod struct { + Value int `json:"value"` + Unit string `json:"unit"` + } `json:"returnPeriod"` + } `json:"returnTerms"` + LocalizedAspects []struct { + Type string `json:"type"` + Name string `json:"name"` + Value string `json:"value"` + } `json:"localizedAspects"` + TopRatedBuyingExperience bool `json:"topRatedBuyingExperience"` + BuyingOptions []string `json:"buyingOptions"` + PrimaryItemGroup struct { + ItemGroupID string `json:"itemGroupId"` + ItemGroupType string `json:"itemGroupType"` + ItemGroupHref string `json:"itemGroupHref"` + ItemGroupTitle string `json:"itemGroupTitle"` + ItemGroupImage struct { + ImageURL string `json:"imageUrl"` + } `json:"itemGroupImage"` + ItemGroupAdditionalImages []struct { + ImageURL string `json:"imageUrl"` + } `json:"itemGroupAdditionalImages"` + } `json:"primaryItemGroup"` + EnabledForGuestCheckout bool `json:"enabledForGuestCheckout"` + AdultOnly bool `json:"adultOnly"` + CategoryID string `json:"categoryId"` + } `json:"items"` + CommonDescriptions []struct { + Description string `json:"description"` + ItemIds []string `json:"itemIds"` + } `json:"commonDescriptions"` +} + +// GetItemByGroupID retrieves the details of the individual items in an item group. +// +// eBay API docs: https://developer.ebay.com/api-docs/buy/browse/resources/item/methods/getItemsByItemGroup +func (s *BrowseService) GetItemByGroupID(ctx context.Context, groupID string, opts ...Opt) (ItemsByGroup, error) { + u := fmt.Sprintf("buy/browse/v1/item/get_items_by_item_group?item_group_id=%s", groupID) + req, err := s.client.NewRequest(http.MethodGet, u, nil, opts...) + if err != nil { + return ItemsByGroup{}, err + } + var it ItemsByGroup + return it, s.client.Do(ctx, req, &it) +} + +// CompatibilityProperty represents a product property. +type CompatibilityProperty struct { + Name string `json:"name"` + Value string `json:"value"` +} + +// Compatibility represents an item compatibility. +type Compatibility struct { + CompatibilityStatus string `json:"compatibilityStatus"` + Warnings []struct { + Category string `json:"category"` + Domain string `json:"domain"` + ErrorID int `json:"errorId"` + InputRefIds []string `json:"inputRefIds"` + LongMessage string `json:"longMessage"` + Message string `json:"message"` + OutputRefIds []string `json:"outputRefIds"` + Parameters []struct { + Name string `json:"name"` + Value string `json:"value"` + } `json:"parameters"` + Subdomain string `json:"subdomain"` + } `json:"warnings"` +} + +// Valid values for the "compatibilityStatus" compatibility field. +const ( + BrowseCheckComoatibilityCompatible = "COMPATIBLE" + BrowseCheckComoatibilityNotCompatible = "NOT_COMPATIBLE" + BrowseCheckComoatibilityUndertermined = "UNDETERMINED" +) + +// CheckCompatibility checks a product is compatible with the specified item. +// +// eBay API docs: https://developer.ebay.com/api-docs/buy/browse/resources/item/methods/checkCompatibility +func (s *BrowseService) CheckCompatibility(ctx context.Context, itemID, marketplaceID string, properties []CompatibilityProperty, opts ...Opt) (Compatibility, error) { + type payload struct { + CompatibilityProperties []CompatibilityProperty `json:"compatibilityProperties"` + } + pl := payload{properties} + u := fmt.Sprintf("buy/browse/v1/item/%s/check_compatibility", itemID) + opts = append(opts, OptBuyMarketplace(marketplaceID)) + req, err := s.client.NewRequest(http.MethodPost, u, &pl, opts...) + if err != nil { + return Compatibility{}, err + } + var c Compatibility + return c, s.client.Do(ctx, req, &c) +} diff --git a/browse_test.go b/browse_test.go index 911786c..a277894 100644 --- a/browse_test.go +++ b/browse_test.go @@ -3,6 +3,7 @@ package ebay_test import ( "context" "fmt" + "io/ioutil" "net/http" "testing" @@ -70,3 +71,46 @@ func TestGettItem(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "PRODUCT", item.ItemID) } + +func TestGetItemByGroupID(t *testing.T) { + client, mux, teardown := setup(t) + defer teardown() + + mux.HandleFunc("/buy/browse/v1/item/get_items_by_item_group", func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Fatalf("expected GET method, got: %s", r.Method) + } + fmt.Fprintf(w, `{"items": [{"itemId": "%s"}]}`, r.URL.Query().Get("item_group_id")) + }) + + it, err := client.Buy.Browse.GetItemByGroupID(context.Background(), "151915076499") + assert.Nil(t, err) + assert.Equal(t, "151915076499", it.Items[0].ItemID) +} + +func TestCheckCompatibility(t *testing.T) { + client, mux, teardown := setup(t) + defer teardown() + + mux.HandleFunc("/buy/browse/v1/item/v1|202117468662|0/check_compatibility", func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Fatalf("expected POST method, got: %s", r.Method) + } + assert.Equal(t, ebay.BuyMarketplaceUSA, r.Header.Get("X-EBAY-C-MARKETPLACE-ID")) + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatalf("%+v", err) + } + assert.Equal(t, `{"compatibilityProperties":[{"name":"0","value":"1"},{"name":"2","value":"3"}]} +`, string(body)) + fmt.Fprintf(w, `{"compatibilityStatus": "NOT_COMPATIBLE", "warnings": [{"category" : "category"}]}`) + }) + compatibilityProperties := []ebay.CompatibilityProperty{ + {Name: "0", Value: "1"}, + {Name: "2", Value: "3"}, + } + compatibility, err := client.Buy.Browse.CheckCompatibility(context.Background(), "v1|202117468662|0", ebay.BuyMarketplaceUSA, compatibilityProperties) + assert.Nil(t, err) + assert.Equal(t, "NOT_COMPATIBLE", compatibility.CompatibilityStatus) + assert.Equal(t, "category", compatibility.Warnings[0].Category) +} diff --git a/ebay.go b/ebay.go index 812a9c7..4e1d9fc 100644 --- a/ebay.go +++ b/ebay.go @@ -14,9 +14,10 @@ import ( "github.com/pkg/errors" ) +// eBay URLs. const ( - baseURL = "https://api.ebay.com/" - sandboxBaseURL = "https://api.sandbox.ebay.com/" + BaseURL = "https://api.ebay.com/" + SandboxBaseURL = "https://api.sandbox.ebay.com/" ) // Some eBay API scopes. @@ -47,13 +48,13 @@ type Client struct { // NewClient returns a new eBay API client. // If a nil httpClient is provided, http.DefaultClient will be used. func NewClient(httpclient *http.Client) *Client { - return newClient(httpclient, baseURL) + return newClient(httpclient, BaseURL) } // NewSandboxClient returns a new eBay sandbox API client. // If a nil httpClient is provided, http.DefaultClient will be used. func NewSandboxClient(httpclient *http.Client) *Client { - return newClient(httpclient, sandboxBaseURL) + return newClient(httpclient, SandboxBaseURL) } // NewCustomClient returns a new custom eBay API client. @@ -118,12 +119,13 @@ func (c *Client) NewRequest(method, url string, body interface{}, opts ...Opt) ( // Do sends an API request and stores the JSON decoded value into v. func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) error { + dump, _ := httputil.DumpRequest(req, true) resp, err := c.client.Do(req.WithContext(ctx)) if err != nil { return errors.WithStack(err) } defer resp.Body.Close() - if err := CheckResponse(req, resp); err != nil { + if err := CheckResponse(req, resp, string(dump)); err != nil { return err } if v == nil { @@ -165,12 +167,11 @@ func (e *ErrorData) Error() string { } // CheckResponse checks the API response for errors, and returns them if present. -func CheckResponse(req *http.Request, resp *http.Response) error { +func CheckResponse(req *http.Request, resp *http.Response, dump string) error { if s := resp.StatusCode; 200 <= s && s < 300 { return nil } - dump, _ := httputil.DumpRequest(req, true) - errorData := &ErrorData{response: resp, requestDump: string(dump)} + errorData := &ErrorData{response: resp, requestDump: dump} _ = json.NewDecoder(resp.Body).Decode(errorData) return errorData } diff --git a/ebay_test.go b/ebay_test.go index 3e7fe24..57a93fd 100644 --- a/ebay_test.go +++ b/ebay_test.go @@ -38,7 +38,7 @@ func TestNewRequest(t *testing.T) { func TestCheckResponseNoError(t *testing.T) { resp := &http.Response{StatusCode: 200} - assert.Nil(t, ebay.CheckResponse(&http.Request{}, resp)) + assert.Nil(t, ebay.CheckResponse(&http.Request{}, resp, "")) } func TestCheckResponse(t *testing.T) { @@ -67,7 +67,7 @@ func TestCheckResponse(t *testing.T) { ] }` resp := &http.Response{StatusCode: 400, Body: ioutil.NopCloser(bytes.NewBufferString(body))} - err, ok := ebay.CheckResponse(&http.Request{URL: &url.URL{}}, resp).(*ebay.ErrorData) + err, ok := ebay.CheckResponse(&http.Request{URL: &url.URL{}}, resp, "").(*ebay.ErrorData) assert.True(t, ok) assert.Equal(t, 1, len(err.Errors)) assert.Equal(t, 15008, err.Errors[0].ErrorID) diff --git a/offer.go b/offer.go index 8d6d570..f9717e1 100644 --- a/offer.go +++ b/offer.go @@ -120,8 +120,6 @@ const ( // // eBay API docs: https://developer.ebay.com/api-docs/buy/offer/resources/bidding/methods/placeProxyBid func (s *OfferService) PlaceProxyBid(ctx context.Context, itemID, marketplaceID, maxAmount, currency string, userConsentAdultOnlyItem bool, opts ...Opt) (ProxyBid, error) { - u := fmt.Sprintf("buy/offer/v1_beta/bidding/%s/place_proxy_bid", itemID) - opts = append(opts, OptBuyMarketplace(marketplaceID)) type userConsent struct { AdultOnlyItem bool `json:"adultOnlyItem,omitempty"` } @@ -139,6 +137,8 @@ func (s *OfferService) PlaceProxyBid(ctx context.Context, itemID, marketplaceID, if userConsentAdultOnlyItem { pl.UserConsent = &userConsent{userConsentAdultOnlyItem} } + u := fmt.Sprintf("buy/offer/v1_beta/bidding/%s/place_proxy_bid", itemID) + opts = append(opts, OptBuyMarketplace(marketplaceID)) req, err := s.client.NewRequest(http.MethodPost, u, &pl, opts...) if err != nil { return ProxyBid{}, err diff --git a/offer_test.go b/offer_test.go index 5895bbf..141ff82 100644 --- a/offer_test.go +++ b/offer_test.go @@ -5,7 +5,6 @@ import ( "fmt" "io/ioutil" "net/http" - "strconv" "testing" "github.com/jybp/ebay" @@ -43,23 +42,17 @@ func TestPlaceProxyBid(t *testing.T) { if r.Method != "POST" { t.Fatalf("expected POST method, got: %s", r.Method) } - marketplaceID := r.Header.Get("X-EBAY-C-MARKETPLACE-ID") + assert.Equal(t, ebay.BuyMarketplaceUSA, r.Header.Get("X-EBAY-C-MARKETPLACE-ID")) body, err := ioutil.ReadAll(r.Body) if err != nil { t.Fatalf("%+v", err) - http.Error(w, err.Error(), 500) - return } - escapedBody := strconv.Quote(string(body)) - escapedBody = escapedBody[1 : len(escapedBody)-1] - fmt.Fprintf(w, `{"proxyBidId": "%s_%s"}`, escapedBody, marketplaceID) + assert.Equal(t, `{"maxAmount":{"currency":"USD","value":"1.23"},"userConsent":{"adultOnlyItem":true}} +`, string(body)) + fmt.Fprintf(w, `{"proxyBidId": "123"}`) }) - bid, err := client.Buy.Offer.PlaceProxyBid(context.Background(), "v1|202117468662|0", ebay.BuyMarketplaceUSA, "1.23", "USD", false) + bid, err := client.Buy.Offer.PlaceProxyBid(context.Background(), "v1|202117468662|0", ebay.BuyMarketplaceUSA, "1.23", "USD", true) assert.Nil(t, err) - assert.Equal(t, "{\"maxAmount\":{\"currency\":\"USD\",\"value\":\"1.23\"}}\n_EBAY_US", bid.ProxyBidID) - - bid, err = client.Buy.Offer.PlaceProxyBid(context.Background(), "v1|202117468662|0", ebay.BuyMarketplaceUSA, "1.23", "USD", true) - assert.Nil(t, err) - assert.Equal(t, "{\"maxAmount\":{\"currency\":\"USD\",\"value\":\"1.23\"},\"userConsent\":{\"adultOnlyItem\":true}}\n_EBAY_US", bid.ProxyBidID) + assert.Equal(t, `123`, bid.ProxyBidID) } diff --git a/test/integration/auction_test.go b/test/integration/auction_test.go index 08dadef..b6e5b40 100644 --- a/test/integration/auction_test.go +++ b/test/integration/auction_test.go @@ -9,7 +9,6 @@ import ( "net/http" "net/url" "os" - "strconv" "strings" "testing" "time" @@ -137,26 +136,5 @@ func TestAuction(t *testing.T) { if err != nil && !ebay.IsError(err, ebay.ErrGetBiddingNoBiddingActivity) { t.Fatalf("Expected error code %d, got %+v.", ebay.ErrGetBiddingNoBiddingActivity, err) } - - var bidValue, bidCurrency string - if len(bid.SuggestedBidAmounts) > 0 { - bidValue = bid.SuggestedBidAmounts[0].Value - bidCurrency = bid.SuggestedBidAmounts[0].Currency - } else { - bidValue = it.CurrentBidPrice.Value - v, err := strconv.ParseFloat(bidValue, 64) - if err != nil { - t.Fatal(err) - } - v += 2 - bidValue = fmt.Sprintf("%.2f", v) - bidCurrency = it.CurrentBidPrice.Currency - } - - _, err = client.Buy.Offer.PlaceProxyBid(ctx, it.ItemID, ebay.BuyMarketplaceUSA, bidValue, bidCurrency, false) - if err != nil { - t.Fatal(err) - } - - t.Logf("Successfully bid %+v.", bid.SuggestedBidAmounts[0]) + t.Logf("bidding: %+v", bid) }