From c9bdb6f7fe53d89a58b3eea9aff798006a83d4bd Mon Sep 17 00:00:00 2001 From: Peter Foster Date: Wed, 15 Apr 2026 09:37:25 +0100 Subject: [PATCH] feat: create fulfillment policies on-demand per postage option and cost Co-Authored-By: Claude Sonnet 4.6 --- .../Services/EbayListingService.cs | 141 +++++++++++++++--- 1 file changed, 118 insertions(+), 23 deletions(-) diff --git a/EbayListingTool/Services/EbayListingService.cs b/EbayListingTool/Services/EbayListingService.cs index a8403aa..f982c42 100644 --- a/EbayListingTool/Services/EbayListingService.cs +++ b/EbayListingTool/Services/EbayListingService.cs @@ -22,27 +22,56 @@ public class EbayListingService private string? _returnPolicyId; private string? _merchantLocationKey; + private readonly Dictionary _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(); } /// Call when the user disconnects so stale IDs are not reused after re-login. 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>(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 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 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 ---- /// @@ -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,