Skip to content

Full outbound import#4786

Merged
awalkowiak merged 22 commits intodevelopfrom
feature/outbound-import
Sep 6, 2024
Merged

Full outbound import#4786
awalkowiak merged 22 commits intodevelopfrom
feature/outbound-import

Conversation

@kchelstowski
Copy link
Collaborator

A PR from feature branch to develop, that contains the whole full outbound import feature.

@kchelstowski kchelstowski added status: work in progress For issues or pull requests that are in progress but not yet completed warn: do not merge Marks a pull request that is not yet ready to be merged warn: do not squash Apply to any pull request whose commits shouldn't be squashed upon merging domain: frontend Changes or discussions relating to the frontend UI domain: backend Changes or discussions relating to the backend server labels Aug 13, 2024
@kchelstowski kchelstowski self-assigned this Aug 13, 2024
@github-actions github-actions bot added type: feature A new piece of functionality for the app domain: l10n Changes or discussions relating to localization & Internationalization labels Aug 13, 2024
@kchelstowski kchelstowski force-pushed the feature/outbound-import branch from b0412f8 to 754af4e Compare August 14, 2024 09:44
Copy link
Member

@jmiranda jmiranda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well done, @kchelstowski.

action = [POST: "createOutbound"]
}

"/api/fulfillments/validate" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't remember if this is true or not, so bear with me.

Is it possible now (or in the future) for us to have validation for different stages of the fullfillment process?

Notes to self:

  • If so we might want to might want to break this out into separate validation steps.
  • Figure out if this is validating the imported data or a JSON object that was created for a step in the workflow to be validated (i.e. validate the fulfillment request before it is created to the database.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now we proceed the whole action as one ("full outbound import"), so for now (and I believe forever for this feature) we would have only one validation (not including the one, that validates the file).

import org.pih.warehouse.outbound.ImportPackingListErrors
import org.pih.warehouse.outbound.ImportPackingListItem

class FulfillmentApiController {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not seeing what I wanted to see from this API. It's supposed to be an API to handle fulfillment activities, not the data import. The idea was that we'd have an API to handle the data import process, we'd transform the imported data into requests for the fulfillment API to handle creating and fulfilling the order. But the Fulfillment API was just going to be focused on fulfillment.

Since that's not really the direction we're heading let's just call this ImportPackingListController or something along those lines. I would argue that it's not an API because the input is a multi-part request (file upload).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it doesn't handle the data import. The data import is handled by the PackingListController. Here, we only validate and save the proper JSONs.

FulfillmentService fulfillmentService

def validate(ImportPackingListCommand command) {
ImportPackingListErrors errors = new ImportPackingListErrors()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be internal to the ImportPackingListCommand.


def validate(ImportPackingListCommand command) {
ImportPackingListErrors errors = new ImportPackingListErrors()
if (command.hasErrors()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume the validate is being called when the ImportPackingListCommand is bind'd, but is there any harm in making that explicit here for readability?

I assume if we left the command object as an argument then there's potentially a lot of database queries happening in the line item constraints.

So we'd have to go with the following approach instead.

def validate() { 
    ImportPackingListCommand command = new ImportPackingListCommand(...whatever...)
    if (command.validate() && command.hasErrors()) { 
        response.status = 400
        render([errorCode: 400, errorMessages: command.errorMessages] as JSON)
    }
}

And then in ImportPackingListCommand you can override the getErrorMessages() method run your buildErrors code.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commands are validated automatically, hence adding an additional validate would make the command validate twice, which I believe doesn't have sense and would decrease the performance.
I think it is commonly known that the commands are bound and validated at the bind time. I created a document about that some time in the past (the one about command objects).

buildErrors(command, errors)
}
List<Map> tableData = command.packingList.collect { ImportPackingListItem item -> item.toTableJson() }
render([errors: errors, data: tableData] as JSON)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we need a separate toTableJson() method? My assumption is that you're adding it because of the rowId, which might feel separate from the other properties in the model (i.e. those coming from the Excel file). If that's true then I would argue that rowId is a fundamental property of ImportPackingListItem. It might be a derived property (an autoincremented index derived by the order in which the imported data row was ingested / processed). But it's still part of the input model. And as such that model has a JSON representation that should be rendered by the toJson() method, the same as a domain.

So just add toJson() methods to ImportPackingList and ImportPackingListItem, then register the toJson as marshallers in Bootstrap.groovy.

And return the whole command object as data.

render([errors: command.errors, data: command] as JSON)

If there's an actual difference between ImportPackingListItem.toJson() and toTableJson() then we probably need a different representation (i.e. a different command and toJson() for what is being rendered via the toTableJson() method).


Tech Huddle Discussion for later

I understand the need for having two

render([errors: errors, data: tableData] as JSON)

We could come up with a convention in which we return a single envelop when we need the data to be returned, in addition to the errors. For example, we could just return the data and the command object would be responsible for rendering its errors.

response.status = 400
render([data: command] as JSON)

The response code tells the client that it should expect either a response with errors or a response with data that may contain errors.

The toJson() would then just need to render the errors.

Map toJson() {
    return [
        errors: hasErrors() ? buildErrors() : []
    ]
} 

No need to implement this now unless it simplies something for you.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally we don't explicitly follow the JSON API specification (https://jsonapi.org). At least not yet. But I would like to move in that direction. And the JSON API says ...

The members data and errors MUST NOT coexist in the same document.

Source: https://jsonapi.org/format/#document-top-level

We still need to solve the problem at hand (i.e. we need both the errors and input data in the response). So I think we could ignore this until/unless we find a better solution.

Copy link
Collaborator Author

@kchelstowski kchelstowski Aug 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The table json is needed due to some requirements provided by Kasia and the table itself.
As you know, we bind domains via id, so for example when sending packingList, we provide product ids, so the payload looks like:

{
  packingList: [
    { product: 10001, ...},
    { product; 10002, ...},
  ]
}

so if we were about to rely on this payload to display the data in the table, we would not be able to display the product name, this is where the toTableJson is needed - since we bound the product at the bind time, we return it to the client, so that we can display the: product name, bin location name, expiration date associated with the inventory item in the table:

[
        rowId: rowId,
        product: [
            id: product?.id,
            name: product?.name,
            productCode: product?.productCode
        ],
        binLocation: binLocation?.name,
        palletName: palletName,
        boxName: boxName,
        lotNumber: lotNumber,
        quantityPicked: quantityPicked,
        recipient: recipient?.name,
        expirationDate: expirationDateToDisplay,
        origin: [
            id: origin?.id,
        ],
]


UploadService uploadService

def importPackingList(ImportDataCommand command) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is the PackingListController, we should remove "PackingList" from the method name. However, since "import" is a reserved word, we should call this method upload().

And since we're going to get rid of the FulfillmentApiController for now, let's just move those other methods here. That way everything is together.

return "$firstName ${anonymize ? lastInitial : lastName}"
}

static Person findPersonByNameOrEmail(String regex) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convention: If the method is in a domain, the method name should not include the name of the domain.

Person findByNameOrEmail(String searchTerms)

Editorial Note: We should have lots of tests for this to show what is possible and what is not possible.

Lastly, "regex" seems unintutitive for what is being passed here. The argument is searchTerms not a regular expression, right?

}


List<Map> toJson() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to remove this as long as we change the render to reference the

render([data: importer.data] as JSON)

return null
}

static String removeWhiteSpace(String s) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export const PICKLIST_CLEAR = id => `${PICKLIST_API}/${id}/items`;

// FULL OUTBOUND IMPORT FEATURE
export const FULFILLMENT_API = `${API}/fulfillments`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned in the UrlMappings, since the Fulfillments API hasn't been fleshed out as well as I'd like let's make this a little more straightforward by using the PackingListController for everything.

MultipartFile importFile = command.importFile
File localFile = uploadService.createLocalFile(importFile.originalFilename)
importFile.transferTo(localFile)
DataImporter packingListImporter = new PackingListExcelImporter(localFile.absolutePath)
Copy link
Member

@jmiranda jmiranda Aug 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One other thing we can do to improve the code would be to get rid of the file handling above and just use an input stream. This avoids the "bit of mess" and also starts us down the path of removing the file system as a dependency

Tech Debt: The application shouldn't write to the local file system.

The way to do that would be to create a new constructor (or replace the existing one)

PackingListExcelImporter(InputStream inputStream) {
    super()
    read(inputStream)
    excelImportService = Holders.grailsApplication.mainContext.getBean("excelImportService")
}

and then replace the code above with these two lines, maybe with some validation of the command for obvious validation errors like an empty file.

DataImporter dataImporter = new PackingListExcelImporter(command.importFile.inputStream)
render([data: dataImporter.data] as JSON)

As far as I recall, we didn't have that available to us in Grails 1.3.9 but the read(InputStream) is there in AbstractExcelImporter in the Grails 3 version of the plugin.

@github-actions github-actions bot added the flag: schema change Hilights a pull request that contains a change to the database schema label Sep 4, 2024
@awalkowiak awalkowiak changed the title WIP: Full outbound import Full outbound import Sep 6, 2024
@awalkowiak awalkowiak merged commit 695a6fe into develop Sep 6, 2024
@awalkowiak awalkowiak deleted the feature/outbound-import branch September 6, 2024 12:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain: backend Changes or discussions relating to the backend server domain: frontend Changes or discussions relating to the frontend UI domain: l10n Changes or discussions relating to localization & Internationalization flag: schema change Hilights a pull request that contains a change to the database schema status: work in progress For issues or pull requests that are in progress but not yet completed type: feature A new piece of functionality for the app warn: do not merge Marks a pull request that is not yet ready to be merged warn: do not squash Apply to any pull request whose commits shouldn't be squashed upon merging

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants