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? _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)
|
||||
{
|
||||
_auth = auth;
|
||||
_categoryService = categoryService;
|
||||
LoadPolicyCacheFromDisk();
|
||||
}
|
||||
|
||||
/// <summary>Call when the user disconnects so stale IDs are not reused after re-login.</summary>
|
||||
public void ClearCache()
|
||||
{
|
||||
_fulfillmentPolicyId = null;
|
||||
_paymentPolicyId = null;
|
||||
_returnPolicyId = 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)
|
||||
{
|
||||
var token = await _auth.GetValidAccessTokenAsync();
|
||||
|
||||
// Resolve business policies and merchant location before touching inventory/offers
|
||||
await EnsurePoliciesAndLocationAsync(token, draft.Postcode);
|
||||
_fulfillmentPolicyId = await GetOrCreateFulfillmentPolicyAsync(draft.Postage, draft.ShippingCost, token);
|
||||
|
||||
// 1. Upload photos and get eBay-hosted URLs
|
||||
var imageUrls = await UploadPhotosAsync(draft.PhotoPaths, token);
|
||||
@@ -73,6 +102,94 @@ public class EbayListingService
|
||||
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 ----
|
||||
|
||||
/// <summary>
|
||||
@@ -84,28 +201,6 @@ public class EbayListingService
|
||||
{
|
||||
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)
|
||||
{
|
||||
using var req = MakeRequest(HttpMethod.Get,
|
||||
|
||||
Reference in New Issue
Block a user