Skip to content

OBPIH-5932 Add validation for expiration date in inbound returns#4454

Merged
awalkowiak merged 3 commits intofeature/upgrade-to-grails-3.3.10from
OBPIH-5932-add-validation-for-expiration-date-in-inbound-returns
Jan 22, 2024
Merged

OBPIH-5932 Add validation for expiration date in inbound returns#4454
awalkowiak merged 3 commits intofeature/upgrade-to-grails-3.3.10from
OBPIH-5932-add-validation-for-expiration-date-in-inbound-returns

Conversation

@drodzewicz
Copy link
Collaborator

Some changes that are worth pointing out

Removed updateFormValues
After inspecting it closes I noticed that this function does not serve any purpose than to make the page probably slower and confuse the developer.

updateFormValues(values) {
  this.setState({ formValues: values });
}

Before, updateFormValues(values) would have been called on each table field onBlurr callback to update the state of formValues. We do not need to manually update the formValues, the ReactForm does it for us and keeps track of the state. When we perform any kind of a save on this addItemsPage, the state that we are providing as a payload for update request is values prop which is provided by renderProps of the react-final-form component, we never used formVlaues anywhere on the page but we were updating it for some reason.

Separated confirm modals into separate methods and made ten return promises instead
I had a little bit of a hard time trying to figure out how to handle two consecutive confirmation modals and got and idea.
So in javascript, in the browser, we have a window.confirm("Some question) which upon calling stalls the code in the browser and awaits users response, then returns a boolean value for which option the user clicked (ok or cancel).
I thought that I could mimic this behavior by making this confirmation return a promise and resolve to true when user clicks ok and false when user clicks cancel

Clean up save stock movement method
in all cases be it nextPage, save, save and exit, we are performing the same save actions with the same validation so I moved common code to the saveStockTransferInCurrentStep and call it in all of the mentioned methods. The difference in these methods are what happens after, for nextPage we trigger next page method for save we display a success alert and for save and exit we are redirecting to the show page.

For more information on the approach I took to validating expiration date please check out the comment under the ticket

…g the items in inbound return

- remove the updateFormValues() which was manually updating state of our form
- fetch inventoryItem of a given product and lot to check to validate expiration date
return this.saveStockTransfer(returnItems, null)
.catch(() => this.props.hideSpinner());
async nextPage(formValues) {
this.saveStockTransferInCurrentStep(formValues, this.state.inboundReturn.status !== 'PLACED' ? 'PLACED' : null)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it will be worth creating an enum with statuses including this PLACED instead of using magic strings

Comment on lines 370 to 386
for (const it of itemsWithLotAndExpirationDate) {
const { data } = await ProductApi.getInventoryItem(it.product?.id, it.lotNumber);
if (data.inventoryItem && data.inventoryItem.expirationDate !== it.expirationDate) {
isSomeItemsDontMatchExpirationDate = true;
break;
}
}

// After find at least a single instance where expiration date we are trying to save
// does not match the existing inventoryItem expiration date, we want to inform the user
// that certain updates to th expiration date in the system will be performed
if (isSomeItemsDontMatchExpirationDate) {
const isConfirmed = await this.confirmExpirationDateSave();
if (!isConfirmed) {
return Promise.reject();
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Wouldn't it be better to place this code from if (isSomeItemsDontMatchExpirationDate) { ... } directly in if (data.inventoryItem && data.inventoryItem.expirationDate !== it.expirationDate)?
Basically, you are trying to find at least one item that doesn't match the expiration date, so I think there is no need to use an additional let declaration, and then check if we change the value of this particular variable because within this loop we already know if we found something that doesn't match expiration date.
So I think it should be something similar to (not tested):

  for (const it of itemsWithLotAndExpirationDate) {
      const { data } = await ProductApi.getInventoryItem(it.product?.id, it.lotNumber);
      if (data.inventoryItem && data.inventoryItem.expirationDate !== it.expirationDate) {
       const isConfirmed = await this.confirmExpirationDateSave();
        if (!isConfirmed) {
          return Promise.reject();
        }
      }
  }

Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree with Alan's comment here

Comment on lines 408 to 419
if (hasErrors) {
const isConfirmed = await this.confirmValidationErrorOnExit();
if (!isConfirmed) {
return;
}
} else {
try {
await this.saveStockTransferInCurrentStep(formValues);
} catch (error) {
return;
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe it would be better to check if (!hasErrors) { ... return } to avoid nesting ifs, and because of that you can avoid using else 😆

Comment on lines 465 to 472
async previousPage(values, invalid) {
if (!invalid) {
this.saveStockTransferInCurrentStep(values.returnItems)
.then(() => this.props.previousPage(this.state.inboundReturn));
await this.saveStockTransfer(values.returnItems, null);
} else {
confirmAlert({
title: this.props.translate('react.inboundReturns.confirmPreviousPage.label', 'Validation error'),
message: this.props.translate('react.inboundReturns.confirmPreviousPage.message', 'Cannot save due to validation error on page'),
buttons: [
{
label: this.props.translate('react.inboundReturns.confirmPreviousPage.correctError.label', 'Correct error'),
},
{
label: this.props.translate('react.inboundReturns.confirmPreviousPage.continue.label', 'Continue (lose unsaved work)'),
onClick: () => this.props.previousPage(this.state.inboundReturn),
},
],
});
const correctErrors = await this.confirmValidationErrorOnPreviousPage();
if (correctErrors) {
return;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe return without else will be more readable?
+ with that change if (correctErrors) won't be nested in the else.

label: this.props.translate('react.default.no.label', 'No'),
},
],
confirmValidationErrorOnExit() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I regret that this component is not a functional one 😢
When it is functional we will be able to move all of those confirmation alerts to a hook, and make this component a little bit shorter (those alerts have close to 100 lines). Maybe other developers may have any idea how we can make this part of the code a little bit cleaner.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe other developers may have any idea how we can make this part of the code a little bit cleaner.

I can see that the only difference between those are either button labels or title/message.
We wouldn't achieve much by creating a generic function that would accept the title, message, buttons, because the amount of the props we'd have to provide would probably be the same, but I would have to see it after the refactor.

A second approach might be to have one alert method, and we could potentially have a map that would store the labels and we could access it depending on an alert key, e.g.:

const Alerts = {
  VALIDATION_ON_EXIT: 'VALIDATION_ON_EXIT',
  ...
};

const alertsMap = {
  [Alerts.VALIDATION_ON_EXIT]: {
    title: '...'
    message: '...',
    resolveButtonLabel: '...',
    rejectButtonLabel: '...',
  }
}

runConfirmAlert(key) {
  const labels = alertsMap[key];
  return new Promise((resolve) => {
     confirmAlert({
        title: labels?.title,
        message: labels?.message,
        willUnmount: () => resolve(false),
        buttons: [
              {
                label: labels?.resolveButtonLabel
                onClick: () => resolve(true),
              },
              {
                label: labels?.rejectButtonLabel
                onClick: () => resolve(false),
              },
        ],
    });
  });
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I decided not to proceed with it, I feel like generalizing it will make the code a bit less maintainable.
I know these methods are very similar but I think we will benefit more if we leave it separated as we have it now.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@drodzewicz OK. I've just wanted to show @alannadolny that there is a possibility to make it generic if we would want to. I'm fine with leaving it, as this component anyway needs to be rewritten anytime soon hopefully, but on the other hand I would not agree that my version makes it difficult to maintain.

label: this.props.translate('react.default.no.label', 'No'),
},
],
confirmValidationErrorOnExit() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe other developers may have any idea how we can make this part of the code a little bit cleaner.

I can see that the only difference between those are either button labels or title/message.
We wouldn't achieve much by creating a generic function that would accept the title, message, buttons, because the amount of the props we'd have to provide would probably be the same, but I would have to see it after the refactor.

A second approach might be to have one alert method, and we could potentially have a map that would store the labels and we could access it depending on an alert key, e.g.:

const Alerts = {
  VALIDATION_ON_EXIT: 'VALIDATION_ON_EXIT',
  ...
};

const alertsMap = {
  [Alerts.VALIDATION_ON_EXIT]: {
    title: '...'
    message: '...',
    resolveButtonLabel: '...',
    rejectButtonLabel: '...',
  }
}

runConfirmAlert(key) {
  const labels = alertsMap[key];
  return new Promise((resolve) => {
     confirmAlert({
        title: labels?.title,
        message: labels?.message,
        willUnmount: () => resolve(false),
        buttons: [
              {
                label: labels?.resolveButtonLabel
                onClick: () => resolve(true),
              },
              {
                label: labels?.rejectButtonLabel
                onClick: () => resolve(false),
              },
        ],
    });
  });
}

Comment on lines 370 to 386
for (const it of itemsWithLotAndExpirationDate) {
const { data } = await ProductApi.getInventoryItem(it.product?.id, it.lotNumber);
if (data.inventoryItem && data.inventoryItem.expirationDate !== it.expirationDate) {
isSomeItemsDontMatchExpirationDate = true;
break;
}
}

// After find at least a single instance where expiration date we are trying to save
// does not match the existing inventoryItem expiration date, we want to inform the user
// that certain updates to th expiration date in the system will be performed
if (isSomeItemsDontMatchExpirationDate) {
const isConfirmed = await this.confirmExpirationDateSave();
if (!isConfirmed) {
return Promise.reject();
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree with Alan's comment here

async save(formValues) {
this.saveStockTransferInCurrentStep(formValues)
.then(() => {
Alert.success(
Copy link
Collaborator

Choose a reason for hiding this comment

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

let's use our notification factory.

label: this.props.translate('react.default.no.label', 'No'),
},
],
async save(formValues) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

if it's made async how about using await instead of .then(() => ...?

try {
await this.saveStockTransferInCurrentStep(formValues);
} catch (error) {
return;
Copy link
Collaborator

Choose a reason for hiding this comment

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

why do we actually want to do nothing when an error happens? Shouldn't we let the error handler handle the error and display it to a user?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the error will be displayed to the user, because when using our custom apiClient we have a response interceptor that displays an alert when we get a server error.

I am returning here to prevent the function going further, because the last step is redirecting to stock movement show page and if we have an error we don;t want to do that.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@drodzewicz ok. I thought it would work like in Java (if we use the catch, we'd override the error handler and not let it handle the error).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The difference between our grails error handler and frontend axios error handler is the order when they are called.
In case of grails, the global error handler is called as the last method, so when we catch a method manually it does not propagate to the global error handler.
For our custom apiClient on frontend we set the error handler as a response interceptor which is always called first. It handles the error and then returns a rejected promise.

export const STOCK_TRANSFER_API = `${API}/stockTransfers`;
export const STOCK_TRANSFER_DELETE = id => `${STOCK_TRANSFER_API}/${id}`;
export const STOCK_TRANSFER_UPDATE = id => `${STOCK_TRANSFER_API}/${id}`;
export const STOCK_TRANSFER_GET = id => `${STOCK_TRANSFER_API}/${id}`;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I apologize not for catching it earlier, but I think we used to name such urls as ..._BY_ID, not to repeat the urls for update, delete, get, read, so I think it should be:

export const STOCK_TRANSFER_BY_ID = id => `${STOCK_TRANSFER_API}/${id}`;

Copy link
Collaborator

Choose a reason for hiding this comment

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

Someone only did it for stock movements, I think it could be just STOCK_TRANSFER, and then you just have:
delete(STOCK_TRANSFER), update(STOCK_TRANSFER) etc.

Copy link
Collaborator

Choose a reason for hiding this comment

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

It was done only for stock movements, because it was the moment in which we decided to avoid using DELETE, GET, POST, and PUT. I remember we created a ticket for the refactor of the rest of the namings (but I could be wrong).

@@ -30,6 +30,9 @@ export const STOCK_MOVEMENT_ITEM_REMOVE = id => `${STOCK_MOVEMENT_ITEM_API}/${id
// STOCK TRANSFER
export const STOCK_TRANSFER_API = `${API}/stockTransfers`;
export const STOCK_TRANSFER_DELETE = id => `${STOCK_TRANSFER_API}/${id}`;
Copy link
Collaborator

Choose a reason for hiding this comment

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

yeah, I think you've followed what was there, but I think this should also be _BY_ID. (Like for STOCK_MOVEMENT)

Copy link
Collaborator

Choose a reason for hiding this comment

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

or perhaps just STOCK_TRANSFER without _BY_ID :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

basically, it would be better to name it as it was done for stock movements

export const STOCK_TRANSFER_API = `${API}/stockTransfers`;
export const STOCK_TRANSFER_BY_ID = id => `${STOCK_TRANSFER_API}/${id}`;

it couldn't be just STOCK_TRANSFER, because we have to separate the URLs for sending requests and redirects.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, that's why it was done like that, forgot about redirects :/

Copy link
Collaborator

Choose a reason for hiding this comment

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

but wait, should not be used for this applicationUrls?

@drodzewicz drodzewicz force-pushed the OBPIH-5932-add-validation-for-expiration-date-in-inbound-returns branch from 4b4a9cd to 8df0177 Compare January 22, 2024 13:35
- added StockTransferStatus consts
- added StockTrasnferApi and replaced regular apiClient on AddItemsPage
- fixed some order of method calls
@drodzewicz drodzewicz force-pushed the OBPIH-5932-add-validation-for-expiration-date-in-inbound-returns branch from 8df0177 to 87d5941 Compare January 22, 2024 13:35
@awalkowiak awalkowiak merged commit ad11b47 into feature/upgrade-to-grails-3.3.10 Jan 22, 2024
@awalkowiak awalkowiak deleted the OBPIH-5932-add-validation-for-expiration-date-in-inbound-returns branch January 22, 2024 14:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants