Skip to content

OBPIH-7597 Allow mass Recipient Edit for line items in multiple POs#5727

Merged
alannadolny merged 7 commits into
developfrom
feature/OBPIH-7597
Jan 28, 2026
Merged

OBPIH-7597 Allow mass Recipient Edit for line items in multiple POs#5727
alannadolny merged 7 commits into
developfrom
feature/OBPIH-7597

Conversation

@SebastianLib
Copy link
Copy Markdown
Collaborator

✨ Description of Change

Link to GitHub issue or Jira ticket:
https://pihemr.atlassian.net/browse/OBPIH-7597

Description:


📷 Screenshots & Recordings (optional)

@github-actions github-actions Bot added type: feature A new piece of functionality for the app domain: frontend Changes or discussions relating to the frontend UI domain: backend Changes or discussions relating to the backend server domain: l10n Changes or discussions relating to localization & Internationalization labels Jan 21, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Jan 21, 2026

Codecov Report

❌ Patch coverage is 35.71429% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 9.85%. Comparing base (1bb7314) to head (e2828cf).
⚠️ Report is 337 commits behind head on develop.

Files with missing lines Patch % Lines
...rchaseOrderActualReadyDateImportDataService.groovy 0.00% 12 Missing ⚠️
...rvices/org/pih/warehouse/data/PersonService.groovy 71.42% 2 Missing and 2 partials ⚠️
...rvices/org/pih/warehouse/order/OrderService.groovy 0.00% 1 Missing ⚠️
.../warehouse/shipping/CombinedShipmentService.groovy 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             develop   #5727      +/-   ##
============================================
+ Coverage       9.12%   9.85%   +0.73%     
- Complexity      1170    1377     +207     
============================================
  Files            701     732      +31     
  Lines          45281   46135     +854     
  Branches       10851   11011     +160     
============================================
+ Hits            4131    4546     +415     
- Misses         40497   40884     +387     
- Partials         653     705      +52     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@alannadolny alannadolny requested a review from jmiranda January 21, 2026 14:48
Comment on lines +165 to +166
InternetAddress internetAddress = new InternetAddress(recipient, false)
internetAddress.validate()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't understand that part

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Here we use InternetAddress only to detect whether the value entered in the recipient column is an email address (e.g. alannadolny@gmail.com or Alan Nadolny <alannadolny@gmail.com>). Based on that, we decide whether to check the person by email (getActivePersonByEmail method) or by full name (getActivePersonByName method). You can read more about this here: https://docs.oracle.com/javaee/5/api/javax/mail/internet/InternetAddress.html

I was inspired by @ewaterman code:

    Person getOrCreatePersonByRecipient(String recipient) {
        if (StringUtils.isBlank(recipient)) {
            return null
        }

        InternetAddress internetAddress
        try {
            internetAddress = new InternetAddress(recipient, false)
        }
        catch (AddressException ignored) {
            // If recipient isn't a valid internet address, it must be a regular name.
            return getOrCreatePersonFromNames(recipient)
        }

        // If a person exists with the given email, return them. Note that this can return inactive people so
        // the caller must properly handle that case. We do this to avoid creating duplicate users.
        Person person = getPersonByEmail(internetAddress.address)
        if (person) {
            return person
        }

        // Otherwise create a new person.
        if (!internetAddress.personal) {
            throw new RuntimeException("Cannot save new recipient without a name: ${recipient}")
        }
        String[] names = extractNames(internetAddress.personal)
        person = new Person(firstName: names[0], lastName: names[1], email: internetAddress.address)
        if (!person.save(flush: true)) {
            throw new ValidationException("Cannot save recipient ${recipient} due to errors", person.errors)
        }
        return person
    }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I forgot I wrote that 😅 I think the code in PersonService can be refactored so that we have cleaner methods:

  • getPersonByName(String) -> split into firstname, lastname and query
  • getPersonByEmail(String) -> simply query on Person.email
  • getPerson(String) -> calls both getPersonByEmail and getPersonByName to find the Person
  • getActivePerson(String) -> calls getPerson then returns them if active == true
  • getOrCreatePerson(String) -> calls getPerson and if null, creates a Person

but I won't make you do that.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The refactor looks simple enough, so I can try tomorrow

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

btw, the internet address looks weird to me, and I am confused every time I see that code 😄 Maybe we can try to do some kind of validator for that? (https://grails.apache.org/docs/7.0.4/ref/Constraints/email.html)

@ewaterman

Copy link
Copy Markdown
Member

@ewaterman ewaterman Jan 22, 2026

Choose a reason for hiding this comment

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

the problem is that when we import from a csv or xls file, we have just one field representing the Person (in this case "recipient"). And to be user friendly, we want to accept either name or email in that field. So we can't just wrap the field in a simple built-in validator. The String field that we get is either an email or a name and we need to figure out which it is here

Comment on lines +105 to +108
if (!StringUtils.isBlank(params["recipient"])) {
Person recipient = personService.getActivePerson(params["recipient"])
if (!recipient) {
command.errors.reject("Row ${index + 1}: Recipient ${params["recipient"]} does not exist in OpenBoxes. Please check your data")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

you can assign the params["recipient"] to a variable

Person getActivePerson(String recipient) {
Person person = getPerson(recipient)
return person?.active ? person : null
}
Copy link
Copy Markdown
Member

@ewaterman ewaterman Jan 23, 2026

Choose a reason for hiding this comment

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

looks good! I know you didn't introduce it, but "recipient" doesn't make much sense here IMO because this can be used by any flow, not just orders and shipments. Really it's just a string for identifying a person.

Can you please change the "recipient" variables here to "identifier" or "personIdentifier" or something similar?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yes, I think that "identifier" makes more sense. I will change that

Comment thread grails-app/services/org/pih/warehouse/data/PersonService.groovy
'Justin Miranda' || '1'
'Unknown Name <justin@openboxes.com>' || '1'
'Justin Inactive' || null
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice tests! I feel you could be more exhaustive with inputs here. Both these methods could test:

- 'Justin'
- 'Justin Miranda'
- 'Justin Justy Miranda'
- 'justin@openboxes.com'
- 'unknown@openboxes.com'
- 'Justin Miranda <justin@openboxes.com>'
- 'Justin Miranda <unknown@openboxes.com>'
- 'Unknown Name <justin@openboxes.com>'
- 'Justin Inactive'
- null

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I agree it’s a good idea to test more cases. I haven’t written many tests in my career, so thanks for the guidance on what exactly to test 🚀

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

👏

'justin@openboxes.com' || '1'
'unknown@openboxes.com' || null
'Justin Miranda <justin@openboxes.com>' || '1'
'Justin Miranda <unknown@openboxes.com>' || null
Copy link
Copy Markdown
Collaborator Author

@SebastianLib SebastianLib Jan 23, 2026

Choose a reason for hiding this comment

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

I think that this test case is curious. In this test case: Justin Miranda <unknown@openboxes.com> the result is null. This happens because the method detects an email format and only searches by the email address. Even though the name 'Justin Miranda' is correct, the system doesn't find the person because the email is unknown.

On the other hand, if the email is correct but the name is wrong (e.g. Unknown Name <justin@openboxes.com>), the system finds the person.

Maybe somehow we should improve this? If the email search fails, then we should also check by name or maybe we should stay as it is and always prioritize email 🤔

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

hmm it's an interesting point. I don't know if there's a clear answer.

Another argument is that if you provide both, both should match. Person isn't actually unique on email or on name so just because the name or email match, it doesn't mean you're getting the user that you want. Providing both could mean you want to be very specific. But idk, that could be overly restrictive.

I think it's more important that the email match because email far more likely to be unique. Two people can have the same name, but I'd be surprised if we have two people with the same email (unless the same person is in the system multiple times).

So given that, I guess the current logic makes sense to prioritize email, but I suppose we could have it try name afterwards if the email doesn't match.

That said, I don't actually know if anyone is providing strings like Justin Miranda <unknown@openboxes.com>. I imagine 99% of the time it's either just name or just email. So maybe it's not a problem that we need to deal with

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@kchelstowski @alannadolny what do you think about this?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I believe it's not a problem because it's a hardcore edge case. But maybe as a simple solution, we can show the user an error like: "We found a person by the name that you provided, but the email doesn't match. Please check your data."

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

hmmmm I think it's better to leave it as it is 🤔

@alannadolny alannadolny merged commit 21bd57f into develop Jan 28, 2026
7 checks passed
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 type: feature A new piece of functionality for the app

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants