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"
|
_ => "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;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
|
using System.Security;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using EbayListingTool.Models;
|
using EbayListingTool.Models;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@@ -92,8 +93,16 @@ public class EbayListingService
|
|||||||
// 4. Create offer
|
// 4. Create offer
|
||||||
var offerId = await CreateOfferAsync(draft, token);
|
var offerId = await CreateOfferAsync(draft, token);
|
||||||
|
|
||||||
// 5. Publish offer → get item ID
|
// 5. Publish offer → get item ID (fall back to Trading API if seller registration incomplete)
|
||||||
var itemId = await PublishOfferAsync(offerId, token);
|
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;
|
draft.EbayItemId = itemId;
|
||||||
var domain = _auth.BaseUrl.Contains("sandbox") ? "sandbox.ebay.co.uk" : "ebay.co.uk";
|
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.");
|
?? 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 ----
|
// ---- Photo upload ----
|
||||||
|
|
||||||
private async Task<List<string>> UploadPhotosAsync(List<string> photoPaths, string token)
|
private async Task<List<string>> UploadPhotosAsync(List<string> photoPaths, string token)
|
||||||
|
|||||||
Reference in New Issue
Block a user