Open alexhauser opened 1 year ago
In particular, this code in my app is not working anymore since switching to the latest release (I was a bit behind in releases, so this might as well have been caused by a breaking change in a previous update).
var fulfillment = new Fulfillment()
{
TrackingCompany = "DPD",
TrackingUrl = String.Format(Settings.Default.ShopifyDPDTrackingUrl, mTrackingNr),
TrackingNumber = mTrackingNr,
LocationId = mShopconfig.FulfillmentLocationId,
NotifyCustomer = true
};
fulfillment = await mFulfillmentService.CreateAsync(mOrder.Id.Value, fulfillment);
Can anyone please give a hint on how this code should look like nowadays?
I am aware that I can create FulfillmentShipping
and pass that to FulfillmentService.CreateAsync()
, however I don't seem to understand how that then relates to a particular order, since nowhere in this process I need to pass in an order id.
Thanks for the awsome work in this project btw!
Thanks for the heads up! That definitely needs to be updated. Fulfillments are slightly more complicated now, you need to work with the FulfillmentOrderService
and the FulfillmentService
to fulfill things. To put it simply, once an order is created Shopify will create one or more fulfillment orders automatically for the order and then it's up to the fulfiller to pick out those fulfillment orders, add line items to them, and then use the FulfllmentShipping
class to create a fulfillment.
You can find our tests for the FulfillmentService
right here, but here's some example code for how you'd find fulfillment orders and create a fulfillment:
var fulfillments = new FulfillmentService(domain, accessToken);
var fulfillmentOrders = new FulfillmentOrderService(domain, accessToken);
var orderId = 123456;
// Find open fulfillment orders for this order
var openFulfillmentOrders = await fulfillmentOrders.ListAsync(orderId);
openFulfillmentOrders = openFulfillmentOrders.Where(f => f.Status == "open").ToList();
// Fulfill the line items
var lineItems = openFulfillmentOrders.Select(o => new LineItemsByFulfillmentOrder
{
FulfillmentOrderId = o.Id.Value
// Optionally specify a list of line items if you're doing a partial fulfillment
// FulfillmentRequestOrderLineItems = ...
});
var fulfillment = await fulfillments.CreateAsync(new FulfillmentShipping
{
Message = "items are shipping!",
FulfillmentRequestOrderLineItems = lineItems,
NotifyCustomer = true,
TrackingInfo = new TrackingInfo
{
Company = "UPS",
Url = "...",
Number = "..."
}
});
Let me know if that answers your question!
Edit
A note on partially fulfilling line items. The ShopifySharp.FulfillmentRequestOrderLineItem.Id
is not the same as the fulfillmentLineItem.LineItemId
value. This is not what Shopify is expecting when you're partially fulfilling a fulfillment order, and if you use it Shopify will return an error. What they actually want is the fulfillmentLineItem.Id
.
Yes, it's confusing.
Here's an example that I'm using in a guide I'm writing for using ShopifySharp to do fulfillments. In the example, the user will be able to select which line items in a single fulfillment order (not an order, a fulfillment order) will be fulfilled.
Here's how we'd do that:
var fulfillmentService = new FulfillmentService(domain, accessToken);
var fulfillmentOrderService = new FulfillmentOrderService(domain, accessToken);
var fulfillmentOrderId = 123456789L;
// Get this fulfillment order
var fulfillmentOrder = await fulfillmentOrderService.GetAsync(fulfillmentOrderId);
// In this example, we let the user choose which line items in this particular fulfillment order will be fulfilled
// Where request = some kind of form post or json model
long[] itemIds = request.ItemsToFulfill;
// Wrap the desired line items together with the fulfillment order id
var fulfillmentOrderLineItems = new LineItemsByFulfillmentOrder
{
FulfillmentOrderId = fulfillmentOrderId,
// This is where you do a partial fulfillment. Using this property means you're telling Shopify "only fulfill these parts of the fulfillment order".
FulfillmentRequestOrderLineItems = itemIds.Select(itemId => new FulfillmentRequestOrderLineItem
{
// NOTE: This Id property is not the same as a `FulfillmentOrderLineItem.LineItemId` value!
// You want to use `FulfillmentOrderLineItem.Id` instead.
Id = itemId,
// You need to tell Shopify how much of this single item to fulfill. You must ensure that
// you are not fulfilling more than the fulfillable quantity, or else Shopify will throw an error.
// In this example, we just want to fulfill all of the available line item, so we pass in the
// entire fulfillable quantity.
Quantity = fulfillmentOrder.FulfillmentOrderLineItems
.First(li => li.Id == itemId)
.FulfillableQuantity
})
};
// Create a new fulfillment using that fulfillment order with partial line items
var fulfillment = await fulfillments.CreateAsync(new FulfillmentShipping
{
Message = "items are shipping!",
FulfillmentRequestOrderLineItems = new [] { lineItem },
NotifyCustomer = true,
TrackingInfo = new TrackingInfo
{
Company = "UPS",
Url = "...",
Number = "..."
}
});
Thanks to @ajvanlaningham below for pointing out the LineItemId
gotcha. That caused me trouble in production.
Thank you so much, that's exactly what I was looking for!
Great! I'll keep this open as a reminder for me to get the fulfillment example code updated.
Thanks for the update, What is the practice when we have multipe TrackingNumbers/Urls? before we could send them as an array. Now the FulfillmentShipping only supports a single "TrackingInfo".
I could create multiple fulfillments for each tracking number, but this is a problem for many of my customers, because this will trigger some payment providers to create multiple invoices.
For anyone who might stumble across this issue after ugrading to the 2022-07 API-version:
If fulfillmentOrders.ListAsync(orderId)
comes back empty and returns no FulfillmentOrder
s, the problem for me was that I had not granted my app access to some of the newly added fulfillment-related API-permissions like read_merchant_managed_fulfillment_orders
or write_merchant_managed_fulfillment_orders
.
It's weird that the call just succeeds and returns an empty array, instead of giving a permission error, but that's exactly what happens. (I know this is the Shopify API's fault, ShopifySharp has nothing to do with this).
Hope this will save someone the hassle of figuring this out the hard way. 😉
I noticed in the fulfillment example code, there is a ToDo list which is still incomplete. For example as of writing this, 'Mark fulfillment order as open' is not checked as complete. Does that mean the code still needs working on, or just the documentation?
@nozzlegear, my app uses the current 2022-07
API release of ShopifySharp and I'm following the fulfillment example code that you have provided a few posts above this one (so NOT the deprecated one in the readme).
But today, I was notified by Shopify, that...
TL;DR: Your custom app is leveraging the deprecated Fulfillment API. Please note that the Fulfillment API will stop working as intended on March 31st, 2023 in favor of our new Fulfillment Order API.
Is it possible that the FulfillmentService
class or any other part of the library is still using the now deprecated Fulfillment API
under the hood?
This is the code I'm using:
private async Task FulfillOrder()
{
var openFulfillmentOrders = await mFulfillmentOrderService.ListAsync(mOrder.Id.Value);
openFulfillmentOrders = openFulfillmentOrders.Where(f => f.Status == "open").ToList();
var lineItems = openFulfillmentOrders.Select(o => new LineItemsByFulfillmentOrder()
{
FulfillmentOrderId = o.Id.Value
});
var fulfillmentShipping = new FulfillmentShipping()
{
NotifyCustomer = true,
Message = "Ihre Bestellung wird nun versendet!",
FulfillmentRequestOrderLineItems = lineItems,
TrackingInfo = new TrackingInfo()
{
Company = "DPD",
Url = String.Format(Settings.Default.ShopifyDPDTrackingUrl, mTrackingNr),
Number = mTrackingNr
}
};
var fulfillment = await mFulfillmentService.CreateAsync(fulfillmentShipping);
}
Thanks in advance for your input!
Thanks for the heads up! That definitely needs to be updated. Fulfillments are slightly more complicated now, you need to work with the
FulfillmentOrderService
and theFulfillmentService
to fulfill things. To put it simply, once an order is created Shopify will create one or more fulfillment orders automatically for the order and then it's up to the fulfiller to pick out those fulfillment orders, add line items to them, and then use theFulfllmentShipping
class to create a fulfillment.You can find our tests for the
FulfillmentService
right here, but here's some example code for how you'd find fulfillment orders and create a fulfillment:var fulfillments = new FulfillmentService(domain, accessToken); var fulfillmentOrders = new FulfillmentOrderService(domain, accessToken); var orderId = 123456; // Find open fulfillment orders for this order var openFulfillmentOrders = await fulfillmentOrders.ListAsync(orderId); openFulfillmentOrders = openFulfillmentOrders.Where(f => f.Status == "open").ToList(); // Fulfill the line items var lineItems = openFulfillmentOrders.Select(o => new LineItemsByFulfillmentOrder { FulfillmentOrderId = o.Id.Value // Optionally specify a list of line items if you're doing a partial fulfillment // FulfillmentRequestOrderLineItems = ... }); var fulfillment = await fulfillments.CreateAsync(new FulfillmentShipping { Message = "items are shipping!", FulfillmentRequestOrderLineItems = lineItems, NotifyCustomer = true, TrackingInfo = new TrackingInfo { Company = "UPS", Url = "...", Number = "..." } });
Let me know if that answers your question!
Hi @nozzlegear , according to you "once an order is created Shopify will create one or more fulfillment orders automatically for the order" if i call "ListAsync" function from FulfillmentOrderService i must receive some response, but my response list is empty; maybe it's because i don't use 2022-07 api? where i can see the API version i'm using?
EDIT: Sorry didn't see this response "If fulfillmentOrders.ListAsync(orderId) comes back empty and returns no FulfillmentOrders, the problem for me was that I had not granted my app access to some of the newly added fulfillment-related API-permissions like read_merchant_managed_fulfillment_orders or write_merchant_managed_fulfillment_orders." after setting this permissions correctly work
Am I missing how/where to set the Fulfillment Location with the new objects?
Am I missing how/where to set the Fulfillment Location with the new objects?
same here
A NOTE ABOUT PARTIAL FULFILLMENT: It took a day for me to figure out so I wanted to expand on this thread for anyone who might find it helpful.
var fulfillments = new FulfillmentService(domain, accessToken);
var fulfillmentOrders = new FulfillmentOrderService(domain, accessToken);
var orderId = 123456;
//*** NEW CODE ***
var lineItemId = 654321; //the long? ShopifySharp.LineItem.Id of the item that you want to partially fulfill
// Find open fulfillment orders for this order
var openFulfillmentOrders = await fulfillmentOrders.ListAsync(orderId);
openFulfillmentOrders = openFulfillmentOrders.Where(f => f.Status == "open").ToList();
//***NEW CODE ***
//grab the open fulfullment order list of line items
var fulfullmentLineItems = openFulfillments.FirstOrDefault().FulfillmentOrderLineItems.ToList();
var fulfillmentLineItem = fulfillmentLineItems.Find(li => li.lineItemId == lineItemId); //pick the correct line item
// Fulfill the line items
var lineItems = openFulfillmentOrders.Select(o => new LineItemsByFulfillmentOrder
{
FulfillmentOrderId = o.Id.Value,
//***NEW CODE***
//Specify a list of line items if you're doing a partial fulfillment for
FulfillmentRequestOrderLineItems = new List<FulfullmentRequestOrderLineItems>
{
new ShopifySharp.FulfillmentRequestOrderLineItem
{
Id = fulfillmentLineItem.Id, //NOTE: this is not fulfillmentLineItem.LineItemId!!
Quantity = ... // The quantity you want to partially fulfill.
}
}
});
var fulfillment = await fulfillments.CreateAsync(new FulfillmentShipping
{
Message = "items are shipping!",
FulfillmentRequestOrderLineItems = lineItems,
NotifyCustomer = true,
TrackingInfo = new TrackingInfo
{
Company = "UPS",
Url = "...",
Number = "..."
}
});
It took me a fair amount of time to figure out why I was getting an error, and ultimately it was because I was using the "FulfillmentOrderLineItem.LineItemId" instead of the "FulfillmentOrderLineItem.Id"... both of which are different from the actual ShopifySharp.LineItem.Id.
I can't see anything wrong with using the above code always, especially if you are doing work for a shopify store front in which partial fulfillments are a regular occurance. If it's a full fulfillment, just loop through all the items in the fulfillmentLineItems list and fill the full quantity. I'd love to hear from a more experienced developer if I'm wrong though!
Thanks Alex! I actually ran into that issue myself recently, I'll update the readme soon with a note about the line item ids specifically.
-- Joshua Harms
On Tue, Apr 25, 2023, at 09:01, Alex VanLaningham wrote:
A NOTE ABOUT PARTIAL FULFILLMENT: It took a day for me to figure out so I wanted to expand on this thread for anyone who might find it helpful.
`var fulfillments = new FulfillmentService(domain, accessToken); var fulfillmentOrders = new FulfillmentOrderService(domain, accessToken); var orderId = 123456; // NEW CODE var lineItemId = 654321; //the long? ShopifySharp.LineItemId of the item that you want to partially fulfill
// Find open fulfillment orders for this order var openFulfillmentOrders = await fulfillmentOrders.ListAsync(orderId); openFulfillmentOrders = openFulfillmentOrders.Where(f => f.Status == "open").ToList();
//grab the open fulfullment order var fulfullmentLineItems = openFulfillments.FirstOrDefault().FulfillmentOrderLineItems.ToList(); var fulfillmentLineItem = fulfillmentLineItems.Find(li => li.lineItemId == lineItem.Id); //pick the correct line item
// Fulfill the line items var lineItems = openFulfillmentOrders.Select(o => new LineItemsByFulfillmentOrder { FulfillmentOrderId = o.Id.Value //NEW CODE //Specify a list of line items if you're doing a partial fulfillment for FulfillmentRequestOrderLineItems = new List { new ShopifySharp.FulfillmentRequestOrderLineItem { Id = fulfillmentLineItem.Id, //NOTE: this is not fulfillmentLineItem.LineItemId!! Quantity = ... // The quantity you want to partially fulfill. } }; }); var fulfillment = await fulfillments.CreateAsync(new FulfillmentShipping { Message = "items are shipping!", FulfillmentRequestOrderLineItems = lineItems, NotifyCustomer = true, TrackingInfo = new TrackingInfo { Company = "UPS", Url = "...", Number = "..." } });`
It took me a fair amount of time to figure out why I was getting an error, and ultimately it was because I was using the "FulfillmentOrderLineItem.LineItemId" instead of the "FulfillmentOrderLineItem.Id".
I can't see anything wrong with using the above code always, especially if you are doing work for a shopify store front in which partial fulfillments are a regular occurance. If it's a full fulfillment, just loop through all the items in the fulfillmentLineItems list and fill the full quantity. I'd love to hear from a more experienced developer if I'm wrong though!
— Reply to this email directly, view it on GitHub https://github.com/nozzlegear/ShopifySharp/issues/828#issuecomment-1521843670, or unsubscribe https://github.com/notifications/unsubscribe-auth/AASOE7C5WIRVRPOQLJB72VTXC7KK7ANCNFSM6AAAAAATXYXVYI. You are receiving this because you were mentioned.Message ID: @.***>
If you're like me, and you commonly had multiple tracking #'s, my version of the code ended up looking like this:
public class ShopifyFulfillment
{
[JsonPropertyName("location_id")]
public long LocationId { get; set; }
[JsonPropertyName("tracking_numbers")]
public string[] TrackingNumbers { get; set; }
[JsonPropertyName("tracking_company")]
public string TrackingCompany { get; set; }
[JsonPropertyName("line_items")]
public ShopifyLineItem[] LineItems { get; set; }
}
public class ShopifyLineItem
{
[JsonPropertyName("id")]
public long Id { get; set; }
[JsonPropertyName("fulfillable_quantity")]
public int? FulfillableQuantity { get; set; }
[JsonPropertyName("fulfillment_service")]
public string FulfillmentService { get; set; }
[JsonPropertyName("fulfillment_status")]
public string FulfillmentStatus { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("product_id")]
public long? ProductId { get; set; }
[JsonPropertyName("quantity")]
public int? Quantity { get; set; }
[JsonPropertyName("requires_shipping")]
public bool? RequiresShipping { get; set; }
[JsonPropertyName("sku")]
public string SKU { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonIgnore]
public string TrackingNumber { get; set; }
}
public async Task FulfillOrder(List<ShopifyFulfillment> orderFulfillment, long orderId)
{
var fulfillments = new FulfillmentService(_config["shopifyBaseAddress"], _config["shopifyApiSecret"]);
var fulfillmentOrders = new FulfillmentOrderService(_config["shopifyBaseAddress"], _config["shopifyApiSecret"]);
// Find open fulfillment orders for this order
var openFulfillmentOrders = await fulfillmentOrders.ListAsync(orderId);
openFulfillmentOrders = openFulfillmentOrders.Where(f => f.Status == "open").ToList();
//grab the open fulfillment order list of line items
var fulfillmentLineItems = openFulfillmentOrders.FirstOrDefault().FulfillmentOrderLineItems.ToList();
foreach (var shipment in orderFulfillment)
{
var lineItems = new List<LineItemsByFulfillmentOrder>();
foreach (var shipmentLineItem in shipment.LineItems)
{
var fulfillmentLineItem = fulfillmentLineItems.Find(li => li.LineItemId == shipmentLineItem.Id);
var lineItem = openFulfillmentOrders.Select(o => new LineItemsByFulfillmentOrder
{
FulfillmentOrderId = o.Id.Value,
FulfillmentRequestOrderLineItems = new List<FulfillmentRequestOrderLineItem>
{
new() {Id = fulfillmentLineItem.Id, Quantity = shipmentLineItem.Quantity}
}
});
lineItems.AddRange(lineItem);
}
var fulfillment = await fulfillments.CreateAsync(new FulfillmentShipping
{
Message = "Your shipment is on it's way!",
FulfillmentRequestOrderLineItems = lineItems,
NotifyCustomer = true,
TrackingInfo = new TrackingInfo
{
Company = shipment.TrackingCompany,
Url = null,
Number = shipment.TrackingNumbers[0]
}
});
}
}
I am having 404 error while executing the following code.
var lineItems = fulfillment.LineItems.Select(x => new FulfillmentRequestOrderLineItem
{
Id = x.Id,
Quantity = x.Quantity,
});
var service = new FulfillmentService(acct.ShopifyURL, acct.ShopifyAccessToken);
var fulfillmentShipping = new FulfillmentShipping()
{
Message = "Items will be shipped now.",
NotifyCustomer = fulfillment.NotifyCustomer,
TrackingInfo = new TrackingInfo
{
Company = fulfillment.TrackingCompany,
Url = fulfillment.TrackingUrl,
Number = fulfillment.TrackingNumber,
},
FulfillmentRequestOrderLineItems = new List<LineItemsByFulfillmentOrder>()
{
new LineItemsByFulfillmentOrder()
{
FulfillmentOrderId = orderId,
FulfillmentRequestOrderLineItems = lineItems.ToArray()
}
}
};
fulfillment = await service.CreateAsync(fulfillmentShipping);
@z0ha1b That looks correct I think. One thing I'd investigate is that orderId
you have -- are you positive that's the fulfillment order's id, and not the full order's id? It has to be the fulfillment order.
@z0ha1b That looks correct I think. One thing I'd investigate is that
orderId
you have -- are you positive that's the fulfillment order's id, and not the full order's id? It has to be the fulfillment order.
thanks, it worked fine :)
Hi there :)
I'm sitting here for hours to get the initial sample from @nozzlegear to work (see here: https://github.com/nozzlegear/ShopifySharp/issues/828#issuecomment-1379795007). I'm using the basic version of the code (the first one) but I'm always getting a 404 from the API without any error message. I don't have any clue what I'm doing wrong. I also tried to enable all scopes but it simply doesn't work.
I also tried the code from the tests right here: https://github.com/nozzlegear/ShopifySharp/blob/6169078e4c2452e54ee0fef08d4f727fa78fea69/ShopifySharp.Tests/Fulfillment_Tests.cs
Is there a chance to get a more detailed error message since I'm desperatly trying for hours now and all I see is this 404?
I receive the correct line items and everything works as expected until I want to create the fulfillment itself :/
Thanks so much in advance :)
This is my current version:
var orderService = new ShopifySharp.OrderService(domain, accessToken);
var fulfillmentService = new ShopifySharp.FulfillmentService(domain, accessToken);
var fulfillmentOrderService = new FulfillmentOrderService(domain, accessToken);
// find order id for order number
var order = await orderService.ListAsync(new OrderFilterWithName { Name = orderIdString });
var orderId = order.Items.First().Id.Value;
var fulfillmentOrders = await fulfillmentOrderService.ListAsync(orderId);
var openFulfillmentOrders = fulfillmentOrders.Where(f => f.Status == "open").ToList();
var lineItems = openFulfillmentOrders.Select(o => new LineItemsByFulfillmentOrder
{
FulfillmentOrderId = o.Id.Value,
});
try
{
var fulfillment = await fulfillmentService.CreateAsync(new FulfillmentShipping
{
Message = "Items are shipping now!",
FulfillmentRequestOrderLineItems = lineItems,
NotifyCustomer = notifyCustomer
});
}
catch (ShopifyException e)
{
telemetryData.Add("Error message", e.Message);
telemetryClient.TrackEvent("OrderFulfillment", telemetryData);
return req.CreateResponse(HttpStatusCode.BadRequest);
}
Hey @PendelinP! Sorry to hear you're having trouble. If the code from the tests didn't work, I think my first suspicion would be to check the shop domain that you're using. Shopify will redirect requests and potentially cause 404s if you're not using the right myshopify domain.
Make sure the domain you're passing in to the service constructors looks like example.myshopify.com
and not example.com
. On top of that, we've recently found that even the myshopify domain can get redirected if a store has more than one of them. Check issue #968 and make sure that's not the case for you!
Hey @PendelinP! Sorry to hear you're having trouble. If the code from the tests didn't work, I think my first suspicion would be to check the shop domain that you're using. Shopify will redirect requests and potentially cause 404s if you're not using the right myshopify domain.
Make sure the domain you're passing in to the service constructors looks like
example.myshopify.com
and notexample.com
. On top of that, we've recently found that even the myshopify domain can get redirected if a store has more than one of them. Check issue #968 and make sure that's not the case for you!
Wow - that was super fast and this was exactly my problem (example.com instead of example.myshopify.com). I'm soooooo thankful! I would have never found this issue :D Again, thank you so, so much!
Awesome, glad it's working now @PendelinP!
Hello, would anyone know why all my fulfillment orders return as closed?
Hey @lcost0323, I think I've run into this before. If I recall correctly, there's a setting that new Shopify stores have where Shopify will automatically fulfill all fulfillment orders as soon as an order is placed. The store owner can turn this setting off in the store by going to the store's Settings Page -> Checkout -> Order Processing and selecting the option that says "Don't fulfill any of the order's line items automatically".
Alternatively, if you don't control the store (i.e. you're building an app for the Shopify App Store), you'll need to create a Fulfillment Service using the Fulfillment Service API instead. With the fulfillment service you can assign the store's products/inventory to your new fulfillment service's location, and after that Shopify should no longer fulfill them for you. That's kind of a simple overview of a fulfillment service, there's a lot more setup to it than that, but that's the gist of it.
Let me know if that solves the problem for you or if you have any questions about fulfillment services. Happy to help!
Hi @nozzlegear , thank you for getting back to me so quickly. It looks like we have it set to the gift card option and the order itself says unfulfilled. When these closed fulfillment orders come over, they also say unsubmitted. What I really want to do is fulfill, and then mark the order shipped/add tracking information so it gets closed out, but it seems like I have to create some kind of fulfilment order prior to updating the tracking information. I am very new to shopify.
This turned out to be a "me" issue.. I was using the wrong orderID, it is working now.
@lcost0323 Awesome, glad you got it working! Sorry I didn't reply to your follow up yesterday, I set a reminder to do so this morning but it looks like it's not necessary! Let me know if you have any other questions or problems getting it working. You can email me at the email address in my GitHub profile.
The fulfillment example code in the
readme.md
does not compile any more, since it does not incorporate the breaking changes introduced with the switch to the2022-07
API.https://github.com/nozzlegear/ShopifySharp#fulfillments