Error at create product api

Hi,

I’m trying to test the create product api but I get an error, am I doing something wrong?

niv@Nivs-MBP ~ % curl -i -X POST -H "Content-Type: application/json" -b cookies.txt -d '{"name":"New product", "category.id":"ff8080819068f394019077a4d0c40069"}' {baseurl}/openboxes/api/products
HTTP/2 500 
date: Wed, 03 Jul 2024 08:24:31 GMT
content-type: application/json;charset=UTF-8
x-application-context: application:production
cf-cache-status: DYNAMIC
server: cloudflare
cf-ray: 89d57acc7bb74bf7-MXP

{"errorCode":500,"cause":"java.lang.IllegalArgumentException","errorMessage":"Invalid action name: index"}

Besides, can I reach the action of create product, create stock for it and assign serial number for each of the products in the stock by the API or it is possible only from the dashboard? I can’t always follow the dashboard actions, is it something limited?
Thanks.

Besides, can I reach the action of create product, create stock for it and assign serial number for each of the products in the stock by the API or it is possible only from the dashboard? I can’t always follow the dashboard actions, is it something limited?

I’ll put together the API request workflow I would use to do this. And if I can figure out how to share it publicly, I’ll create a collection on Postman. Otherwise, I’ll post the sample requests here.

You’re using 0.9.x, correct?


:information_source: Lastly, I wanted to address the overarching concern of your questions:

What is up with this API, man?

I’ve been doing a lot of self-reflection over the last few months and I have a bit more clarity about how I want to move the project forward, particularly concerning the API. There are fundamental problems with how we’ve designed the API to date. For example, most endpoints are created on an as-needed basis and they are tightly coupled to whatever UI we are building. But we’re taking some steps to address the lack of design and I’m starting to see a path forward that will hopefully make the API more complete and developer-friendly. Unfortunately, I haven’t had enough time to make progress on these topics recently. But given interest from community members like you, @David_Douma, @Domas, @gorbyhail and others, I think it’s a good time to create a plan of action and make some improvements on this front. I’ll try to lay that out in a separate post, but will start addressing some of the issues that are blocking y’all at the moment.

1 Like

In 0.9.x, there seems to be a bug with the default URL mapping that causes the error you encountered when POSTing to the Products API.

Invalid action name: index

Note: I haven’t tested it with 0.8.x, but I’m almost positive it works (not to say you should downgrade; I just wanted to state that this feels like a regression).

I used the generic Product API, and it seems to be working fine.

Here’s the cURL for that request (change the hostname and jsessionid accordingly)

curl --location 'https://<hostname>/openboxes/api/generic/product' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Cookie: JSESSIONID=<jsessionid>' \
--data '{
    "category": { "id": "ROOT" },
    "productType": { "id": "DEFAULT" },
    "name": "Sausages, Plastic, plum, Intelligent, each"
}'

I’ll work on the other requests as i have time.

@jmiranda Ok, thanks for the answer. It will be greate to put more clarity on how the api is limited comparing to the openboxes dashboard. I have to figure out if I can use the service for my requirements.

For now, can you please give some more specific details if its possible to create a stock of a product over the API?

Creating stock usually requires the developer to do some heavy lifting across multiple APIs because the workflows for inbounding stock are complicated. There are purchase orders, inbound stock movements, and inbound returns that act as the pending record of what’s been ordered and/or in-transit (these each have separate APIs). Upon delivery, the system converts those objects into receipts (using the Receipt API). Receipts create low-level transactions that become inventory items. These inventory items are usually transferred into their final storage location using putaways (using the Putaway API).

Note: Let me know if you’re interested in learning more about any of those aforementioned APIs, and I will point you in the right direction. These APIs are the safer and more complete approach, while the following approach is a bit more experimental. But once we learn how you plan to use the API we’ll be able to build more coarse-grained REST and state machine endpoints for those use cases.

Since you’re likely just looking to build a quick stock count or stock adjustment feature, we do expose a way to create low-level transactions through the Generic API.

The following request would create a “stock count” (what we call a “product inventory”) transaction, which acts as a baseline count for a set of inventory items, usually for a single product, but you can include multiple products if desired. These transactions reset the inventory, so only use them in the case where you know the count for all inventory items of a product.

curl --location 'https://<REDACTED>/openboxes/api/generic/transaction' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Cookie: JSESSIONID=<REDACTED>' \
--data '{
    "inventory": "1",
    "transactionType":"11",
    "transactionDate": "07/07/2024 00:27:00", 
    "transactionEntries": [
        {
            "product": "ff808181907703450190797770480000",
            "inventoryItem": "ff8081819077034501908b6f3b1f0020",
            "binLocation": "09f5e1168103c8ba0181496a1a416df2",
            "quantity": "1"
        }
    ]
}'

This creates a “stock count” transaction for a product that was created using the API.

There are other transaction types besides the Stock Count (Product Inventory) transaction:

  • Adjustment (Credit)
  • Adjustment (Debit)
  • Consumption (Debit)
  • Damaged (Debit)
  • Expired (Debit)

And you can add others as well. For example, perhaps you want to track stock that was Scrapped or Destroyed. Just remember that all transaction types are either debit or credit, so quantities should always be positive integers.

Lastly, if you ever use Transfer In or Transfer Out, note that for these transactions, you should provide a source and destination, respectively. They might not be required, but it’s a good idea to provide them just so you know the intended from/to.

Limitations

The biggest pains with this approach are:

  1. We don’t use the Generic API much, so it’s not very well tested at this point.

  2. Creating transactions using the Generic API only seems to work with 0.9.x releases.

  3. You’ll need to find or create an inventory item (product, lot, expiry date combination) for each of your transaction entries. If you’re only updating the stock count for a single inventory item then this isn’t a big deal.

    But updating multiple inventory items may require you to send multiple requests for each, which adds some chattiness to the conversation.

    Note: the API should allow you to provide a product code (or product ID), a lot number (or inventory item ID), and an optional expiration date in the Create Transaction request body and the API should take care of the “find or create” logic for you.

    With that said, the find or create inventory item requests are painless.

    a. Creating new inventory items is fairly easy to do at the moment.

     curl --location 'https://<REDACTED>/api/generic/inventoryItem' \
     --header 'Accept: application/json' \
     --header 'Content-Type: application/json' \
     --header 'Cookie: JSESSIONID=<REDACTED>' \
     --data '{
         "product": "ff808181907703450190797770480000",
         "lotNumber": "ABC123",
         "expirationDate": "03/31/2028"
     }'
    

    b. Retrieving can probably be done a few different ways but this is the most straightforward.

     curl --location 'https://<REDACTED>/openboxes/api/products/ff808181907703450190797770480000/inventoryItems/ABC123' \
     --header 'Accept: application/json' \
     --header 'Content-Type: application/json' \
     --header 'Cookie: JSESSIONID=<REDACTED>'
    
  4. All transactions require a reference to an inventory record. Thankfully, every location that manages inventory has a foreign key reference to an inventory record. Unfortunately, we don’t seem to expose this required “inventory_id” anywhere in the UI or API, so you’ll need to hardcode the value (or maintain a location/inventory map in your client application until we have a chance to resolve some of the known limitations of the API.

    Here’s the query to find the inventory ID for a given location.

     mysql> select id, name, inventory_id from location where name = 'Distribution Center' \G
     *************************** 1. row ***************************
               id: 8a8a9e9665c4f59d0165c54ec6b10027
             name: Distribution Center
     inventory_id: 8a8a9e9665c4f59d0165c54ec6b10028
     1 row in set (0.01 sec)
    
  5. Just as I was finishing this post I realized that the system seems to be parsing the transactionDate field in the Create Transaction request as a date instead of a datetime. Therefore it doesn’t include a timestamp so it is saved to the database as midnight on the given date. This may not be an issue because we differentiate transactions based on the transaction date, as well as the date the transaction was created. Therefore, it might not lead to a bug, but it may look confusing to the end user with all of the transactions piling up on the same date.

@jmiranda This might be a game changer; it sounds like it can cover my requirements!

Do you think someone can look into the mentioned error, or is there a 0.9.x version without this regression?

Thanks, much appreciated!

Do you think someone can look into the mentioned error, or is there a 0.9.x version without this regression?

It’s not necessarily a regression but a limitation of our default configuration. The current data binding mechanism is configured to handle all date and datetime input using the following date format, by default.

image

I assume most of our APIs customize the date format used for data binding when a date and time are required, while the Generic API uses the default data binding config.

As for timing … we just started regression testing for 0.9.2 and this is too big of a change to make so close to release. So, if this is the solution it’s probably going be merged into the develop branch and available in a 0.9.3-SNAPSHOT release.

As I wrote this response, I realized that you could potentially customize the configuration to include the additional date formats for your own implementation.

So, depending on whether you use openboxes.yml or openboxes.groovy, the configuration might look like this

openboxes.yml

grails:
  databinding: 
    dateFormats: 
      - 'MM/dd/yyyy'
      - 'MM/dd/yyyy HH:mm:ss'

openboxes.groovy

grails.databinding.dateFormats = ['MM/dd/yyyy', 'MM/dd/yyyy HH:mm:ss']

I’m going to test this out and will confirm whether it works. The ordering of the date formats probably matters so I’m going to play around with that as well.

Ok, I added the following configuration to my local config file (~/.grails/openboxes.groovy)

grails.databinding.dateFormats = ["MM/dd/yyyy", "MM/dd/yyyy HH:mm:ss"]

Then I wrote a quick endpoint to test whether the date binding worked as expected.

Result: :x:

Ok, so that’s probably related to the ordering of the date formats (8 The Web Layer 5.2.1), so let’s put the datetime format ahead of the date format.

grails.databinding.dateFormats = ["MM/dd/yyyy HH:mm:ss", "MM/dd/yyyy"]

and test again …

Result: :white_check_mark:

Yay! Ok, so for the moment of truth. Let’s see what happens when we test the data binding used by the Generic API.

Request

curl --location 'https://<REDACTED>/openboxes/api/generic/transaction' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Cookie: JSESSIONID=<REACTED>' \
--data '{
    "inventory": "1",
    "transactionType":"11",
    "transactionDate": "07/08/2024 12:03:00", 
    "transactionEntries": [
        {
            "product": "ff808181907703450190797770480000",
            "inventoryItem": "ff8081819077034501908b6f3b1f0020",
            "binLocation": "09f5e1168103c8ba0181496a1a416df2",
            "quantity": "1"
        }
    ]
}'

Response (excluded details for the sake of brevity):

{
    "data": {
        "id": "ff808181909386e80190938a90140000",
        "dateCreated": "2024-07-08T18:10:50Z",
        "lastUpdated": "2024-07-08T18:10:50Z",
        ...
        "transactionDate": "2024-07-08T17:03:00Z",
        ...
    }
}

Result :white_check_mark: :open_mouth:

To conclude, this means you can add whatever date formats you want to support for your API client(s).

I would recommend adding the default Grails date formats and only configuring custom date formats if necessary.

@jmiranda Hey thanks Justing.

Is it also referring to the 500 error I get at create product endpoint I mentioned?

niv@Nivs-MBP ~ % curl -i -X POST -H "Content-Type: application/json" -b cookies.txt -d '{"name":"New product", "category.id":"ff8080819068f394019077a4d0c40069"}' {baseurl}/openboxes/api/products
HTTP/2 500 
date: Wed, 03 Jul 2024 08:24:31 GMT
content-type: application/json;charset=UTF-8
x-application-context: application:production
cf-cache-status: DYNAMIC
server: cloudflare
cf-ray: 89d57acc7bb74bf7-MXP

{"errorCode":500,"cause":"java.lang.IllegalArgumentException","errorMessage":"Invalid action name: index"}

You’ll need to use the Generic API to create products until we fix the bug with the URL mapping related to the Product API. See the following comment for more details.

@jmiranda Ok, the generic create product API seems to work fine!

Lastly, if you ever use Transfer In or Transfer Out, note that for these transactions, you should provide a source and destination, respectively. They might not be required, but it’s a good idea to provide them just so you know the intended from/to.

Another issue that I have noticed is that while creating a Transfer Out transaction, I am still receiving a successful response even though I have no stock for the inventory item in my inventory, causing the stock to go negative. I need to ensure that if this inventory item is already depleted, it should not be possible to transfer it out. Additionally, I would expect the system to handle race conditions appropriately to prevent such inconsistencies. Is it possible?

This might not be easy to explain or understand. But when working with low-level, fine-grained APIs like the generic transaction API, validation becomes your responsibility. We’re exposing these APIs as building blocks—basically, as a way to push data into the system at the most atomic level. The transaction API has no knowledge of available items. It’s just there to push transactions into the system.

That said, this type of validation exists within the more coarse-grained APIs like Stock Movement, Receiving, and Putaway. These APIs use the building block APIs (services) to perform the work of saving transactions to the database, but they first validate that it’s okay to save these transactions based on what services like the ProductAvailabilityService tell them about available quantities.

For example, the Stock Movement API performs an available quantity validation as part of two state transitions (when items are allocated to an order and when items are shipped).

Eventually, we might add a coarse-grained API for performing more simple stock transfers between facilities or from a warehouse to a customer, but as far as we’re concerned we already have that in the Stock Movement API. If you want the benefit of quantity validation, you’d use that. Or you’ll need to build your own validation.

One way to do that would be to manually check product availability before you create a transaction to make sure the inventory item you are attempting to transfer has quantityAvailable >= quantityToTransfer.

GET /api/products/:productId/availableItems?location.id=:locationId

@jmiranda Ok, your answer explains exactly what I thought so.
I’ll check for the best way to work this out.

I’m getting some errors trying to set stockMovement, I’ll open another issue for that.