From 0c836a47c39948e5f10f729bf17758d87d4bf3be Mon Sep 17 00:00:00 2001 From: Peter Foster Date: Fri, 17 Apr 2026 13:27:34 +0100 Subject: [PATCH] feat: Trading API AddItem fallback when Inventory API publish fails (25002) When sellerRegistrationCompleted=false (sandbox), PublishOffer returns 25002. On 25002, PostListingAsync now falls back to Trading API AddItem SOAP call using SellerProfiles (business policy IDs) for shipping/payment/returns. Also adds ConditionNumericId to ListingDraft for Trading API numeric condition IDs. Co-Authored-By: Claude Sonnet 4.6 --- EbayListingTool/Models/ListingDraft.cs | 11 +++ .../Services/EbayListingService.cs | 97 ++++++++++++++++++- 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/EbayListingTool/Models/ListingDraft.cs b/EbayListingTool/Models/ListingDraft.cs index 9b6f4e9..7988ba9 100644 --- a/EbayListingTool/Models/ListingDraft.cs +++ b/EbayListingTool/Models/ListingDraft.cs @@ -172,6 +172,17 @@ public class ListingDraft : INotifyPropertyChanged _ => "USED_VERY_GOOD" }; + // Numeric condition IDs for Trading API (AddItem) + public string ConditionNumericId => Condition switch + { + ItemCondition.New => "1000", + ItemCondition.OpenBox => "1500", + ItemCondition.Refurbished => "2500", + ItemCondition.Used => "3000", + ItemCondition.ForPartsOrNotWorking => "7000", + _ => "3000" + }; + public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string? name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); diff --git a/EbayListingTool/Services/EbayListingService.cs b/EbayListingTool/Services/EbayListingService.cs index e715fa4..6638838 100644 --- a/EbayListingTool/Services/EbayListingService.cs +++ b/EbayListingTool/Services/EbayListingService.cs @@ -1,5 +1,6 @@ using System.Net.Http; using System.Net.Http.Headers; +using System.Security; using System.Text; using EbayListingTool.Models; using Newtonsoft.Json; @@ -92,8 +93,16 @@ public class EbayListingService // 4. Create offer var offerId = await CreateOfferAsync(draft, token); - // 5. Publish offer → get item ID - var itemId = await PublishOfferAsync(offerId, token); + // 5. Publish offer → get item ID (fall back to Trading API if seller registration incomplete) + string itemId; + try + { + itemId = await PublishOfferAsync(offerId, token); + } + catch (HttpRequestException ex) when (ex.Message.Contains("25002")) + { + itemId = await AddItemViaTradingApiAsync(draft, imageUrls, token); + } draft.EbayItemId = itemId; var domain = _auth.BaseUrl.Contains("sandbox") ? "sandbox.ebay.co.uk" : "ebay.co.uk"; @@ -405,6 +414,90 @@ public class EbayListingService ?? throw new InvalidOperationException("No listingId in publish response."); } + // ---- Trading API fallback (AddItem) ---- + + private async Task AddItemViaTradingApiAsync( + ListingDraft draft, List imageUrls, string token) + { + var tradingUrl = _auth.BaseUrl.Contains("sandbox") + ? "https://api.sandbox.ebay.com/ws/api.dll" + : "https://api.ebay.com/ws/api.dll"; + + var pictureXml = imageUrls.Count > 0 + ? "" + + string.Concat(imageUrls.Select(u => $"{u}")) + + "" + : ""; + + var aspectsXml = draft.Aspects.Count > 0 + ? "" + + string.Concat(draft.Aspects.Select(kv => + $"{SecurityElement.Escape(kv.Key)}" + + $"{SecurityElement.Escape(kv.Value)}")) + + "" + : ""; + + var listingType = draft.Format == ListingFormat.Auction ? "Chinese" : "FixedPriceItem"; + var duration = draft.Format == ListingFormat.Auction ? "Days_7" : "GTC"; + + var soap = $""" + + + {token} + en_GB + High + + {SecurityElement.Escape(draft.Title)} + + {SecurityElement.Escape(draft.CategoryId)} + {draft.Price:F2} + {draft.ConditionNumericId} + GB + GBP + 1 + {duration} + {listingType} + {draft.Quantity} + {SecurityElement.Escape(draft.Postcode)} + {pictureXml} + {aspectsXml} + + {_fulfillmentPolicyId} + {_paymentPolicyId} + {_returnPolicyId} + + + + """; + + using var req = new HttpRequestMessage(HttpMethod.Post, tradingUrl); + req.Headers.Add("X-EBAY-API-SITEID", "3"); + req.Headers.Add("X-EBAY-API-COMPATIBILITY-LEVEL", "967"); + req.Headers.Add("X-EBAY-API-CALL-NAME", "AddItem"); + req.Headers.Add("X-EBAY-API-IAF-TOKEN", token); + req.Content = new StringContent(soap, Encoding.UTF8, "text/xml"); + + var res = await _photoHttp.SendAsync(req); + var xml = await res.Content.ReadAsStringAsync(); + + var ackMatch = System.Text.RegularExpressions.Regex.Match(xml, @"(.*?)"); + var ack = ackMatch.Success ? ackMatch.Groups[1].Value : "Unknown"; + + if (ack is not ("Success" or "Warning")) + { + var errMatch = System.Text.RegularExpressions.Regex.Match( + xml, @"(.*?)"); + var errMsg = errMatch.Success ? errMatch.Groups[1].Value : xml[..Math.Min(500, xml.Length)]; + throw new HttpRequestException($"Trading API AddItem failed ({ack}): {errMsg}"); + } + + var idMatch = System.Text.RegularExpressions.Regex.Match(xml, @"(\d+)"); + if (!idMatch.Success) + throw new InvalidOperationException("No ItemID in AddItem response."); + + return idMatch.Groups[1].Value; + } + // ---- Photo upload ---- private async Task> UploadPhotosAsync(List photoPaths, string token)