package mrktplaats import ( "bytes" "context" "encoding/json" "fmt" "io" "mime/multipart" "net/url" ) // SYIService handles creating listings (Sell Your Item). type SYIService struct { t *transport } // SYIAttribute describes a form field for creating a listing in a category. type SYIAttribute struct { ID string `json:"id"` Key string `json:"key"` Label string `json:"label"` Name string `json:"name"` AttributeType string `json:"attributeType"` Mandatory bool `json:"mandatory"` SupportedValues []SYIAttributeValue `json:"supportedValues"` } // SYIAttributeValue is a selectable value for an SYI attribute. type SYIAttributeValue struct { ID string `json:"id"` Key string `json:"key"` Label string `json:"label"` Name string `json:"name"` } // SYIFormResponse is returned when fetching the SYI form for a category. type SYIFormResponse struct { CategoryID int `json:"categoryId"` SYIAttributes []SYIAttribute `json:"syiAttributes"` } // Form retrieves the listing creation form for a category. func (s *SYIService) Form(ctx context.Context, categoryID int) (*SYIFormResponse, error) { path := fmt.Sprintf("/app/syi/v7/%d", categoryID) var resp SYIFormResponse if err := s.t.get(ctx, path, nil, &resp); err != nil { return nil, err } return &resp, nil } // Translation holds locale-specific title and description. type Translation struct { Locale string `json:"locale"` Title string `json:"title"` Description string `json:"description"` } // ShippingConfig defines shipping carrier configuration. type ShippingConfig struct { Carriers []string `json:"carriers"` DIYPriceInCent *int `json:"diyPriceInCent,omitempty"` PackageID *string `json:"packageId,omitempty"` } // CreateAdRequest defines the payload for creating a new listing. type CreateAdRequest struct { CategoryID int `json:"categoryId"` Translations []Translation `json:"translations"` PriceInCents int `json:"priceInCents"` PriceType string `json:"priceType"` BiddingEnabled bool `json:"biddingEnabled"` BuyItNowEnabled bool `json:"buyItNowEnabled"` BuyerProtectionAllowed bool `json:"buyerProtectionAllowed"` InitialMinimumBid int `json:"initialMinimumBid,omitempty"` Attributes []any `json:"attributes"` PictureIDs []string `json:"pictureIds"` DeliveryMethod string `json:"deliveryMethod"` ShippingConfig ShippingConfig `json:"shippingConfig"` SellerName string `json:"sellerName"` Postcode string `json:"postcode"` AddToPaymentCart bool `json:"addToPaymentCart"` SelectedBundle string `json:"selectedBundle"` SYISessionID string `json:"syiSessionId"` IntegratedShippingForTWHEnabled bool `json:"integratedShippingForTWHEnabled"` FeatureSource string `json:"featureSource,omitempty"` FeatureTypes []any `json:"featureTypes,omitempty"` URL *string `json:"url"` LicensePlate *string `json:"licensePlate"` MicroTip *string `json:"microTip"` OriginalAd *string `json:"originalAd"` PhoneNumber *string `json:"phoneNumber"` GenAIVersion *string `json:"genAiVersion"` UseDynamicPricing bool `json:"useDynamicPricing"` } // CreateAdData holds the ad and feature data returned after creating a listing. type CreateAdData struct { AdCore json.RawMessage `json:"adCore"` Features json.RawMessage `json:"features"` } // CreateAdResponse is the response from creating a listing. type CreateAdResponse struct { Ad CreateAdData `json:"ad"` } // URN parses and returns the URN of the newly created listing. func (r *CreateAdResponse) URN() string { var core struct { URN string `json:"urn"` } if err := json.Unmarshal(r.Ad.AdCore, &core); err != nil { return "" } return core.URN } // Create publishes a new listing. func (s *SYIService) Create(ctx context.Context, req *CreateAdRequest) (*CreateAdResponse, error) { params := url.Values{"syiType": {"shortFlow"}} var resp CreateAdResponse if err := s.t.postJSON(ctx, "/app/syi/v7/ads", params, nil, req, &resp); err != nil { return nil, err } return &resp, nil } // UploadImage uploads an image for use in a listing. // Returns the picture ID to include in CreateAdRequest.PictureIDs. // Large images are automatically compressed to stay within the API size limit. func (s *SYIService) UploadImage(ctx context.Context, img io.Reader, filename string) (string, error) { data, err := autoCompressImage(img) if err != nil { return "", fmt.Errorf("mrktplaats: reading image: %w", err) } var buf bytes.Buffer w := multipart.NewWriter(&buf) part, err := w.CreateFormFile("image_file", filename) if err != nil { return "", fmt.Errorf("mrktplaats: creating form file: %w", err) } if _, err := io.Copy(part, bytes.NewReader(data)); err != nil { return "", fmt.Errorf("mrktplaats: copying image: %w", err) } w.Close() var resp struct { PictureID string `json:"pictureId"` } if err := s.t.postMultipart(ctx, "/app/syi/v7/images/upload", nil, nil, w.FormDataContentType(), &buf, &resp); err != nil { return "", err } return resp.PictureID, nil } // PriceSegment represents a price range suggestion. type PriceSegment struct { Title string `json:"title"` MinPrice int `json:"minPrice"` MaxPrice int `json:"maxPrice"` TotalSimilarAdsCount int `json:"totalSimilarAdsCount"` } // PriceSuggestionResponse contains price range suggestions. type PriceSuggestionResponse struct { Segments []PriceSegment `json:"segments"` } // PriceSuggestion returns price range suggestions for a title in a category. func (s *SYIService) PriceSuggestion(ctx context.Context, categoryID int, title string) (*PriceSuggestionResponse, error) { params := url.Values{ "category_id": {fmt.Sprintf("%d", categoryID)}, "title": {title}, } var resp PriceSuggestionResponse if err := s.t.get(ctx, "/app/syi/v7/price-suggestion", params, &resp); err != nil { return nil, err } return &resp, nil } // AttributeSuggestions returns suggested attribute values for a description. func (s *SYIService) AttributeSuggestions(ctx context.Context, categoryID int, text string) (json.RawMessage, error) { body := struct { CategoryID int `json:"categoryId"` Text string `json:"text"` }{CategoryID: categoryID, Text: text} var resp json.RawMessage if err := s.t.postJSON(ctx, "/app/syi/v7/attribute-suggestions", nil, nil, body, &resp); err != nil { return nil, err } return resp, nil } // SuspiciousKeywordsWarnings checks a listing description for suspicious content. func (s *SYIService) SuspiciousKeywordsWarnings(ctx context.Context, categoryID int, text string) (json.RawMessage, error) { body := struct { CategoryID int `json:"categoryId"` Text string `json:"text"` }{CategoryID: categoryID, Text: text} var resp json.RawMessage if err := s.t.postJSON(ctx, "/app/syi/v7/category/suspicious-keywords-warnings", nil, nil, body, &resp); err != nil { return nil, err } return resp, nil }