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 <noreply@anthropic.com>
This commit is contained in:
Peter Foster
2026-04-17 13:27:34 +01:00
parent d9072a6018
commit 0c836a47c3
2 changed files with 106 additions and 2 deletions

View File

@@ -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));

View File

@@ -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<string> AddItemViaTradingApiAsync(
ListingDraft draft, List<string> 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
? "<PictureDetails>" +
string.Concat(imageUrls.Select(u => $"<PictureURL>{u}</PictureURL>")) +
"</PictureDetails>"
: "";
var aspectsXml = draft.Aspects.Count > 0
? "<ItemSpecifics>" +
string.Concat(draft.Aspects.Select(kv =>
$"<NameValueList><Name>{SecurityElement.Escape(kv.Key)}</Name>" +
$"<Value>{SecurityElement.Escape(kv.Value)}</Value></NameValueList>")) +
"</ItemSpecifics>"
: "";
var listingType = draft.Format == ListingFormat.Auction ? "Chinese" : "FixedPriceItem";
var duration = draft.Format == ListingFormat.Auction ? "Days_7" : "GTC";
var soap = $"""
<?xml version="1.0" encoding="utf-8"?>
<AddItemRequest xmlns="urn:ebay:apis:eBLBaseComponents">
<RequesterCredentials><eBayAuthToken>{token}</eBayAuthToken></RequesterCredentials>
<ErrorLanguage>en_GB</ErrorLanguage>
<WarningLevel>High</WarningLevel>
<Item>
<Title>{SecurityElement.Escape(draft.Title)}</Title>
<Description><![CDATA[{draft.Description}]]></Description>
<PrimaryCategory><CategoryID>{SecurityElement.Escape(draft.CategoryId)}</CategoryID></PrimaryCategory>
<StartPrice>{draft.Price:F2}</StartPrice>
<ConditionID>{draft.ConditionNumericId}</ConditionID>
<Country>GB</Country>
<Currency>GBP</Currency>
<DispatchTimeMax>1</DispatchTimeMax>
<ListingDuration>{duration}</ListingDuration>
<ListingType>{listingType}</ListingType>
<Quantity>{draft.Quantity}</Quantity>
<Location>{SecurityElement.Escape(draft.Postcode)}</Location>
{pictureXml}
{aspectsXml}
<SellerProfiles>
<SellerShippingProfile><ShippingProfileID>{_fulfillmentPolicyId}</ShippingProfileID></SellerShippingProfile>
<SellerPaymentProfile><PaymentProfileID>{_paymentPolicyId}</PaymentProfileID></SellerPaymentProfile>
<SellerReturnProfile><ReturnProfileID>{_returnPolicyId}</ReturnProfileID></SellerReturnProfile>
</SellerProfiles>
</Item>
</AddItemRequest>
""";
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, @"<Ack>(.*?)</Ack>");
var ack = ackMatch.Success ? ackMatch.Groups[1].Value : "Unknown";
if (ack is not ("Success" or "Warning"))
{
var errMatch = System.Text.RegularExpressions.Regex.Match(
xml, @"<ShortMessage>(.*?)</ShortMessage>");
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, @"<ItemID>(\d+)</ItemID>");
if (!idMatch.Success)
throw new InvalidOperationException("No ItemID in AddItem response.");
return idMatch.Groups[1].Value;
}
// ---- Photo upload ----
private async Task<List<string>> UploadPhotosAsync(List<string> photoPaths, string token)