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

Move check for existing participants (for a specific contact/event) out of event registration forms #30698

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

jensschuppe
Copy link
Contributor

Overview

Validating event registrations implies checking for existing participants, which has been done in the form classes where needed by querying CRM_Event_BAO_Participant objects. This PR tries to refactor that part out into the Participant BAO class and using the API for doing so.

Before

Different event registration and participant forms did this validation on their own (with differing criteria!), there was no central point of checking for existing participants that could've been reliably called from elsewhere.

After

Checking for existing participants is now a static method (actually two) on CRM_Event_BAO_Participant and can be called from elsewhere - eventually an API action validating an order.

Technical Details

The new methods have a fairly extensive signature, which I'm not sure is necessary and which I'd like to be discussed, hence this PR being a draft for now. The calls I added (replacing individual queries) had differing criteria for checking for existing participants, which I tried to retain semantically.

Comments

Shouldn't we rely on is_counted only and not bother with On waitlist or Cancelled status separately? This would make the signatures of the new methods cleaner. And if you configure On waitlist or Cancelled to be counted, the current assumptions don't really fit anymore …

Also, why does \CRM_Event_Form_Registration_Register::checkRegistration() take roles into account and other forms don't?

Also, I'm quite sure the checks in \CRM_Event_Form_Participant::formRule() and \CRM_Event_Form_Task_Register::postProcess() should not include test participants, as their current implementations do, and they don't care for is_counted or other status-related attributes at all.

Lastly, did I miss any more checks that can be replaced by the new method?

@colemanw, @JoeMurray, @eileenmcnaughton happy to hear your thoughts!

Copy link

civibot bot commented Jul 17, 2024

🤖 Thank you for contributing to CiviCRM! ❤️ We will need to test and review this PR. 👷

Introduction for new contributors...
  • If this is your first PR, an admin will greenlight automated testing with the command ok to test or add to whitelist.
  • A series of tests will automatically run. You can see the results at the bottom of this page (if there are any problems, it will include a link to see what went wrong).
  • A demo site will be built where anyone can try out a version of CiviCRM that includes your changes.
  • If this process needs to be repeated, an admin will issue the command test this please to rerun tests and build a new demo site.
  • Before this PR can be merged, it needs to be reviewed. Please keep in mind that reviewers are volunteers, and their response time can vary from a few hours to a few weeks depending on their availability and their knowledge of this particular part of CiviCRM.
  • A great way to speed up this process is to "trade reviews" with someone - find an open PR that you feel able to review, and leave a comment like "I'm reviewing this now, could you please review mine?" (include a link to yours). You don't have to wait for a response to get started (and you don't have to stop at one!) the more you review, the faster this process goes for everyone 😄
  • To ensure that you are credited properly in the final release notes, please add yourself to contributor-key.yml
  • For more information about contributing, see CONTRIBUTING.md.
Quick links for reviewers...

➡️ Online demo of this PR 🔗

@civibot civibot bot added the master label Jul 17, 2024
CRM/Event/BAO/Participant.php Show resolved Hide resolved
CRM/Event/Form/Participant.php Outdated Show resolved Hide resolved
CRM/Event/Form/Participant.php Outdated Show resolved Hide resolved
CRM/Event/Form/Task/Register.php Show resolved Hide resolved
@eileenmcnaughton
Copy link
Contributor

@jensschuppe I agree with your instinct to figure out the signature up front.

I'm trying to think through the logic you have exposed

Fundamentally the code seems to be checking for duplicates on the contact ID & event ID with filters on status & role & (of doubtful benefit) on 'is_test'

        $dupeCheck = new CRM_Event_BAO_Participant();
        $dupeCheck->contact_id = $contactId;
        $dupeCheck->event_id = $eventId;
        $dupeCheck->whereAdd("status_id != {$cancelledStatusID} ");
        $dupeCheck->find(TRUE);

I think (and @colemanw is the person to confirm) we should maybe consider adding a v4 API

Participant.getDuplicates() and also Contribution.getDuplicates() (I don't love the function name but we do have Contact.getDuplicates().

The question becomes - what does the signature look like & @colemanw will likely have thoughts - but

Participant.getDuplicates()
 ->addWhere('event_id', '=', $eventID)
 ->addWhere('contact_id', '=', $contactID)
// optional
 ->addWhere('role_id:name', 'NOT IN', ['Attended'])
 ->addWhere('status_id.is_counted', '=', TRUE)
 ->addWhere('status_id.name', '=', 'Waitlist');

The main difference between this & calling Participant.get() seems to be the addition of a default of '!= Cancelled' on status & possibly a default of excluding is_test

A couple of other notes

  • I would also say we need to check out whether there is adequate test cover on the lines that are being refactored.

  • We need to be really careful we don't add a BAO method with any implied support for external usage because any functionality should be exposed via the api ONLY. Of course the general rule is that BAO methods are generally NOT supported for external use but it's better to be explicit. You can tag the function @internal with a comment that it is not supported for external use

  • I think the 2 back office forms should share an object oriented function since one inherits from the other - I don't know it makes sense for them to share with the front end form because really all they are doing is a participant get with an exclusion on status of cancelled and perhaps an exclusion on is test.

@eileenmcnaughton
Copy link
Contributor

@MegaphoneJon I feel like you might know some of the is_counted stuff

@MegaphoneJon
Copy link
Contributor

@MegaphoneJon I feel like you might know some of the is_counted stuff

But...what if I pretended I didn't?

There are several different functions that try to count this. Some are only for waitlists, some exclude the waitlist, and others are just wrong (e.g. ignoring Participant.participant_count and treating each participant record as one person).

IMO, the most comprehensive and correct implementation (and I've banged on it a bit) is CRM_Event_BAO_Participant::eventFull() - which, despite its name, will return the number of available seats, not just a boolean.

@MegaphoneJon
Copy link
Contributor

IIRC - API4 does not use this function.

@eileenmcnaughton
Copy link
Contributor

@MegaphoneJon I agree - pretending not to know is often the best approach!

@MegaphoneJon
Copy link
Contributor

I just skimmed over this code to see if it was redundant with the participant eventFull() function - but it's not. I'm in favor of this change.

@jensschuppe jensschuppe changed the title Move check for existing participants out of event registration forms Move check for existing participants (for a specific contact/event) out of event registration forms Jul 18, 2024
@jensschuppe
Copy link
Contributor Author

I just skimmed over this code to see if it was redundant with the participant eventFull() function - but it's not.

Just changed the title to clarify that this PR is about checking for existing participants for a specific contact and event (i.e. not registering the same person twice). Checking for available seats and price options are next steps 😄

@eileenmcnaughton
Copy link
Contributor

I had a go at pseudocoding where this might eventually wind up as part of a validate action over at

https://github.com/civicrm/civicrm-core/pull/30730/files#diff-20459ef04e7e201eb28f6c30e2f170062e6107cb21f39304741e43d6fd565905R49

CRM/Event/Form/Participant.php Outdated Show resolved Hide resolved
CRM/Event/Form/Participant.php Outdated Show resolved Hide resolved
CRM/Event/Form/Participant.php Outdated Show resolved Hide resolved
CRM/Event/Form/Task/Register.php Show resolved Hide resolved
@eileenmcnaughton
Copy link
Contributor

Just looking at the comments - I came to most of the same conclusions when I had a go at putting this in a more 'final' place - #30768

@jensschuppe
Copy link
Contributor Author

@eileenmcnaughton

Just looking at the comments - I came to most of the same conclusions when I had a go at putting this in a more 'final' place - #30768

@colemanw came up with the idea of providing this business logic functionality outside of the API (as that might get updated eventually to e.g. APIv5) on a more abstract level by having a generic BAO/Record validate Symfony event.

We agreed to have the logic it in the BAO and the API invoke that low-level event which in turn calls the actual logic in the BAO. Also, this is just part of the complete validation for participants (or better: event registrations, which might be about more to validate than just a single contact's participant objects). I renamed the new methods accordingly to reflect that it's the logic looking for counted participants for a contact/event combination.

So this PR is for moving the logic out of the forms into the BAO as a first step. Having an API, a validate Symfony event, etc. comes next.

@jensschuppe
Copy link
Contributor Author

The failing test suggests that the On waitlist status does not have is_counted by default. I'm reverting the last commit that introduced more assumptions about specific status and their is_counted attribute.

@jensschuppe jensschuppe marked this pull request as ready for review August 13, 2024 11:48
@jensschuppe
Copy link
Contributor Author

As this came back green, I'd suggest merging it and to have follow-up PRs for:

  • excluding is_test participants in counting globally
  • figuring out whether and how counting should rely on is_counted only (as the On waitlist status doesn't seem to have that attribute by default)
  • calling the two new methods from the API4 validate action in dev/core#5308 : APIv4 - Add Order.validate #30716 or in an entity-level event (see this comment)

@eileenmcnaughton
Copy link
Contributor

@jensschuppe I think the main value of this PR is the unit tests that would be added with it - then we can move the code around safely - ie I think the code would live better on the api layer per #30768 - because the api lends itself to a saner signature - ie


    $duplicate = Participant::getDuplicates(FALSE)
      ->setValues([
        'event_id' => $this->ids['Event']['event'],
        'contact_id' => $this->ids['Contact']['default'],
      ])->execute();

Of course there is the idea that logic should sit on the BAO not the api - it's a pretty thin bit of functionality so it is not completely out of line with other stuff at the api level but @colemanw was looking to define a validate listener at the BAO level so maybe a getDuplicates one makes sense there too - & if that is defined we can move it there - & will be able to rely on the tests that are added to support moving the code again at that point

@jensschuppe jensschuppe force-pushed the checkExistingParticipants branch 2 times, most recently from 489ff75 to 454af83 Compare August 27, 2024 12:41
@jensschuppe
Copy link
Contributor Author

jensschuppe commented Aug 27, 2024

In reaction to @eileenmcnaughton's comment and following what we've been discussing in the dev-financial mattermost channel and in meetings with @JoeMurray, @monishdeb, and @colemanw, I tried to capture more business logic of the event registration validation in BAO methods and call them from the form layer, while also trying to understand (and illustrate in the code) the different aspects of validation, such as differences between admin and public forms.

I (and others mentioned above) disagree about business logic living in the API. The API should be a thin wrapper around BAO code and the interface/signature should still be discussed, as a simple Participant.getDuplicates might not be enough.

I see that intermediate merges maybe don't make sense, so I'm continuing my work on this PR with the principles we tried to lay out in our previous meeting:

  • business logic to move into the Participant BAO
  • error messages to be defined in the business logic
  • form layer to be able to "enhance" error messages and define the means of presentation (e.g. adding to an error message with a link to a form, or redirecting)
  • adapting signatures of internal business logic methods over time until we have moved everything to where it belongs and an API signature can be defined upon that
  • form layer to eventually call the API for validating, but in the meantime continue to call BAO methods or custom formRule code

@eileenmcnaughton
Copy link
Contributor

Hmm OK - I think my main point is that we should be front ending the focus on unit tests with this work

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
5 participants