Skip to content

OBPIH-5182 IdentifierService overhaul#4885

Merged
ewaterman merged 6 commits intodevelopfrom
maintenance/OBPIH-5182-custom-id-refactor
Oct 23, 2024
Merged

OBPIH-5182 IdentifierService overhaul#4885
ewaterman merged 6 commits intodevelopfrom
maintenance/OBPIH-5182-custom-id-refactor

Conversation

@ewaterman
Copy link
Member

@ewaterman ewaterman commented Oct 9, 2024

✨ Description of Change

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

Description:

Wowie this is a big change. Sorry. I don't expect you to review this in its entirety before we get a chance to discuss it in a tech huddle. Tech huddle chat has happened

The main focus of this ticket is to do the following:

  • Add a retry mechanism when generating ids in case the one we generate already exists
  • Check for uniqueness across tables when generating ids that are shared across tables. Specifically:
    • We use shipment number as location number when creating internal locations from shipments
    • We use order number as shipment number when creating a shipment from an order
    • We use request number as shipment number when performing stock movements

It was a pain to achieve the above since the code was quite segmented across classes, but also IdentifierService was bloated with feature specific logic that each had their own way of generating ids, so I opted to refactor the flow.

The logic is largely the same, but now instead of doing IdentifierService.generateInvoiceIdentifer() we do InvoiceIdentifierService.generate(). The feature specific services all extend from the IdentifierService. This gives us the following:

  • All id generation methods share the same feature agnostic base flow, meaning they all get the benefits of all the id generation tools (custom delimiters, format overrides, random sections...)
  • It's easy to hook in additional functionality to the feature specific services if needed
  • It makes adding new feature services really easy (just a few methods to override)
  • It keeps feature specific logic isolated from the other features

Still TODO:

  • The logic in ProductIdentifierService, OrganizationIdentifierService, and PurchaseOrderIdentifierService need to be generalized and moved into IdentifierService so that we can support sequential ids more generally.
    • I won't be doing this in this task because it's big enough already so those classes are just using the old code as it was.
  • Writing unit tests on all refactored the id generation flow
  • Writing integration / smoke tests to verify that the current config can generate ids without error

@ewaterman ewaterman added the status: work in progress For issues or pull requests that are in progress but not yet completed label Oct 9, 2024
@ewaterman ewaterman self-assigned this Oct 9, 2024
@github-actions github-actions bot added type: maintenance Code improvements, optimizations and refactors, dependency upgrades... domain: backend Changes or discussions relating to the backend server flag: config change Hilights a pull request that contains a change to the app config labels Oct 9, 2024
* not called by the AssignIdentifierJob.
*/
@Slf4j
trait BlankIdentifierResolver<T extends GormEntity> {
Copy link
Member Author

@ewaterman ewaterman Oct 9, 2024

Choose a reason for hiding this comment

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

This class allows us to generalize supporting the AssignIdentifierJob that batch generates ids for rows where the id is null. I made it a separate trait from IdentifierService because not every implementation needs to support it.

}

@Override
protected Integer countDuplicates(String requestNumber) {
Copy link
Member Author

Choose a reason for hiding this comment

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

this is one of the examples of how we handle checking multiple tables for uniqueness when generating an id.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks. I was going to ask about how we handle complex cases like multiple tables.

  1. Could we change the argument to stay consistent with the abstract class method. Or do you feel this is an important detail for someone trying to figure out what's going on?
     countDuplicates(String identifier)
    
  2. We should add a comment here to explain what's happening and include a FIXME to remind us that we need to fix this logic once we unmingle the identifiers.

We essentially want to support the following

  • a requisition can be associated with multiple shipments
  • shipments should always get their own shipment number assigned
  • shipment number != requisition number

Note

Context: As far as I recall, this is primarily the fault of the Stock Movement resource design. We were trying to keep things lean and backwards-compatible for inbound (which only used shipments) while building the new outbound SM workflow. We probably should have designed everything as separate resources from the start.

The work of separating the identifiers is also somewhat related to the Order Everywhere refactoring that I've discussed in the past where an Order should be the central object for all of our inbound, outbound and internal stock movement workflows. Requisitions can still be created for both inbound (purchase requisition) and outbound (stock transfer request) but the order object should define what is actually being done. Where this comes into play is that we should be able to get replace the Stock Movement resource once there's a suitable alternative resource for each of the workflows.

Copy link
Member Author

Choose a reason for hiding this comment

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

for 1 and 2, yes can do. Thanks for the context on the why. Maybe we can add a dev task in the next release to refactor this properly, or at least a discovery to determine the work required. The nice thing about the ids being the same is that it's simpler to cross-reference them, but yes if we're refactoring to not need them, we could remove the check here on other tables.

Another option would be to not check for duplicates in other tables at all, and instead of simply assigning shipmentNumber=requestNumber, we do something like:

shipmentNumber = shipmentIdentifierService.generate(IdentifierGeneratorParams.builder()
                .formatOverride("\${custom.requestNumber}-\${random}")
                .customKeys(['requestNumber': requisition.requestNumber])
                .build())

So given requestNumber ABC123, shipmentNumber would be ABC123-123456.

// TODO: We should be able to move most of this logic into the identifier service. That way we can support sequential
// id generation more generally. We'd need an abstract method there for fetching the next sequence number to use,
// and a new ${sequence} config option to know where to put the sequence.
class ProductIdentifierService extends IdentifierService implements BlankIdentifierResolver<Product> {
Copy link
Member Author

Choose a reason for hiding this comment

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

A majority of this code is just copied out from IdentifierService and some other places. I couldn't get around to refactoring this as a part of this change because it has such a complex configuration.

See the TODO here for how we could refactor in the future.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah this one is though, perhaps we could get more tests for it? This is not a change request for now, but might be an entry point to bigger refactor of that part

// TODO: We should be able to move this sequential logic into the identifier service. That way we can support sequential
// id generation more generally. We'd need an abstract method there for fetching the next sequence number to use,
// and a new ${sequence} config option to know where to put the sequence.
class PurchaseOrderIdentifierService extends IdentifierService {
Copy link
Member Author

@ewaterman ewaterman Oct 9, 2024

Choose a reason for hiding this comment

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

A majority of this code is just copied out from IdentifierService and OrderService. I couldn't get around to refactoring this as a part of this change.

See the TODO here for how we could refactor in the future.

Copy link
Member

Choose a reason for hiding this comment

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

I haven't read the TODOs but we can discuss if/when we get another chance to refactor. Or discuss now and add an idea garden ticket to briefly discuss the options.

// format that would get injected to based on the same logic that we have here. Then we'd add a new
// identifier.x.abbreviation.field = "path.to.field" config that we can use to extract the field from the entity.
// Or if that's too challenging, we can simply return the string to use via abstract method in IdentifierService.
class OrganizationIdentifierService {
Copy link
Member Author

Choose a reason for hiding this comment

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

A majority of this code is just copied out from IdentifierService and some other places. I couldn't get around to refactoring this as a part of this change because it has such a complex configuration.

All I really did here was add comments to clarify the code.

See the TODO here for how we could refactor in the future.

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 a really good idea to take some time now to think about how all of the different generate() methods work, document their requirements / behaviors, and come up with a design / unifying interface that will allow us to refactor these one-by-one in the future.

  • PO identifier requires a sequential identifier that is being stored on the organization
  • Product identifiers use a random number identifier unless there's a different format provided by the product type (i don't think we don't currently support sequential number identifiers here, but we should)
  • Shipment identifiers are assigned (should be deprecated in favor of separate)
  • Requisition identifiers are random number identifiers
  • yada yada yada

Copy link
Member Author

@ewaterman ewaterman Oct 21, 2024

Choose a reason for hiding this comment

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

The TODO describes my goal here. I want to have a single generate() method that is driven entirely by properties. I'm imagining this being configured like:

openboxes.identifier.organization.format = "\${abbreviation}"
openboxes.identifier.organization.abbreviation.field = "name"
openboxes.identifier.organization.minSize = 2
openboxes.identifier.organization.maxSize = 3

then run with:

Organization org = new Organization(name: "A Good Name")
String id = organizationIdentifierService.generate(org)
id == "AGN"

and you can make it as complex as you want. For example:

openboxes.identifier.organization.format = "\${abbreviation}\${random}"
openboxes.identifier.organization.abbreviation.field = "name"
openboxes.identifier.organization.minSize = 2
openboxes.identifier.organization.maxSize = 3
openboxes.identifier.organization.random.template = "-NNNN"
openboxes.identifier.organization.random.condition = RandomCondition.ON_DUPLICATE

which will output "AGN-1234" if "AGN" already exists.

/**
* Handles fetching property values from the app configuration.
*/
class ConfigService {
Copy link
Member Author

Choose a reason for hiding this comment

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

This was an attempt to unify our approach to fetching config.

It's based on best practices defined here: https://grails.org/blog/2016-08-31.html

It also makes mocking config in tests a lot easier.

Copy link
Member

@jmiranda jmiranda Oct 21, 2024

Choose a reason for hiding this comment

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

There's a ticket or two from OBGM to discover best practices on how to properly retrieve config properties in the application (and to avoid the legacy Holder pattern).

I was planning to migrate to using the Value annotation in as many places as possible. But having multiple ways to retrieve the configuration properties (particularly if we cannot inject them via Value in some places) makes sense. If we later determine that the Value annotation gets us all the way there, then we can remove this service.

identifierService.assignRequisitionIdentifiers()
identifierService.assignTransactionIdentifiers()
}
// Product.withNewSession {
Copy link
Member Author

Choose a reason for hiding this comment

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

Note to self: test if we still need this!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Did you check that one?

Copy link
Member Author

Choose a reason for hiding this comment

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

No unfortunately... I think I'm going to leave it as is for now, but I do think it's worth investigating in the future if this is actually a good idea. I'm thinking it'd be better to split them all into their own sessions/transactions.

openboxes.identifier.alphanumeric = "0123456789ABCDEFGHJKMNPQRSTUVWXYZ"
openboxes.identifier.sequenceNumber.format = "00000"

// Default identifier
Copy link
Member Author

Choose a reason for hiding this comment

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

Note that I added "default" format and random fallbacks. This simplifies configuration for features that don't need a custom format

Copy link
Member

Choose a reason for hiding this comment

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

Should the properties above also fall under the "default" or "random" namespace since they are the defaults?

Thinking out loud ... I think the first three would make more sense under the "random" namespace since they are only used with the random generator.

openboxes.identifier.random.numeric = "0123456789"
openboxes.identifier.random.alphabetic = "ABCDEFGHJKMNPQRSTUVXYZ"
openboxes.identifier.random.alphanumeric = "0123456789ABCDEFGHJKMNPQRSTUVWXYZ"

The sequence number is separate but that could potentially be under

openboxes.identifier.sequence.format = "00000"

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree. I didn't change those ones because I wanted to preserve existing properties, but I do think they make sense as random ones specifically. If we're comfortable with breaking backwards compatibility, I can switch them over

Copy link
Member

Choose a reason for hiding this comment

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

Ooh, that's a good point re: maintaining backwards compatibility. I don't actually think we know if anyone has overriden these properties so maybe I was wrong. Maybe we need to keep them for now. We can discuss or I'll leave it up to you to decide.

Ideally, we would provide a mechanism to "deprecate" config properties to allow config property changes like this. We could keep a mapping of old config properties (to their superseding property) for backwards-compatibility.

One brute force way to deal with these changes would be to keep track of the deprecated config properties in a list somewhere and throw an exception during startup if we encounter any of them in the grailsApplication.config. The better approach would be to fallback to these deprecated values (if they exist) but that adds complexity for testing and lifecycle management (when do they die).

Copy link
Member Author

@ewaterman ewaterman Oct 21, 2024

Choose a reason for hiding this comment

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

In general, we could do something like fetch the new format, then if we find nothing fetch the old format. It's two lookups but only if you're on the old format.

In this case though I agree it's unlikely anyone has modified these properties so I'll just change them.

EDIT: Actually I think I'm going to leave these as is. I don't see much need to provide the ability to override them on a per feature basis (and so don't need a "default" in my opinion), and they're not strictly specific to random (though currently that's all they're used for).

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good.

Copy link
Member Author

@ewaterman ewaterman Oct 9, 2024

Choose a reason for hiding this comment

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

This is where the bulk of the change is. I generalize the generate flow here and split out any feature specific stuff to feature services. It's also where the retry mechanism is. It's hard to parse in diff view so I suggest reviewing the raw file.

@ewaterman ewaterman removed the status: work in progress For issues or pull requests that are in progress but not yet completed label Oct 11, 2024
@awalkowiak
Copy link
Collaborator

(review still in progress)

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.

There still might be some places that are not thoroughly reviewed by me, but it overall lgtm and I think this requires some good testing locally and/or on dev instance

identifierService.assignRequisitionIdentifiers()
identifierService.assignTransactionIdentifiers()
}
// Product.withNewSession {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Did you check that one?

// TODO: We should be able to move most of this logic into the identifier service. That way we can support sequential
// id generation more generally. We'd need an abstract method there for fetching the next sequence number to use,
// and a new ${sequence} config option to know where to put the sequence.
class ProductIdentifierService extends IdentifierService implements BlankIdentifierResolver<Product> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah this one is though, perhaps we could get more tests for it? This is not a change request for now, but might be an entry point to bigger refactor of that part

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.

Sigh. I'm still not done but I wanted to post my comments so you have a chance to review.

openboxes.identifier.alphanumeric = "0123456789ABCDEFGHJKMNPQRSTUVWXYZ"
openboxes.identifier.sequenceNumber.format = "00000"

// Default identifier
Copy link
Member

Choose a reason for hiding this comment

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

Should the properties above also fall under the "default" or "random" namespace since they are the defaults?

Thinking out loud ... I think the first three would make more sense under the "random" namespace since they are only used with the random generator.

openboxes.identifier.random.numeric = "0123456789"
openboxes.identifier.random.alphabetic = "ABCDEFGHJKMNPQRSTUVXYZ"
openboxes.identifier.random.alphanumeric = "0123456789ABCDEFGHJKMNPQRSTUVWXYZ"

The sequence number is separate but that could potentially be under

openboxes.identifier.sequence.format = "00000"

openboxes.identifier.purchaseOrder.sequenceNumber.format = Constants.DEFAULT_PO_SEQUENCE_NUMBER_FORMAT
openboxes.identifier.purchaseOrder.format = "PO-\${destinationPartyCode}-\${sequenceNumber}"
openboxes.identifier.purchaseOrder.sequenceNumber.format = "000000"
openboxes.identifier.purchaseOrder.format = "PO-\${destinationPartyCode}-\${custom.sequenceNumber}"
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 custom is an argument passed to the identifier generation method but need to understand this better.

Copy link
Member Author

@ewaterman ewaterman Oct 21, 2024

Choose a reason for hiding this comment

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

I think you figured it out, but just in case, custom is a map that is passed in as a param. In this case:

IdentifierGeneratorParams.builder()
        .templateEntity(order)
        .customKeys(["sequenceNumber": sequenceNumberStr])
        .build())

but for sequenceNumber, this is a halfway solution since I want to eventually integrate that logic with the core id generation flow. Eventually you'll be able to just define openboxes.identifier.purchaseOrder.format = "PO-\${destinationPartyCode}-\${sequenceNumber}" and it'll know to look up the openboxes.identifier.x.sequenceNumber.format property (so no need for the custom key)

openboxes.identifier.default.prefix.enabled = true

// Transaction identifier
openboxes.identifier.transaction.random.template = "AAA-AAA-AAA"
Copy link
Member

Choose a reason for hiding this comment

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

We can't get there right now, but it would be great if we could hide the implementation details and expose the identifier generation strategies as an "API" for configuring for each identifier type.

In other words, if a system admin wants to override the transaction identifier, they would first need to know that the transaction uses the random identifier strategy i.e. because that is the default strategy.

openboxes.identifier.default.format = "\${random}"  // By default, simply generate a random id

They would also need to know that the random identifier strategy uses the default template

openboxes.identifier.default.random.template = "NNNLLL"

But there's already a config property for the transaction object ...

openboxes.identifier.transaction.random.template = "AAA-AAA-AAA"

so we just need to override this property.

openboxes.identifier.transaction.random.template = "NNN-NNN-NNN"

Instead, we could just provide an expression language (exposing an API of identifier strategies) to allow the system admin to be explicit about what they want to do.

openboxes.identifier.transaction.format = "#random('AAA-AAA-AAA')"

or it's a sequence number

openboxes.identifier.purchaseOrder.format = "#sequence('00000')"

or even something like this

# annotate the properties that should be included in the hashed value
openboxes.identifier.myObject.format = "#hash()"  
# explicitly define the properties to use in the config
openboxes.identifier.myObject.format = "#hash(['property1', 'property2'])"

The one caveat for the sequence identifier strategy is that we'd need to tell it what column it's targetting, which is a fact known by the caller, but not by the identifier strategy itself.

purchaseOrder.orderNumber = identifierService.generateIdentifier(context)

So we would probably need to build a context to pass to the generateIdentifier method so that it's aware of the identifier config as well as the target of the identifier. But this feels a little gross since it would be nice if the strategy just knew where it look to compare. I guess that could be handled by an annotation (@Targetproperty) or we standardize on an "identifier" property.

IdentifierConfigContext context = new SequenceIdentifierConfigContext([namespace: "purchaseOrder", targetProperty: "orderNumber"])
purchaseOrder.orderNumber = identifierService.generateSequenceIdentifier(context)

But anyway, exposing an API through an expression language would allow us to use the same API throughout the application, not just in runtime config i.e. party type, product type.

image

We'd probably want to use an existing expression language (like Spring EL similar to how Spring Security handles authorization rules)
https://docs.spring.io/spring-security/reference/5.8/servlet/authorization/expression-based.html

or template library like Thymeleaf.

Copy link
Member Author

Choose a reason for hiding this comment

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

interesting. We could definitely look at something like that to make the config simpler, but I think regardless we should consider this an admin feature that needs public documentation. Do you need custom ids? Here's how to define that for your implementation.

*
* This implementation will almost certainly come from the IdentifierService.
*/
abstract String generate(IdentifierGeneratorParams params=null)
Copy link
Member

Choose a reason for hiding this comment

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

I think the generate method probably needs two arguments:

  • a context that can be filled with config properties and other metadata used by the ID generator
  • the object for which the identifier is being generated

The Context could be specific to ID generation or perhaps we have some generic context object that can be used to store values needed for the work done by the specific implementation. For example, this would be very similar to a context object that gets passed to a template rendering component. I haven't looked at IdentifierGeneratorParams yet (or had time to think this through), but it seems this class is tighter version of the Context, so I think this works for now.

*/
String generateForEntity(T entity) {
return generate(IdentifierGeneratorParams.builder()
.templateEntity(entity)
Copy link
Member

Choose a reason for hiding this comment

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

Ah, you pass the object only when you you need it. So I think that's fine for now. Perhaps we could find a way to pass both the "object" and "configuration" in a more magical / implicit way i.e. if you call generate with no args, the abstract class creates a IdentifierGeneratorParams and populates it with the config properties and anything else that might be available.

Copy link
Member Author

Choose a reason for hiding this comment

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

I actually originally had the generate method passing in the entity as a requirement but it got complicated because there were some cases where two different entities were sharing the same format for generating ids so it got wonky with all the typing.

I was struggling with the balance of "this should be driven 100% via properties" and "well we need to provide certain params in certain situations directly in the code". With the custom params it's kinda inevitable that we need to provide them via code, but for everything else it can be determined purely via properties.

I suppose I was trying too hard to make the identifiers not need to rely on a specific entity (by making it optional), but given that the property config is entity specific, maybe we should just lean in and say an identifier service implementation is for a specific entity, full stop. Any entity that uses an identifier must define their own service.

// format that would get injected to based on the same logic that we have here. Then we'd add a new
// identifier.x.abbreviation.field = "path.to.field" config that we can use to extract the field from the entity.
// Or if that's too challenging, we can simply return the string to use via abstract method in IdentifierService.
class OrganizationIdentifierService {
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 a really good idea to take some time now to think about how all of the different generate() methods work, document their requirements / behaviors, and come up with a design / unifying interface that will allow us to refactor these one-by-one in the future.

  • PO identifier requires a sequential identifier that is being stored on the organization
  • Product identifiers use a random number identifier unless there's a different format provided by the product type (i don't think we don't currently support sequential number identifiers here, but we should)
  • Shipment identifiers are assigned (should be deprecated in favor of separate)
  • Requisition identifiers are random number identifiers
  • yada yada yada

* Ex: Given a name "Biggie" it might generate "BIG", or "BI0" if "BIG" already exists (up to "BB9").
*/
String generate(String name) {
Integer minSize = grailsApplication.config.getProperty('openboxes.identifier.organization.minSize', Integer)
Copy link
Member

@jmiranda jmiranda Oct 21, 2024

Choose a reason for hiding this comment

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

Potential Tech Huddle conversation to be had ... do we provide the config properties as part of the Params / Context object passed to the generate method OR do we always inject these into the service using the Value annotation.

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah we should discuss. I think @value is the recommended approach in 99% of cases. There are situations here where we need to fetch individual settings by feature type, and for those it makes more sense to use the config service since we don't want to assume which feature settings are defined since they'll vary between implementations.

also the only reason it's using the old way here is because I didn't want to change the code until I had time to revisit and properly refactor it.


@Override
protected Integer countDuplicates(String locationNumber) {
return Location.countByLocationNumber(locationNumber)
Copy link
Member

Choose a reason for hiding this comment

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

Seeing this implementation makes me wonder if the appropriate method name for this is ...

Integer countByIdentifier(String identifier);

That way it's more precise and we're not implying anything by it's invocation. However, maybe the implication ("I want to know how many duplicates are there") is the point.

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah I wanted to be specific about what we need, which is "how many copies of this id already exist in the database". Though I suppose countByIdentifier implies something similar

organization.name = name
organization.partyType = PartyType.findByCode(Constants.DEFAULT_ORGANIZATION_CODE)
organization.code = code?:identifierService.generateOrganizationIdentifier(name)
organization.code = code?:organizationIdentifierService.generate(name)
Copy link
Member

Choose a reason for hiding this comment

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

This does not have to happen now, but passing the organization object here (either in params or as an argument on its own) seems like the right approach.

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah I agree. Just didn't want to do the refactor at this point since it was already getting unwieldy.

return generate(IdentifierGeneratorParams.builder()
.customKeys([
'productCode': productCode,
'organizationCode': organizationCode,
Copy link
Member

@jmiranda jmiranda Oct 21, 2024

Choose a reason for hiding this comment

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

Seeing customKeys being used for the first time, I'm wondering if there's a more intutive name for that. customKeys implies that we need to pass in a list of map keys.

You can leave it for now, but something as simple as data, customData, or customProperties might suffice.

Copy link
Member Author

Choose a reason for hiding this comment

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

I like customProperties

@jmiranda
Copy link
Member

@ewaterman The tests are 👩‍🍳 💋 - nice work. The only lingering doubt for is that we have lots of code changes so lots of avenues for regression. I think we're beyond the point at which we can turn back but if you have any lingering doubts, perhaps you and I can enumerate those fears so it's easier for testing. We can bring them up with Manon and let her decide on whether this gets merged or not.

Aside: @awalkowiak One thing we should have done from the beginning was deploy this branch to its own server for testing. I completely forgot to do that, but perhaps that's an option if Evan would prefer.

Otherwise, if Evan feels confident, let's merge it into develop and start testing it out.

@ewaterman
Copy link
Member Author

@jmiranda I regression tested the identifiers on my local today (and did end up catching a few edge case errors that I fixed and wrote some more automation tests for).

I think because this is going to develop (and not a release branch) I feel confident enough about the change in that it won't break any flows and the ids all look correct. But yes we can definitely enumerate the regression testing steps to perform. I'll add that to the ticket.

One thing I'm not certain about is POs. Mine generates PO-MA-000000 but everything I see on prod is INTPO000000. However looking at the old code, it seems like it should also generate with a format like mine. I'll chat with the team about it tomorrow before I merge anything in.

@ewaterman
Copy link
Member Author

ewaterman commented Oct 22, 2024

answered my own question. PIH environments have set this in their openboxes-config.groovy:

openboxes.identifier.product.format = 'LLNNN'  // overrides Constants.DEFAULT_PRODUCT_NUMBER_FORMAT='LLNN'
openboxes.identifier.purchaseOrder.format = "\${destinationPartyCode}PO\${sequenceNumber}"

This is fine because they're just overrides so shouldn't clash with my changes.

We could update what we have in runtime.groovy to match what all our environments have so that we're more consistent, but not strictly required. Lets chat tomorrow about it.

@ewaterman ewaterman merged commit e597cec into develop Oct 23, 2024
@ewaterman ewaterman deleted the maintenance/OBPIH-5182-custom-id-refactor branch October 23, 2024 14:32
@ewaterman ewaterman restored the maintenance/OBPIH-5182-custom-id-refactor branch October 24, 2024 14:06
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 flag: config change Hilights a pull request that contains a change to the app config type: maintenance Code improvements, optimizations and refactors, dependency upgrades...

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants