feat: create fulfillment policies on-demand per postage option and cost
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,27 +22,56 @@ public class EbayListingService
|
|||||||
private string? _returnPolicyId;
|
private string? _returnPolicyId;
|
||||||
private string? _merchantLocationKey;
|
private string? _merchantLocationKey;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, string> _policyCache = new();
|
||||||
|
private static readonly string PolicyCacheFile =
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
|
"EbayListingTool", "fulfillment_policies.json");
|
||||||
|
|
||||||
public EbayListingService(EbayAuthService auth, EbayCategoryService categoryService)
|
public EbayListingService(EbayAuthService auth, EbayCategoryService categoryService)
|
||||||
{
|
{
|
||||||
_auth = auth;
|
_auth = auth;
|
||||||
_categoryService = categoryService;
|
_categoryService = categoryService;
|
||||||
|
LoadPolicyCacheFromDisk();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Call when the user disconnects so stale IDs are not reused after re-login.</summary>
|
/// <summary>Call when the user disconnects so stale IDs are not reused after re-login.</summary>
|
||||||
public void ClearCache()
|
public void ClearCache()
|
||||||
{
|
{
|
||||||
_fulfillmentPolicyId = null;
|
|
||||||
_paymentPolicyId = null;
|
_paymentPolicyId = null;
|
||||||
_returnPolicyId = null;
|
_returnPolicyId = null;
|
||||||
_merchantLocationKey = null;
|
_merchantLocationKey = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LoadPolicyCacheFromDisk()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(PolicyCacheFile)) return;
|
||||||
|
var json = File.ReadAllText(PolicyCacheFile);
|
||||||
|
var dict = JsonConvert.DeserializeObject<Dictionary<string, string>>(json);
|
||||||
|
if (dict != null)
|
||||||
|
foreach (var kv in dict) _policyCache[kv.Key] = kv.Value;
|
||||||
|
}
|
||||||
|
catch { /* ignore corrupt cache */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SavePolicyCacheToDisk()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(PolicyCacheFile)!);
|
||||||
|
File.WriteAllText(PolicyCacheFile, JsonConvert.SerializeObject(_policyCache));
|
||||||
|
}
|
||||||
|
catch { /* non-critical */ }
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<string> PostListingAsync(ListingDraft draft)
|
public async Task<string> PostListingAsync(ListingDraft draft)
|
||||||
{
|
{
|
||||||
var token = await _auth.GetValidAccessTokenAsync();
|
var token = await _auth.GetValidAccessTokenAsync();
|
||||||
|
|
||||||
// Resolve business policies and merchant location before touching inventory/offers
|
// Resolve business policies and merchant location before touching inventory/offers
|
||||||
await EnsurePoliciesAndLocationAsync(token, draft.Postcode);
|
await EnsurePoliciesAndLocationAsync(token, draft.Postcode);
|
||||||
|
_fulfillmentPolicyId = await GetOrCreateFulfillmentPolicyAsync(draft.Postage, draft.ShippingCost, token);
|
||||||
|
|
||||||
// 1. Upload photos and get eBay-hosted URLs
|
// 1. Upload photos and get eBay-hosted URLs
|
||||||
var imageUrls = await UploadPhotosAsync(draft.PhotoPaths, token);
|
var imageUrls = await UploadPhotosAsync(draft.PhotoPaths, token);
|
||||||
@@ -73,6 +102,94 @@ public class EbayListingService
|
|||||||
return draft.EbayListingUrl;
|
return draft.EbayListingUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Fulfillment policy: on-demand creation ----
|
||||||
|
|
||||||
|
private static string ToShippingServiceCode(PostageOption option) => option switch
|
||||||
|
{
|
||||||
|
PostageOption.RoyalMailFirstClass => "UK_RoyalMailFirstClassStandard",
|
||||||
|
PostageOption.RoyalMailSecondClass => "UK_RoyalMailSecondClassStandard",
|
||||||
|
PostageOption.RoyalMailTracked24 => "UK_RoyalMailTracked24",
|
||||||
|
PostageOption.RoyalMailTracked48 => "UK_RoyalMailTracked48",
|
||||||
|
PostageOption.CollectionOnly => "UK_CollectInPerson",
|
||||||
|
PostageOption.FreePostage => "UK_RoyalMailSecondClassStandard",
|
||||||
|
_ => "UK_RoyalMailSecondClassStandard"
|
||||||
|
};
|
||||||
|
|
||||||
|
private async Task<string> GetOrCreateFulfillmentPolicyAsync(
|
||||||
|
PostageOption postage, decimal shippingCost, string token)
|
||||||
|
{
|
||||||
|
var free = postage == PostageOption.FreePostage || postage == PostageOption.CollectionOnly;
|
||||||
|
var cost = free ? 0m : shippingCost;
|
||||||
|
var cacheKey = $"{postage}_{cost:F2}";
|
||||||
|
|
||||||
|
if (_policyCache.TryGetValue(cacheKey, out var cached)) return cached;
|
||||||
|
|
||||||
|
var serviceCode = ToShippingServiceCode(postage);
|
||||||
|
var policyName = $"ELT_{postage}_{cost:F2}".Replace(" ", "");
|
||||||
|
|
||||||
|
object shippingServiceObj;
|
||||||
|
if (postage == PostageOption.CollectionOnly)
|
||||||
|
{
|
||||||
|
shippingServiceObj = new
|
||||||
|
{
|
||||||
|
shippingServiceCode = serviceCode,
|
||||||
|
shippingCost = new { value = "0.00", currency = "GBP" },
|
||||||
|
freeShipping = false,
|
||||||
|
buyerResponsibleForShipping = true,
|
||||||
|
sortOrder = 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
shippingServiceObj = new
|
||||||
|
{
|
||||||
|
shippingCarrierCode = "RoyalMail",
|
||||||
|
shippingServiceCode = serviceCode,
|
||||||
|
shippingCost = new { value = cost.ToString("F2"), currency = "GBP" },
|
||||||
|
freeShipping = free,
|
||||||
|
sortOrder = 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = new
|
||||||
|
{
|
||||||
|
name = policyName,
|
||||||
|
marketplaceId = "EBAY_GB",
|
||||||
|
categoryTypes = new[] { new { name = "ALL_EXCLUDING_MOTORS_VEHICLES" } },
|
||||||
|
handlingTime = new { value = 2, unit = "DAY" },
|
||||||
|
shippingOptions = new[]
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
optionType = "DOMESTIC",
|
||||||
|
costType = postage == PostageOption.CollectionOnly ? "NOT_SPECIFIED" : "FLAT_RATE",
|
||||||
|
shippingServices = new[] { shippingServiceObj }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonConvert.SerializeObject(body,
|
||||||
|
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
|
||||||
|
|
||||||
|
using var req = MakeRequest(HttpMethod.Post,
|
||||||
|
$"{_auth.BaseUrl}/sell/account/v1/fulfillment_policy", token);
|
||||||
|
req.Content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var res = await _http.SendAsync(req);
|
||||||
|
var resJson = await res.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
if (!res.IsSuccessStatusCode)
|
||||||
|
throw new HttpRequestException(
|
||||||
|
$"Could not create fulfillment policy ({(int)res.StatusCode}): {resJson}");
|
||||||
|
|
||||||
|
var policyId = JObject.Parse(resJson)["fulfillmentPolicyId"]?.ToString()
|
||||||
|
?? throw new InvalidOperationException("No fulfillmentPolicyId in response.");
|
||||||
|
|
||||||
|
_policyCache[cacheKey] = policyId;
|
||||||
|
SavePolicyCacheToDisk();
|
||||||
|
return policyId;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Setup: policies + location ----
|
// ---- Setup: policies + location ----
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -84,28 +201,6 @@ public class EbayListingService
|
|||||||
{
|
{
|
||||||
var baseUrl = _auth.BaseUrl;
|
var baseUrl = _auth.BaseUrl;
|
||||||
|
|
||||||
if (_fulfillmentPolicyId == null)
|
|
||||||
{
|
|
||||||
using var req = MakeRequest(HttpMethod.Get,
|
|
||||||
$"{baseUrl}/sell/account/v1/fulfillment_policy?marketplace_id=EBAY_GB", token);
|
|
||||||
var res = await _http.SendAsync(req);
|
|
||||||
var json = await res.Content.ReadAsStringAsync();
|
|
||||||
|
|
||||||
if (!res.IsSuccessStatusCode)
|
|
||||||
throw new HttpRequestException(
|
|
||||||
$"Could not fetch fulfillment policies ({(int)res.StatusCode}): {json}");
|
|
||||||
|
|
||||||
var arr = JObject.Parse(json)["fulfillmentPolicies"] as JArray;
|
|
||||||
_fulfillmentPolicyId = arr?.Count > 0
|
|
||||||
? arr[0]["fulfillmentPolicyId"]?.ToString()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (_fulfillmentPolicyId == null)
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"No fulfillment policy found on your eBay account.\n\n" +
|
|
||||||
"Please set one up in My eBay → Account → Business policies, then try again.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_paymentPolicyId == null)
|
if (_paymentPolicyId == null)
|
||||||
{
|
{
|
||||||
using var req = MakeRequest(HttpMethod.Get,
|
using var req = MakeRequest(HttpMethod.Get,
|
||||||
|
|||||||
Reference in New Issue
Block a user