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:
@@ -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));
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user