Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OBPIH-5806 approval and reject action approvals end to end workflow #4293

Conversation

drodzewicz
Copy link
Collaborator

No description provided.

Base automatically changed from OBPIH-5803 to develop September 22, 2023 13:30
@drodzewicz drodzewicz force-pushed the OBPIH-5806-approval-and-reject-action-approvals-end-to-end-workflow branch 2 times, most recently from e6c9763 to 450cac5 Compare September 22, 2023 15:21
@drodzewicz drodzewicz self-assigned this Sep 25, 2023
@drodzewicz drodzewicz marked this pull request as ready for review September 25, 2023 07:05
Copy link
Collaborator

@kchelstowski kchelstowski left a comment

Choose a reason for hiding this comment

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

overall good job, those are only "beautify" suggestions

def updateStatus = {
StockMovement stockMovement = stockMovementService.getStockMovement(params.id)

try {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I really don't like throwing that try/catch everywhere - lately we've been doing that very much. Just wanted to mention that a potential rollback might not take place in Grails 1 when we catch exceptions.
Is it in requirements that after an exception we want to show the same view page?

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 was not mentioned if it should be the same page so it was my own interpretation.

It was mentioned that reject and approve buttons should be disabled if a request is already approved/rejected.
This try catch was more like a failsafe I added so I think I can remove it and let the generic handler do it's job and render generic error page.

return
}

redirect(action: "list", params: ["flash": flash as JSON, sourceType: "ELECTRONIC", direction: "OUTBOUND"])
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
redirect(action: "list", params: ["flash": flash as JSON, sourceType: "ELECTRONIC", direction: "OUTBOUND"])
redirect(action: "list", params: ["flash": flash as JSON, sourceType: RequisitionSourceType.ELECTRONIC, direction: StockMovementDirection.OUTBOUND.toUpperCase()])

@@ -236,6 +236,13 @@ class UserService {
return isSuperuser(currentUser) || (currentUser.getHighestRole(location) >= otherUser.getHighestRole(location))
}

Boolean userHasRoles(String userId, Collection roleTypes, String locationId) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I see an inconsistency here - you return the method you've created in the domain, but the return types are different - here it is Boolean and in the domain it is boolean - I think it should be boolean here as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes, it should be boolean

Event event = createEvent(EventCode.REJECTED, requisition.origin, new Date(), currentUser)
requisition.addToEvents(event)
requisition.save()
return
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 this could be refactored. We have almost the same content in all of the statements, hence we could create a helper method to handle that, something like:

Requisition transitionRequisitionStatus(Requisition requisition, RequisitionStatus requisitionStatus, EventCode eventCode, User currentUser) {
   requisition.dateApproved = new Date()
   requisition.approvedBy = currentUser
   requisition.status = requisitionStatus

   Event event = createEvent(eventCode, requisition.origin, new Date(), currentUser)
   requisition.addToEvents(event)
   requisition.save()
}

and here:

if (status == RequisitionStatus.PENDING_APPROVAL) {
    if (requisition.origin.approvalRequired) {
        Event event = createEvent(EventCode.PENDING_APPROVAL, requisition.origin, new Date(), currentUser)
        requisition.addToEvents(event)
        requisition.status = RequisitionStatus.PENDING_APPROVAL
        requisition.approvalRequired = true
        requisition.save()
        return
    }
    Event event = createEvent(EventCode.SUBMITTED, requisition.origin, new Date(), currentUser)
    requisition.addToEvents(event)
    requisition.status = RequisitionStatus.VERIFYING
    requisition.approvalRequired = false
    requisition.save()
    return
} 
if (status == RequisitionStatus.APPROVED) {
   transitionRequisitionStatus(requisition, RequisitionStatus.APPROVED, EventCode.APPROVED, currentUser)
   return
}
if (status == RequisitionStatus.REJECTED) {
   transitionRequisitionStatus(requisition, RequisitionStatus.REJECTED, EventCode.REJECTED, currentUser)
   return
}

we could go even further and you could refactor also the statements I've made with one minor change:

if (status == RequisitionStatus.PENDING_APPROVAL) {
    if (requisition.origin.approvalRequired) {
       Requisition updatedRequisition = transitionRequisitionStatus(requisition, RequisitionStatus.PENDING_APPROVAL, EventCode.PENDING_APPROVAL, currentUser)
       updatedRequisition.approvalRequired = true
       // requisition.save() <--- notice I've commented that, it shouldn't be necessary anymore, because the event which has not been yet persisted has been already saved in the transition method, hence another save should not be necessary and the approvalRequired flag should be persisted at the end of the session, but please, double check that.
       return
    }
    Requisition updatedRequisition = transitionRequisitionStatus(requisition, RequisitionStatus.VERIFYING, EventCode.SUBMITTED, currentUser)
    requisition.approvalRequired = false
    return
} 
if (status == RequisitionStatus.APPROVED) {
   transitionRequisitionStatus(requisition, RequisitionStatus.APPROVED, EventCode.APPROVED, currentUser)
   return
}
if (status == RequisitionStatus.REJECTED) {
   transitionRequisitionStatus(requisition, RequisitionStatus.REJECTED, EventCode.REJECTED, currentUser)
   return
}

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 would be great to move some common logic into some other method but the reason why I didn't do it at first was that there a slight differences in each of the status changes like in example of a APPROVED we assign approvedBy but in case of the REJECETD we must assign rejectedBy.
These are small differences so a common method should work but I'll think about if there is a better implementation for all of this

Copy link
Collaborator

Choose a reason for hiding this comment

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

then you can just follow the path I've shown with my statements with the approvalRequired - so just from this method return the common stuff, and then just operate on it like:

Requisition updatedRequisition = transitionRequisitionStatus(...)
requisition.rejectedBy = currentUser

Copy link
Member

Choose a reason for hiding this comment

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

I would also be happy with a switch statement for the top level status check

switch (status) { 
    case PENDING_APPROVAL: 
        doPendingApprovalStuff()
        return
    case APPROVED: 
        transitionRequisitionStatus(requisition, RequisitionStatus.APPROVED, EventCode.APPROVED, currentUser)
        return
    ...  
}

Copy link
Member

Choose a reason for hiding this comment

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

requisition.save() <--- notice I've commented that, it shouldn't be necessary anymore, because the event which has not been yet persisted has been already saved in the transition method, hence another save should not be necessary and the approvalRequired flag should be persisted at the end of the session, but please, double check that.

Not sure if that'll be true on Grails 3.

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 like the switch case a lot more

Copy link
Collaborator

Choose a reason for hiding this comment

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

@jmiranda

Not sure if that'll be true on Grails 3.

yes, it will, because it's Hibernate's mechanism, not Grails'

grails-app/views/stockMovement/show.gsp Outdated Show resolved Hide resolved
<img src="${resource(dir: 'images/icons/silk', file: 'arrow_rotate_anticlockwise.png')}" />&nbsp;
<warehouse:message code="stockMovement.rollbackLastReceipt.label" />
<g:set var="userHasRequestApproverRole" value="${false}"/>
<g:userHasRoles location="${stockMovement?.origin?.id}" roles="${[RoleType.ROLE_REQUISITION_APPROVER]}">
Copy link
Collaborator

Choose a reason for hiding this comment

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

I know @jmiranda is not a big fan of that, but I don't know if I see any other possible solution at the moment
But I like the fact, that you've actually checked it once, and assigned it to a variable, so we wouldn't have to call this <g:userHasRoles> in many places, like we do for <g:isSuperuser> in many places

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I hate this paradigm, but I think I'm the one who made it popular so I don't blame @drodzewicz.

What is interesting is that the <g:set /> actually gets invoked with that. I guess that makes sense, but I assumed g:if did something special. I guess it's just that GSP is rendered/invoked recursively so whether the next tag is rendering or setting doesn't really matter ... it just gets invoked. But it would be great if someone investigated the Grails tab library where "g:if" is defined to see what they do in this case.

And as I said above, I would prefer ifUserHasRoles over userHasRoles.

Copy link
Member

Choose a reason for hiding this comment

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

It probably needs to be done as a tech debt refactoring but we probably should have thought about converging our permission/supports taglibs with something closer to Spring Security (like using authorize with logic expressions).

<img src="${resource(dir: 'images/icons/', file: 'handtruck.png')}" />&nbsp;
<warehouse:message code="default.button.receive.label" />
</g:link>
<g:isUserAdmin>
Copy link
Collaborator

Choose a reason for hiding this comment

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

I built you up in the comment above for assigning it to a variable, but here I can see that you call the <g:isUserAdmin> a few times - maybe it'd be good from the performance POV to do it also in this place?

btw... I don't know if it'd be a good direction in the future, but why can't we just return user roles in the model while rendering the view?... it's a "loud thinking", but I'll leave this comment here in case anyone wanted to comment on that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this is old code whcih github highlighted as a new change since I wrapped a <g:if> and <g:userHasRoles> around it

Copy link
Member

Choose a reason for hiding this comment

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

Can someone add to the tech debt backlog an investigation into the right way to do these types of tag libs? It should be under Security and Performance. We invoke things like "isInRole" all the time throughout the code and I know it degrades performance. But what I don't understand is how Spring Security gets away with it. Do all of the security invocations go through the thread local?

Comment on lines 254 to 271
switch(stockMovement.requisition?.status) {
case RequisitionStatus.APPROVED:
flash.message = g.message(
code: "request.approved.message.label",
default: "You have successfully approved the request {0}",
args: [stockMovement.id]
)
break
case RequisitionStatus.REJECTED:
flash.message = g.message(
code: "request.rejected.message.label",
default: "You have successfully rejected the request {0}",
args: [stockMovement.id]
)
break
default:
break
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe we can shorten it to:

if (stockMovement.requisition?.status in [RequisitionStatus.APPROVED, RequisitionStatus.REJECTED]) {
   String status = stockMovement.requisition?.status?.toLowerCase()   
   flash.message = g.message(
                            code: "request.${status}.message.label",
                            default: "You have successfully ${status} the request {0}",
                            args: [stockMovement.id]
                    )
}

but if you don't like this solution, let's leave it as is

@drodzewicz drodzewicz force-pushed the OBPIH-5806-approval-and-reject-action-approvals-end-to-end-workflow branch from 57a57db to b0dd4e1 Compare September 25, 2023 13:09
@drodzewicz drodzewicz force-pushed the OBPIH-5806-approval-and-reject-action-approvals-end-to-end-workflow branch from b0dd4e1 to 177d3ab Compare September 26, 2023 10:16
requisition.dateApproved = new Date()
requisition.approvedBy = currentUser
return
} else if (requisition.status == RequisitionStatus.REJECTED) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nitpicky: else is not required if you call return above (take a look at my suggestion below)

Copy link
Member

Choose a reason for hiding this comment

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

I'd rather remove the return than the else.

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.

LGTM. I can't remember if I requested any changes, so I'll leave this as a comment for now.

def updateStatus = {
StockMovement stockMovement = stockMovementService.getStockMovement(params.id)

stockMovementService.transitionStockMovement(stockMovement, params as JSONObject)
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, this sort of highlights a bad decision I made when we first created the stock movement API. I'm a dum dum. JSONObject is a web layer class so it's not meant to be passed to the service layer. I'm guessing we've noted this as a tech debt somewhere in the past, but if not we should think about it adding it to our tech debt backlog now.

stockMovementService.transitionStockMovement(stockMovement, params as JSONObject)

if (stockMovement.requisition?.status in [RequisitionStatus.APPROVED, RequisitionStatus.REJECTED]) {
String status = stockMovement.requisition?.status
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 sure what you're trying to do here with the dynamic coercion, but I would assume you'd need to use toLowerCase() here to get the proper message from messages.properties. It's possible that the message is resolved properly only because you set a default message.

// check if status is either approved or rejecetd
// if no then throw error and return a message that it can't be rolledback

// TODO: to be implemented
Copy link
Member

Choose a reason for hiding this comment

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

It would be good to throw an UnsupportedOperationException (or NotImplementedException) here until it's implemented.

}

redirect(action: "list", params: [
"flash" : flash 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.

Can you explain why this is necessary? I assume you're not seeing the flash.message on the list page due to the redirect, but this doesn't seem like the right approach to that problem.

Once you explain what you're encountering I'll try to come up with a more elegant solution.

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 redirect is directing us to react page for request list page.
There on the frontend we have a handler that extracts flash object from the parameters and renders a message or error depending on what parameters are set on the flash.

I was following an example from the method above for def remove()

Copy link
Collaborator

Choose a reason for hiding this comment

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

@jmiranda This is our workaround to get messages from "grails" pages to "react" pages (when it is done through a redirect). This won't be needed if, in some bright future, we separate frontend from backend (read: we finally get rid of gsp templates and server-side rendering).

@@ -236,6 +236,13 @@ class UserService {
return isSuperuser(currentUser) || (currentUser.getHighestRole(location) >= otherUser.getHighestRole(location))
}

boolean userHasRoles(String userId, Collection roleTypes, String locationId) {
Copy link
Member

Choose a reason for hiding this comment

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

Use hasRoles as the name here too. Although, I'd also be fine with

  • doesUserHaveRoles
  • isUserInRoles
  • isUserInAllRoles
  • isUserInAnyRole

<img src="${resource(dir: 'images/icons/silk', file: 'arrow_rotate_anticlockwise.png')}" />&nbsp;
<warehouse:message code="stockMovement.rollbackLastReceipt.label" />
<g:set var="userHasRequestApproverRole" value="${false}"/>
<g:userHasRoles location="${stockMovement?.origin?.id}" roles="${[RoleType.ROLE_REQUISITION_APPROVER]}">
Copy link
Member

Choose a reason for hiding this comment

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

It probably needs to be done as a tech debt refactoring but we probably should have thought about converging our permission/supports taglibs with something closer to Spring Security (like using authorize with logic expressions).

<img src="${resource(dir: 'images/icons/', file: 'handtruck.png')}" />&nbsp;
<warehouse:message code="default.button.receive.label" />
</g:link>
<g:isUserAdmin>
Copy link
Member

Choose a reason for hiding this comment

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

Can someone add to the tech debt backlog an investigation into the right way to do these types of tag libs? It should be under Security and Performance. We invoke things like "isInRole" all the time throughout the code and I know it degrades performance. But what I don't understand is how Spring Security gets away with it. Do all of the security invocations go through the thread local?

</g:if>
<g:elseif test="${stockMovement?.hasBeenIssued() || ((stockMovement?.hasBeenShipped() ||
stockMovement?.hasBeenPartiallyReceived()) && stockMovement?.isFromOrder)}">
<g:link controller="stockMovement" action="rollback" id="${stockMovement.id}" class="button">
Copy link
Member

Choose a reason for hiding this comment

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

@drodzewicz Are you saying that all of the green code is old? If not we definitely need to add another tech debt item for cleaning up our logic because this is getting out of hand.

At the very least we need to push more of this logic into the stock movement.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

by pushing more of this logic to stock Movement do you mean pushing authorization logic there, like eg. stockMovement.approverAuhtorized(user) stockMovement.canEddit(user)?

>
<img src="${resource(dir: 'images/icons/silk', file: 'delete.png')}" />&nbsp;
<warehouse:message code="default.button.delete.label" />
<img src="${resource(dir: 'images/icons/silk', file: 'accept.png')}" />&nbsp;
Copy link
Member

Choose a reason for hiding this comment

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

Is this the only place we use this icon on this page? If so accept.png (approved) and decline.png (rejected) should work perfectly. Otherwise, we might want to consider using a different pair (like tick.png, cross.png).

<img src="${resource(dir: 'images/icons/silk', file: 'delete.png')}" />&nbsp;
<warehouse:message code="default.button.delete.label" />
<img src="${resource(dir: 'images/icons/silk', file: 'accept.png')}" />&nbsp;
<g:message code="request.approval.approve.label" default="approve" />
Copy link
Member

Choose a reason for hiding this comment

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

Why lowecase?

@drodzewicz drodzewicz force-pushed the OBPIH-5806-approval-and-reject-action-approvals-end-to-end-workflow branch from 177d3ab to bd7b909 Compare September 27, 2023 10:56
- refactor method names
- refactor triggerRequisitionStatusTransition to use switch case instead of if
@drodzewicz drodzewicz force-pushed the OBPIH-5806-approval-and-reject-action-approvals-end-to-end-workflow branch from bd7b909 to cd42095 Compare September 27, 2023 11:01
Copy link
Collaborator

@awalkowiak awalkowiak left a comment

Choose a reason for hiding this comment

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

@drodzewicz two small requests from my side, other than that I think it LGTM

requisition.status = RequisitionStatus.VERIFYING
requisition.approvalRequired = false
requisition.save()
switch(requisition.status) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

@drodzewicz Perhaps a comment here would be handy? Please add a note here about why we have various options for status transition here, with a link to request approvals epic ticket.

@@ -88,6 +88,12 @@ class AuthTagLib {
out << body()
}

def ifUserHasRoles = {attrs, body ->
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please for now follow here the convention of isUserInRole and make this one isUserInAllRoles instead ifUserHasRoles

@awalkowiak awalkowiak merged commit 565da4f into develop Sep 27, 2023
3 checks passed
@awalkowiak awalkowiak deleted the OBPIH-5806-approval-and-reject-action-approvals-end-to-end-workflow branch September 27, 2023 13:01
awalkowiak pushed a commit that referenced this pull request Oct 26, 2023
…4293)

* OBPIH-5803 Change fetchMode of Requisition events to avoid LazyInitializationException in a published event

* OBPIH-5803 Include status transition in RequisitionStatusTransitionEvent

* OBPIH-5803 Add createdBy field for Event domain

* OBPIH-5803 Fix assigning createdBy to an Event

* OBPIH-5803 Fixes after review

* OBPIH-5806 Render approve and reject buttons on request show page

* OBPIH-5806 Implement approve and reject status changes logic

* OBPIH-5806 Fix messages after updaing request status

* OBPIH-5806 Render approve and reject buttons only on request with origin which supports approvals and for users that have roletype request approver

* OBPIH-5806 Fix after rebase

* OBPIH-5806 Render only reject, approve and rollback buttons to approver

* OBPIH-5806 Rafactor requets state transition function
remove catching excetion on updateStatus and let generic error handler catch it

* OBPIH-5806 use requisition.status instead of status

* OBPIH-5806 Fixes after review
- refactor method names
- refactor triggerRequisitionStatusTransition to use switch case instead of if

* OBPIH-5806 Rename AuthTagLib method and add comment on status transition

---------

Co-authored-by: kchelstowski <kchelstowski@soldevelo.com>
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.

None yet

5 participants