Skip to content

Dropping Center: Goonj Office Allocation Based on State#281

Merged
belwalshubham merged 15 commits intodevelopfrom
feat/assign-goonj-office-by-state
Oct 14, 2024
Merged

Dropping Center: Goonj Office Allocation Based on State#281
belwalshubham merged 15 commits intodevelopfrom
feat/assign-goonj-office-by-state

Conversation

@belwalshubham
Copy link
Copy Markdown
Collaborator

@belwalshubham belwalshubham commented Sep 28, 2024

This PR implements the automatic assignment of Goonj offices based on the state selected during the filling of dropping center intent form.

Added Goonj Office and PU coordinator for dropping center Based on State

image

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Introduced new constants for fallback office and relationship type names.
    • Added functionality to manage office details during the creation of collection camps, including improved coordinator assignments.
    • Introduced new traits for retrieving subtype names associated with entities.
  • Improvements

    • Enhanced logic for managing collection camps and office assignments, including robust error handling for coordinator availability.

@belwalshubham belwalshubham self-assigned this Sep 28, 2024
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Oct 9, 2024

Caution

Review failed

The pull request is closed.

Walkthrough

The DroppingCenterService class in Civi/DroppingCenterService.php has been significantly modified. New constants for fallback office and relationship type names have been added. The getSubscribedEvents method now includes a hook referencing the setOfficeDetails method. Several private static methods have been introduced to manage office and coordinator assignments effectively. A new method, setOfficeDetails, has been implemented to handle office details during the creation of collection camps, improving the logic for assigning offices and coordinators based on state.

Changes

File Path Change Summary
wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php - Added constants: FALLBACK_OFFICE_NAME, RELATIONSHIP_TYPE_NAME.
- Added method: setOfficeDetails($op, $groupID, $entityID, &$params).
- Updated getSubscribedEvents to include &hook_civicrm_custom.
- Added private static methods: getFallbackCoordinator(), findStateField(array $array), getFallbackOffice().
wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php - Added trait: use SubtypeSource;.
- Updated method signature: setOfficeDetails($op, $groupID, $entityID, &$params) to include subtype checks and error handling.
wp-content/civi-extensions/goonjcustom/Civi/Traits/SubtypeSource.php - Added method: getEntitySubtypeName($entityID) for retrieving subtype names associated with entity IDs.
wp-content/civi-extensions/goonjcustom/Civi/Traits/CollectionSource.php - Added method: getEntitySubtypeName($entityID) for retrieving subtype names associated with entity IDs.

Possibly related PRs

Suggested labels

status : ready for review

Suggested reviewers

  • pokhiii

🎉 In the land of code where changes flow,
A service was tweaked to help us grow.
With offices set and coordinators found,
Goonj's mission thrives, spreading joy all around!
New methods added, constants defined,
In the world of Civi, progress aligned! 🌟


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 5033b74 and c5acf4a.

📒 Files selected for processing (1)
  • wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php (2 hunks)
🧰 Additional context used
🔇 Additional comments (1)
wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php (1)

159-161: Prevent 'rand()' function errors with zero coordinators

When $coordinatorCount is greater than one, you use rand(0, $coordinatorCount - 1). If $coordinatorCount is zero, this will cause an error. However, the condition checks for $coordinatorCount > 1, so zero should not occur here. Ensure that this condition reliably prevents zero values.

Comment thread wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php Outdated
Comment on lines +100 to +174
public static function setOfficeDetails($op, $groupID, $entityID, &$params) {
if ($op !== 'create') {
return;
}

if (!($stateField = self::findStateField($params))) {
return;
}

$stateId = $stateField['value'];

$collectionCampId = $stateField['entity_id'];

$collectionCamp = EckEntity::get('Collection_Camp', FALSE)
->addSelect('Dropping_Centre.Will_your_dropping_center_be_open_for_general_public_as_well_out')
->addWhere('id', '=', $collectionCampId)
->execute();

$collectionCampData = $collectionCamp->first();

if (!$stateId) {
\CRM_Core_Error::debug_log_message('Cannot assign Goonj Office to collection camp: ' . $collectionCamp['id']);
\CRM_Core_Error::debug_log_message('No state provided on the intent for collection camp: ' . $collectionCamp['id']);
return FALSE;
}

$officesFound = Contact::get(FALSE)
->addSelect('id')
->addWhere('contact_type', '=', 'Organization')
->addWhere('contact_sub_type', 'CONTAINS', 'Goonj_Office')
->addWhere('Goonj_Office_Details.Collection_Camp_Catchment', 'CONTAINS', $stateId)
->execute();

$stateOffice = $officesFound->first();

// If no state office is found, assign the fallback state office.
if (!$stateOffice) {
$stateOffice = self::getFallbackOffice();
}

$stateOfficeId = $stateOffice['id'];

EckEntity::update('Collection_Camp', FALSE)
->addValue('Dropping_Centre.Goonj_Office', $stateOfficeId)
->addWhere('id', '=', $collectionCampId)
->execute();

$coordinators = Relationship::get(FALSE)
->addWhere('contact_id_b', '=', $stateOfficeId)
->addWhere('relationship_type_id:name', '=', self::RELATIONSHIP_TYPE_NAME)
->addWhere('is_current', '=', TRUE)
->execute();

$coordinatorCount = $coordinators->count();

if ($coordinatorCount === 0) {
$coordinator = self::getFallbackCoordinator();
}
elseif ($coordinatorCount > 1) {
$randomIndex = rand(0, $coordinatorCount - 1);
$coordinator = $coordinators->itemAt($randomIndex);
}
else {
$coordinator = $coordinators->first();
}

$coordinatorId = $coordinator['contact_id_a'];

EckEntity::update('Collection_Camp', FALSE)
->addValue('Dropping_Centre.Coordinating_Urban_POC', $coordinatorId)
->addWhere('id', '=', $collectionCampId)
->execute();

return TRUE;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Adhere to Single Responsibility Principle in 'setOfficeDetails'

The setOfficeDetails method is handling multiple responsibilities, including state field retrieval, office assignment, and coordinator selection. Consider refactoring this method into smaller, single-purpose methods to improve maintainability and readability.

Comment thread wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php Outdated
Comment thread wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php Outdated
Comment on lines +38 to +42
$fallbackOffice = self::getFallbackOffice();
$fallbackCoordinators = Relationship::get(FALSE)
->addWhere('contact_id_b', '=', $fallbackOffice['id'])
->addWhere('relationship_type_id:name', '=', self::RELATIONSHIP_TYPE_NAME)
->addWhere('is_current', '=', FALSE)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Incorrect 'is_current' condition in 'getFallbackCoordinator'

The method getFallbackCoordinator is filtering relationships where is_current is FALSE. Typically, is_current indicates whether a relationship is active. Filtering inactive coordinators may not be intended. Consider changing the condition to is_current equals TRUE to retrieve active coordinators.

Apply this diff to correct the condition:

-      ->addWhere('is_current', '=', FALSE)
+      ->addWhere('is_current', '=', TRUE)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$fallbackOffice = self::getFallbackOffice();
$fallbackCoordinators = Relationship::get(FALSE)
->addWhere('contact_id_b', '=', $fallbackOffice['id'])
->addWhere('relationship_type_id:name', '=', self::RELATIONSHIP_TYPE_NAME)
->addWhere('is_current', '=', FALSE)
$fallbackOffice = self::getFallbackOffice();
$fallbackCoordinators = Relationship::get(FALSE)
->addWhere('contact_id_b', '=', $fallbackOffice['id'])
->addWhere('relationship_type_id:name', '=', self::RELATIONSHIP_TYPE_NAME)
->addWhere('is_current', '=', TRUE)

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between c5acf4a and 96c8d5b.

📒 Files selected for processing (1)
  • wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php (6 hunks)
🧰 Additional context used
🔇 Additional comments (7)
wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php (7)

66-70: Previous Issue Still Present: Incorrect 'is_current' Condition in 'getFallbackCoordinator'

The condition is_current is still set to FALSE, which may be incorrect if you intend to retrieve active coordinators.


82-86: Previous Refactor Suggestion Still Applicable in 'findStateField' Method

The suggestion to enhance readability by clarifying the purpose of the findStateField method is still applicable.


101-107: Previous Refactor Suggestion Still Applicable: Simplify Array Search Logic

The recommendation to simplify the array search logic in the findStateField method remains valid.


111-117: Previous Issue Still Present: Handle Case When Fallback Office Is Not Found

The potential issue of the fallback office not being found and returning NULL is still present.


155-157: Previous Issue Still Present: Use Exceptions Instead of Debug Logs for Error Handling

Using \CRM_Core_Error::debug_log_message may not provide immediate feedback. Consider throwing an exception instead.


167-172: Previous Issue Still Present: Consistent Error Handling When Assigning Fallback Office

The code assigns a fallback office without checking if the retrieval was successful. This issue remains unaddressed.


134-208: Previous Refactor Suggestion Still Applicable: Adhere to Single Responsibility Principle in 'setOfficeDetails'

The method setOfficeDetails is still handling multiple responsibilities. The previous recommendation to refactor this method into smaller, single-purpose methods remains valid.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Outside diff range and nitpick comments (5)
wp-content/civi-extensions/goonjcustom/Civi/Traits/SubtypeSource.php (2)

5-8: Beef up that PHPDoc, champ!

While you've got a good start with the trait description, let's make it even more informative. Consider adding:

  • A more detailed explanation of when and why to use this trait
  • Any dependencies or requirements for using the trait
  • Examples of classes that might benefit from using this trait

Here's a beefier version to consider:

/**
 * Trait to handle subtype name retrieval by entity ID.
 *
 * This trait provides a method for fetching the subtype name of an entity
 * using the CiviCRM API. It's useful for classes that need to work with
 * entity subtypes, particularly in the context of Collection Camps.
 *
 * @see \Civi\Api4\Eck_Collection_Camp
 */

10-18: Let's soup up that method PHPDoc!

Your PHPDoc game is strong, but let's take it to the next level:

  1. Add a @throws tag to document any exceptions that might be thrown.
  2. Provide an example usage of the method.
  3. Mention any performance considerations.

Here's a souped-up version:

/**
 * Get the subtype name by entity ID.
 *
 * This method fetches the subtype name for a given Collection Camp entity ID
 * using the CiviCRM API.
 *
 * @param int $entityID
 *   The ID of the Collection Camp entity.
 *
 * @return string|null
 *   The subtype name or NULL if not found.
 *
 * @throws \Civi\API\Exception\UnauthorizedException
 *   If the user doesn't have permission to access the API.
 *
 * @example
 *   $subtypeName = SubtypeSource::getSubtypeNameByEntityId(123);
 *
 * @note This method makes an API call, so use it judiciously in performance-critical sections.
 */
wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php (2)

23-25: Consider a more flexible approach for FALLBACK_OFFICE_NAME.

The addition of constants improves code readability. However, hardcoding FALLBACK_OFFICE_NAME to 'Delhi' might limit flexibility. Consider making this configurable, perhaps through a settings file or database entry, to allow for easy changes in the future without modifying the code.


Line range hint 1-227: Overall assessment: Functionality implemented, but room for improvement.

The changes introduce new functionality for office and coordinator assignment in the DroppingCenterService class. While the implementation achieves its goals, there are several areas where the code could be improved:

  1. Code duplication: There's repeated logic for coordinator selection that could be extracted into a shared method.
  2. Error handling: Several methods could benefit from more robust error handling, particularly getFallbackOffice and parts of setOfficeDetails.
  3. Single Responsibility Principle: The setOfficeDetails method is handling multiple responsibilities and could be broken down into smaller, more focused methods.
  4. Configurability: Consider making the FALLBACK_OFFICE_NAME configurable rather than hardcoded.

Addressing these points will improve the overall quality, maintainability, and robustness of the code. Despite these areas for improvement, the core functionality appears to be implemented correctly.

wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php (1)

636-639: Throw More Specific Exceptions

In line 637, a generic \Exception is thrown:

throw new \Exception('No coordinator available to assign.');

Using a generic exception makes error handling less precise. Consider using a more specific exception class, such as \RuntimeException, or define a custom exception to provide better context.

Apply this diff to specify the exception type:

- throw new \Exception('No coordinator available to assign.');
+ throw new \RuntimeException('No coordinator available to assign.');
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 96c8d5b and 26b236d.

📒 Files selected for processing (3)
  • wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php (3 hunks)
  • wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php (4 hunks)
  • wp-content/civi-extensions/goonjcustom/Civi/Traits/SubtypeSource.php (1 hunks)
🧰 Additional context used
🔇 Additional comments (4)
wp-content/civi-extensions/goonjcustom/Civi/Traits/SubtypeSource.php (2)

1-3: LGTM: Namespace declaration is spot-on!

The namespace Civi\Traits is correctly declared and aligns perfectly with the file's location in the directory structure. Keep up the good work!


8-8: Trait declaration: Nailed it!

The SubtypeSource trait is declared with finesse. It's clear, concise, and follows the single responsibility principle. Keep rocking those traits!

wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php (2)

5-8: LGTM: New use statements are appropriate.

The added use statements for Contact and Relationship classes are relevant to the new functionality implemented in this file.


34-35: LGTM: New hook subscription added.

The addition of the '&hook_civicrm_custom' hook, associated with the setOfficeDetails method, is appropriate for the new functionality being implemented.

Comment on lines +19 to +32
public static function getSubtypeNameByEntityId($entityID) {
$getSubtypeName = civicrm_api4('Eck_Collection_Camp', 'get', [
'select' => [
'subtype:name',
],
'where' => [
['id', '=', $entityID],
],
]);

$entityData = $getSubtypeName[0] ?? [];

return $entityData['subtype:name'] ?? NULL;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Time to turbocharge that method implementation!

Your code is looking sharp, but let's add some nitro to this engine:

  1. Add type hinting to the method signature and return type:

    public static function getSubtypeNameByEntityId(int $entityID): ?string
  2. Wrap the API call in a try-catch block to handle potential exceptions:

    try {
      $getSubtypeName = civicrm_api4('Eck_Collection_Camp', 'get', [
        'select' => ['subtype:name'],
        'where' => [['id', '=', $entityID]],
      ]);
      return $getSubtypeName[0]['subtype:name'] ?? null;
    }
    catch (\Civi\API\Exception\UnauthorizedException $e) {
      // Log the error or handle it as appropriate
      return null;
    }
  3. Consider adding a limit to the API call to ensure we're only fetching one result:

    'limit' => 1,

These changes will make your code more robust, type-safe, and efficient. Keep crushing it!

Comment on lines +85 to +115
/**
*
*/
private static function findStateField(array $array) {
$filteredItems = array_filter($array, fn($item) => $item['entity_table'] === 'civicrm_eck_collection_camp');

if (empty($filteredItems)) {
return FALSE;
}

$collectionCampStateFields = CustomField::get(FALSE)
->addSelect('id')
->addWhere('name', '=', 'State')
->addWhere('custom_group_id:name', '=', 'Dropping_Centre')
->execute()
->first();

if (!$collectionCampStateFields) {
return FALSE;
}

$stateFieldId = $collectionCampStateFields['id'];

$stateItemIndex = array_search(TRUE, array_map(fn($item) =>
$item['entity_table'] === 'civicrm_eck_collection_camp' &&
$item['custom_field_id'] == $stateFieldId,
$filteredItems
));

return $stateItemIndex !== FALSE ? $filteredItems[$stateItemIndex] : FALSE;
}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Oct 12, 2024

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Simplify findStateField method for better readability and performance.

The current implementation is overly complex. Consider simplifying it to improve readability and performance. Here's a suggested refactoring:

private static function findStateField(array $array) {
    $stateFieldId = CustomField::get(FALSE)
        ->addSelect('id')
        ->addWhere('name', '=', 'State')
        ->addWhere('custom_group_id:name', '=', 'Dropping_Centre')
        ->execute()
        ->first()['id'] ?? null;

    if (!$stateFieldId) {
        return FALSE;
    }

    foreach ($array as $item) {
        if ($item['entity_table'] === 'civicrm_eck_collection_camp' && 
            $item['custom_field_id'] == $stateFieldId) {
            return $item;
        }
    }

    return FALSE;
}

This refactoring reduces the number of array operations and simplifies the logic, making it easier to read and potentially more performant.

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.

@belwalshubham can we do this?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines +144 to +226
/**
*
*/
public static function setOfficeDetails($op, $groupID, $entityID, &$params) {
if ($op !== 'create' || self::getSubtypeNameByEntityId($entityID) !== self::ENTITY_SUBTYPE_NAME) {
return;
}

if (!($stateField = self::findStateField($params))) {
return;
}

$stateId = $stateField['value'];

$collectionCampId = $stateField['entity_id'];

$collectionCamp = EckEntity::get('Collection_Camp', FALSE)
->addSelect('Dropping_Centre.Will_your_dropping_center_be_open_for_general_public_as_well_out')
->addWhere('id', '=', $collectionCampId)
->execute();

$collectionCampData = $collectionCamp->first();

if (!$stateId) {
\CRM_Core_Error::debug_log_message('Cannot assign Goonj Office to collection camp: ' . $collectionCamp['id']);
\CRM_Core_Error::debug_log_message('No state provided on the intent for collection camp: ' . $collectionCamp['id']);
return FALSE;
}

$officesFound = Contact::get(FALSE)
->addSelect('id')
->addWhere('contact_type', '=', 'Organization')
->addWhere('contact_sub_type', 'CONTAINS', 'Goonj_Office')
->addWhere('Goonj_Office_Details.Collection_Camp_Catchment', 'CONTAINS', $stateId)
->execute();

$stateOffice = $officesFound->first();

// If no state office is found, assign the fallback state office.
if (!$stateOffice) {
$stateOffice = self::getFallbackOffice();
}

$stateOfficeId = $stateOffice['id'];

EckEntity::update('Collection_Camp', FALSE)
->addValue('Dropping_Centre.Goonj_Office', $stateOfficeId)
->addWhere('id', '=', $collectionCampId)
->execute();

$coordinators = Relationship::get(FALSE)
->addWhere('contact_id_b', '=', $stateOfficeId)
->addWhere('relationship_type_id:name', '=', self::RELATIONSHIP_TYPE_NAME)
->addWhere('is_current', '=', TRUE)
->execute();

$coordinatorCount = $coordinators->count();

if ($coordinatorCount === 0) {
$coordinator = self::getFallbackCoordinator();
}
elseif ($coordinatorCount > 1) {
$randomIndex = rand(0, $coordinatorCount - 1);
$coordinator = $coordinators->itemAt($randomIndex);
}
else {
$coordinator = $coordinators->first();
}

if (!$coordinator) {
throw new \Exception('No coordinator available to assign.');
}

$coordinatorId = $coordinator['contact_id_a'];

EckEntity::update('Collection_Camp', FALSE)
->addValue('Dropping_Centre.Coordinating_Urban_POC', $coordinatorId)
->addWhere('id', '=', $collectionCampId)
->execute();

return TRUE;

}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Refactor setOfficeDetails for better maintainability and error handling.

This method is handling multiple responsibilities and is quite long. Consider breaking it down into smaller, more focused methods to improve maintainability and readability. Also, there are several areas where error handling could be improved:

  1. Break down the method into smaller functions, each handling a specific task (e.g., findOffice, assignOffice, findCoordinator, assignCoordinator).
  2. Replace error logging with exceptions for better error handling.
  3. Use the previously suggested selectRandomCoordinator method to reduce code duplication.

Here's a high-level example of how you might refactor this:

public static function setOfficeDetails($op, $groupID, $entityID, &$params) {
    if ($op !== 'create' || self::getSubtypeNameByEntityId($entityID) !== self::ENTITY_SUBTYPE_NAME) {
        return;
    }

    $stateField = self::findStateField($params);
    if (!$stateField) {
        throw new \Exception("State field not found for collection camp: {$entityID}");
    }

    $stateId = $stateField['value'];
    $collectionCampId = $stateField['entity_id'];

    $office = self::findOffice($stateId);
    self::assignOffice($collectionCampId, $office['id']);

    $coordinator = self::findCoordinator($office['id']);
    self::assignCoordinator($collectionCampId, $coordinator['contact_id_a']);

    return TRUE;
}

private static function findOffice($stateId) {
    // Logic to find office based on state
}

private static function assignOffice($collectionCampId, $officeId) {
    // Logic to assign office to collection camp
}

private static function findCoordinator($officeId) {
    // Logic to find coordinator for office
}

private static function assignCoordinator($collectionCampId, $coordinatorId) {
    // Logic to assign coordinator to collection camp
}

This refactoring improves the overall structure of the code, making it more maintainable and easier to test individual components.

Comment thread wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php Outdated
Comment thread wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php Outdated
*
*/
public static function setOfficeDetails($op, $groupID, $entityID, &$params) {
if ($op !== 'create' || self::getSubtypeNameByEntityId($entityID) !== self::ENTITY_SUBTYPE_NAME) {
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 created a new trait to retrieve the subtype name because the setOfficeDetails function doesn't provide the subtype ID. However, I do get the entity ID, so I’m using that to fetch the subtype name. The setOfficeDetails function should work for both the Dropping Center and Collection Camp.

@belwalshubham belwalshubham requested a review from pokhiii October 12, 2024 07:22
*/
public static function setOfficeDetails($op, $groupID, $entityID, &$params) {
if ($op !== 'create') {
if ($op !== 'create' || self::getSubtypeNameByEntityId($entityID) !== self::ENTITY_SUBTYPE_NAME) {
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.

Can we simplify the name: getEnitySubtypeName

Suggested change
if ($op !== 'create' || self::getSubtypeNameByEntityId($entityID) !== self::ENTITY_SUBTYPE_NAME) {
if ($op !== 'create' || self::getEntitySubtypeName($entityID) !== self::ENTITY_SUBTYPE_NAME) {

}

if (!$coordinator) {
throw new \Exception('No coordinator available to assign.');
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.

Who is catching this expection?

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 think, Instead of throwing an exception, I can do something like below returning a specific value (e.g., FALSE or null) when no coordinator is found, this is also a better way of error handling. what do you think Pokhi ?

if (!$coordinator) {
    \CRM_Core_Error::debug_log_message('No coordinator available to assign.');
    return FALSE;
}

$fallbackCoordinators = Relationship::get(FALSE)
->addWhere('contact_id_b', '=', $fallbackOffice['id'])
->addWhere('relationship_type_id:name', '=', self::RELATIONSHIP_TYPE_NAME)
->addWhere('is_current', '=', FALSE)
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.

Why is is_current false used?

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 think this is not correct here. Setting it to false means that we would return relationships that are not currently active, but we need to retrieve only the active ones. This should be set to true. I have made this correction in both the collection camp and Dropping Center.

Comment on lines +85 to +115
/**
*
*/
private static function findStateField(array $array) {
$filteredItems = array_filter($array, fn($item) => $item['entity_table'] === 'civicrm_eck_collection_camp');

if (empty($filteredItems)) {
return FALSE;
}

$collectionCampStateFields = CustomField::get(FALSE)
->addSelect('id')
->addWhere('name', '=', 'State')
->addWhere('custom_group_id:name', '=', 'Dropping_Centre')
->execute()
->first();

if (!$collectionCampStateFields) {
return FALSE;
}

$stateFieldId = $collectionCampStateFields['id'];

$stateItemIndex = array_search(TRUE, array_map(fn($item) =>
$item['entity_table'] === 'civicrm_eck_collection_camp' &&
$item['custom_field_id'] == $stateFieldId,
$filteredItems
));

return $stateItemIndex !== FALSE ? $filteredItems[$stateItemIndex] : FALSE;
}
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.

@belwalshubham can we do this?

$collectionCampData = $collectionCamp->first();

if (!$stateId) {
\CRM_Core_Error::debug_log_message('Cannot assign Goonj Office to collection camp: ' . $collectionCamp['id']);
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.

Suggested change
\CRM_Core_Error::debug_log_message('Cannot assign Goonj Office to collection camp: ' . $collectionCamp['id']);
\CRM_Core_Error::debug_log_message('Cannot assign Goonj Office to dropping center: ' . $collectionCamp['id']);


$stateId = $stateField['value'];

$collectionCampId = $stateField['entity_id'];
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.

Suggested change
$collectionCampId = $stateField['entity_id'];
$droppingCenterId = $stateField['entity_id'];


$collectionCampId = $stateField['entity_id'];

$collectionCamp = EckEntity::get('Collection_Camp', FALSE)
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 think here collection camp is more suitable

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.

My bad. It should be

Suggested change
$collectionCamp = EckEntity::get('Collection_Camp', FALSE)
$droppingCenter = EckEntity::get('Collection_Camp', FALSE)


EckEntity::update('Collection_Camp', FALSE)
->addValue('Dropping_Centre.Coordinating_Urban_POC', $coordinatorId)
->addWhere('id', '=', $collectionCampId)
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.

Suggested change
->addWhere('id', '=', $collectionCampId)
->addWhere('id', '=', $droppingCenterId)

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.

Instead of creating a new trait can you continue using: wp-content/civi-extensions/goonjcustom/Civi/Traits/CollectionSource.php

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Outside diff range and nitpick comments (7)
wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php (2)

20-25: Constants and trait usage look good, but consider grouping constants.

The addition of the SubtypeSource trait and the new constants improve code organization and readability. However, consider grouping related constants together for better maintainability.

Consider rearranging the constants as follows:

  const ENTITY_NAME = 'Collection_Camp';
+ const ENTITY_SUBTYPE_NAME = 'Dropping_Center';
  const RELATIONSHIP_TYPE_NAME = 'Dropping Center Coordinator of';
- const ENTITY_SUBTYPE_NAME = 'Dropping_Center';
  const FALLBACK_OFFICE_NAME = 'Delhi';

Line range hint 1-300: Overall improvements with room for further refinement

The DroppingCenterService class has been significantly enhanced with new functionality for office and coordinator assignment. The additions of new methods and constants improve the overall structure and capabilities of the class.

However, there are still some areas that could be improved:

  1. Consider extracting the office and coordinator assignment logic into a separate class to better adhere to the Single Responsibility Principle.
  2. Implement consistent error handling throughout the class, preferring exceptions over error logging.
  3. Add comprehensive unit tests for the new methods to ensure reliability and ease of future maintenance.

To further improve the architecture, consider:

  1. Creating an OfficeAssignmentService class to handle the office and coordinator assignment logic.
  2. Implementing a CoordinatorSelectionStrategy interface with different implementations (e.g., RandomCoordinatorStrategy, FallbackCoordinatorStrategy) to make the coordinator selection process more flexible and extensible.
  3. Using dependency injection to provide the necessary services and strategies to the DroppingCenterService class, improving testability and flexibility.

These changes would result in a more modular, maintainable, and testable codebase.

wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php (5)

Line range hint 1-30: Consider reducing class dependencies

The CollectionCampService class has a large number of use statements, which might indicate high coupling and potential violations of the Single Responsibility Principle. Consider refactoring the class to reduce its dependencies and improve maintainability.

Would you like assistance in identifying potential areas for refactoring?


Line range hint 32-45: Add documentation for static properties

The private static properties $individualId, $collectionCampAddress, and $fromAddress lack documentation. Consider adding PHPDoc comments to explain their purpose and usage within the class.

Example:

/**
 * @var int|null Caches the ID of the last created individual
 */
private static $individualId = NULL;

Line range hint 50-74: Consider splitting class responsibilities

The getSubscribedEvents method subscribes to a large number of hooks and events, indicating that the CollectionCampService class might be handling too many responsibilities. This could potentially violate the Single Responsibility Principle.

Consider refactoring this class into smaller, more focused classes, each handling a specific set of related events or functionalities. This would improve maintainability and make the code easier to understand and test.

Would you like assistance in identifying potential areas for splitting the class?


Line range hint 79-121: Consider extracting tab configuration

The collectionCampTabset method uses a hardcoded array for tab configuration. To improve maintainability and allow for easier future modifications, consider extracting this configuration to a separate method or even a configuration file.

This would make it easier to add, remove, or modify tabs without changing the method logic.

Example:

private static function getTabConfigs() {
    return [
        'logistics' => [
            'title' => ts('Logistics'),
            'module' => 'afsearchCollectionCampLogistics',
            'directive' => 'afsearch-collection-camp-logistics',
            'template' => 'CRM/Goonjcustom/Tabs/CollectionCamp.tpl',
        ],
        // ... other tab configurations ...
    ];
}

Line range hint 1-1141: Consider overall class structure and potential improvements

The CollectionCampService class is handling a wide variety of responsibilities related to collection camps. While the code is generally well-structured, there are several areas where improvements could be made:

  1. Single Responsibility Principle: The class is handling many different aspects of collection camps. Consider breaking it down into smaller, more focused classes (e.g., CollectionCampTabService, CollectionCampOfficeService, etc.).

  2. Method Length: Some methods, like setOfficeDetails and createActivityForCollectionCamp, are quite long. Consider breaking these down into smaller, more focused methods.

  3. Error Handling: Ensure consistent error handling throughout the class. Some methods use exceptions, while others use return values or logging.

  4. Configuration: Consider moving hardcoded values (like field names, relationship types, etc.) to a configuration file for easier maintenance.

  5. Dependency Injection: The class seems to have many dependencies. Consider using dependency injection to make the class more testable and reduce coupling.

  6. Comments and Documentation: While there are some comments, adding more detailed PHPDoc comments for methods and properties would improve code readability and maintainability.

Implementing these suggestions would significantly improve the overall quality, maintainability, and testability of the code.

Would you like assistance in planning out these refactoring steps?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 26b236d and e213e4b.

📒 Files selected for processing (3)
  • wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php (3 hunks)
  • wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php (4 hunks)
  • wp-content/civi-extensions/goonjcustom/Civi/Traits/SubtypeSource.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • wp-content/civi-extensions/goonjcustom/Civi/Traits/SubtypeSource.php
🧰 Additional context used
🔇 Additional comments (5)
wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php (2)

5-8: New use statements look good!

The added use statements for Contact, CustomField, and Relationship classes are appropriate for the new functionality introduced in this file.


34-34: New hook subscription looks good!

The addition of the '&hook_civicrm_custom' hook linked to the setOfficeDetails method is consistent with the new functionality introduced in the class.

wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php (3)

Line range hint 1-1141: Summary of CollectionCampService review

Overall, the CollectionCampService class provides comprehensive functionality for managing collection camps. However, there are several areas where the code could be improved:

  1. The class has too many responsibilities and could benefit from being split into smaller, more focused classes.
  2. Some methods are overly complex and long, making them difficult to maintain and test.
  3. There are opportunities to improve error handling and logging throughout the class.
  4. Consider extracting configuration values and using dependency injection to reduce coupling.
  5. Additional documentation and comments would improve code readability.

Implementing these suggestions would lead to a more maintainable, testable, and robust codebase. Despite these areas for improvement, the code appears to be functional and well-structured overall.


22-22: ⚠️ Potential issue

Remove duplicate use statement for SubtypeSource

The SubtypeSource trait is being imported twice:

  • Line 22: use Civi\Traits\SubtypeSource;
  • Line 30: use SubtypeSource;

This duplication could lead to confusion or namespace conflicts. Please remove the redundant import.

Apply this diff to resolve the duplication:

- use SubtypeSource;

Also applies to: 30-30


Line range hint 570-639: Refactor setOfficeDetails method for improved readability and maintainability

The setOfficeDetails method is handling multiple responsibilities, including:

  1. Checking operation type and subtype name
  2. Finding the state field
  3. Updating collection camp details
  4. Assigning coordinators

This complexity makes the method harder to understand, test, and maintain. Consider refactoring this method into smaller, focused methods to improve readability and adhere to the Single Responsibility Principle.

Additionally, the new exception thrown when no coordinator is available (throw new \Exception('No coordinator available to assign.');) should be handled appropriately by the calling code. Ensure that this exception is caught and handled gracefully to prevent unexpected crashes.

Would you like assistance in breaking down this method into smaller, more focused methods?

To verify the exception handling, we can use the following script:

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Outside diff range and nitpick comments (3)
wp-content/civi-extensions/goonjcustom/Civi/Traits/CollectionSource.php (1)

29-45: Ahoy, matey! This new method looks shipshape, but we're missing our treasure map!

Yarr, the getEntitySubtypeName method be a fine addition to our pirate code! It's as efficient as a well-oiled cannon and follows the Pirate Code of Conduct (also known as software development best practices). However, we be missing our treasure map (documentation) for this new booty.

Consider adding some documentation to help future pirates navigate these treacherous waters:

/**
 * Retrieves the subtype name for a given entity ID.
 *
 * @param int $entityID The ID of the entity to retrieve the subtype name for.
 * @return string|null The subtype name if found, or NULL if not found.
 */
public static function getEntitySubtypeName($entityID) {
    // ... existing code ...
}

This way, even the greenest of cabin boys can understand what this method does and how to use it!

wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php (2)

Line range hint 568-637: Consider refactoring setOfficeDetails for improved modularity

The setOfficeDetails method appears to handle multiple responsibilities, including:

  1. Checking operation type and entity subtype
  2. Finding and assigning a state office
  3. Finding and assigning a coordinator

Consider breaking this method down into smaller, more focused methods to improve readability and maintainability. This refactoring would align better with the Single Responsibility Principle.

Here's a suggested refactor outline:

private function setOfficeDetails($op, $groupID, $entityID, &$params) {
    if (!$this->shouldProcessOfficeDetails($op, $entityID)) {
        return false;
    }

    $stateId = $this->findStateId($params);
    if (!$stateId) {
        return false;
    }

    $this->assignStateOffice($stateId, $params);
    $this->assignCoordinator($stateId, $params);

    return true;
}

private function shouldProcessOfficeDetails($op, $entityID) {
    return $op === 'create' && self::getEntitySubtypeName($entityID) === self::ENTITY_SUBTYPE_NAME;
}

private function findStateId($params) {
    // Logic to find state ID
}

private function assignStateOffice($stateId, &$params) {
    // Logic to assign state office
}

private function assignCoordinator($stateId, &$params) {
    // Logic to assign coordinator
}

This refactoring would make the code more modular and easier to maintain.


Line range hint 1-1144: Consider breaking down CollectionCampService into smaller classes

The CollectionCampService class is quite large and seems to handle multiple responsibilities, including:

  • Office assignment
  • QR code generation
  • Email notifications
  • Activity creation
  • Logistics handling

This structure might violate the Single Responsibility Principle and could make the class harder to maintain and test.

Consider breaking this class down into smaller, more focused classes. For example:

  • OfficeAssignmentService
  • QRCodeService
  • NotificationService
  • ActivityService
  • LogisticsService

This refactoring would improve modularity, make the code easier to understand, and facilitate easier testing and maintenance.

Additionally, consider reducing the use of static methods, as they can make the code harder to test and maintain. Instead, you could use dependency injection to provide the necessary dependencies to each service.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between e213e4b and 6624c48.

📒 Files selected for processing (3)
  • wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php (2 hunks)
  • wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php (5 hunks)
  • wp-content/civi-extensions/goonjcustom/Civi/Traits/CollectionSource.php (1 hunks)
🧰 Additional context used
🔇 Additional comments (5)
wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php (4)

5-8: Imports and constants look good!

The new imports and constants enhance the functionality and readability of the code. Using constants for relationship type and fallback office name improves maintainability.

Also applies to: 21-23


32-32: New hook added correctly

The addition of the '&hook_civicrm_custom' hook for the 'setOfficeDetails' method is consistent with the existing pattern and extends the functionality of the class.


211-213: Good addition of error handling

The new error handling for the case when no coordinator is available is a good improvement. It enhances the robustness of the method and is consistent with best practices for error handling in PHP.


115-125: ⚠️ Potential issue

Add error handling to getFallbackOffice method

The method successfully retrieves the fallback office, but it doesn't handle the case where no fallback office is found. This could lead to issues in other parts of the code that depend on this method.

Consider adding error handling as follows:

   private static function getFallbackOffice() {
     $fallbackOffices = Contact::get(FALSE)
       ->addSelect('id')
       ->addWhere('organization_name', 'CONTAINS', self::FALLBACK_OFFICE_NAME)
       ->execute();

-    return $fallbackOffices->first();
+    $fallbackOffice = $fallbackOffices->first();
+    if (!$fallbackOffice) {
+      throw new \Exception("Fallback office '" . self::FALLBACK_OFFICE_NAME . "' not found.");
+    }
+    return $fallbackOffice;
   }

This change ensures that an exception is thrown if no fallback office is found, allowing for proper error handling in the calling code.

Likely invalid or redundant comment.

wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php (1)

634-637: Clarify exception handling strategy

An exception is thrown when no coordinator is available to assign, which is a good practice for error handling. However, it's not clear from this context how this exception is being caught and handled.

To ensure proper error handling, let's verify how this exception is being caught:

Consider adding a comment explaining the exception handling strategy or ensure that this exception is properly caught and handled at a higher level in the code.

Comment on lines +83 to +113
/**
*
*/
private static function findStateField(array $array) {
$filteredItems = array_filter($array, fn($item) => $item['entity_table'] === 'civicrm_eck_collection_camp');

if (empty($filteredItems)) {
return FALSE;
}

$collectionCampStateFields = CustomField::get(FALSE)
->addSelect('id')
->addWhere('name', '=', 'State')
->addWhere('custom_group_id:name', '=', 'Dropping_Centre')
->execute()
->first();

if (!$collectionCampStateFields) {
return FALSE;
}

$stateFieldId = $collectionCampStateFields['id'];

$stateItemIndex = array_search(TRUE, array_map(fn($item) =>
$item['entity_table'] === 'civicrm_eck_collection_camp' &&
$item['custom_field_id'] == $stateFieldId,
$filteredItems
));

return $stateItemIndex !== FALSE ? $filteredItems[$stateItemIndex] : FALSE;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Simplify findStateField method for better readability and performance

The current implementation of findStateField is overly complex. We can simplify it to improve both readability and performance.

Consider refactoring the method as follows:

private static function findStateField(array $array) {
    $stateFieldId = CustomField::get(FALSE)
        ->addSelect('id')
        ->addWhere('name', '=', 'State')
        ->addWhere('custom_group_id:name', '=', 'Dropping_Centre')
        ->execute()
        ->first()['id'] ?? null;

    if (!$stateFieldId) {
        return FALSE;
    }

    foreach ($array as $item) {
        if ($item['entity_table'] === 'civicrm_eck_collection_camp' && 
            $item['custom_field_id'] == $stateFieldId) {
            return $item;
        }
    }

    return FALSE;
}

This refactoring:

  1. Retrieves the state field ID first, returning early if not found.
  2. Uses a simple loop to find the matching item, which is more readable and potentially more performant than the current array operations.
  3. Eliminates the need for multiple array operations, simplifying the logic.

Comment on lines +142 to +224
/**
*
*/
public static function setOfficeDetails($op, $groupID, $entityID, &$params) {
if ($op !== 'create' || self::getEntitySubtypeName($entityID) !== self::ENTITY_SUBTYPE_NAME) {
return;
}

if (!($stateField = self::findStateField($params))) {
return;
}

$stateId = $stateField['value'];

$droppingCenterId = $stateField['entity_id'];

$collectionCamp = EckEntity::get('Collection_Camp', FALSE)
->addSelect('Dropping_Centre.Will_your_dropping_center_be_open_for_general_public_as_well_out')
->addWhere('id', '=', $droppingCenterId)
->execute();

$collectionCampData = $collectionCamp->first();

if (!$stateId) {
\CRM_Core_Error::debug_log_message('Cannot assign Goonj Office to dropping center: ' . $collectionCamp['id']);
\CRM_Core_Error::debug_log_message('No state provided on the intent for dropping center: ' . $collectionCamp['id']);
return FALSE;
}

$officesFound = Contact::get(FALSE)
->addSelect('id')
->addWhere('contact_type', '=', 'Organization')
->addWhere('contact_sub_type', 'CONTAINS', 'Goonj_Office')
->addWhere('Goonj_Office_Details.Collection_Camp_Catchment', 'CONTAINS', $stateId)
->execute();

$stateOffice = $officesFound->first();

// If no state office is found, assign the fallback state office.
if (!$stateOffice) {
$stateOffice = self::getFallbackOffice();
}

$stateOfficeId = $stateOffice['id'];

EckEntity::update('Collection_Camp', FALSE)
->addValue('Dropping_Centre.Goonj_Office', $stateOfficeId)
->addWhere('id', '=', $droppingCenterId)
->execute();

$coordinators = Relationship::get(FALSE)
->addWhere('contact_id_b', '=', $stateOfficeId)
->addWhere('relationship_type_id:name', '=', self::RELATIONSHIP_TYPE_NAME)
->addWhere('is_current', '=', TRUE)
->execute();

$coordinatorCount = $coordinators->count();

if ($coordinatorCount === 0) {
$coordinator = self::getFallbackCoordinator();
}
elseif ($coordinatorCount > 1) {
$randomIndex = rand(0, $coordinatorCount - 1);
$coordinator = $coordinators->itemAt($randomIndex);
}
else {
$coordinator = $coordinators->first();
}

if (!$coordinator) {
throw new \Exception('No coordinator available to assign.');
}

$coordinatorId = $coordinator['contact_id_a'];

EckEntity::update('Collection_Camp', FALSE)
->addValue('Dropping_Centre.Coordinating_Urban_POC', $coordinatorId)
->addWhere('id', '=', $droppingCenterId)
->execute();

return TRUE;

}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Refactor setOfficeDetails for better maintainability and consistency

The setOfficeDetails method is handling multiple responsibilities and is quite long. This violates the Single Responsibility Principle and makes the code harder to maintain and test.

Consider breaking down this method into smaller, more focused methods:

  1. Split the method into smaller functions, each handling a specific task (e.g., findOffice, assignOffice, findCoordinator, assignCoordinator).
  2. Replace error logging with exceptions for consistent error handling.
  3. Use the previously suggested selectRandomCoordinator method to reduce code duplication.

Here's a high-level example of how you might refactor this:

public static function setOfficeDetails($op, $groupID, $entityID, &$params) {
    if ($op !== 'create' || self::getEntitySubtypeName($entityID) !== self::ENTITY_SUBTYPE_NAME) {
        return;
    }

    $stateField = self::findStateField($params);
    if (!$stateField) {
        throw new \Exception("State field not found for dropping center: {$entityID}");
    }

    $stateId = $stateField['value'];
    $droppingCenterId = $stateField['entity_id'];

    $office = self::findOffice($stateId);
    self::assignOffice($droppingCenterId, $office['id']);

    $coordinator = self::findCoordinator($office['id']);
    self::assignCoordinator($droppingCenterId, $coordinator['contact_id_a']);

    return TRUE;
}

private static function findOffice($stateId) {
    // Logic to find office based on state
}

private static function assignOffice($droppingCenterId, $officeId) {
    // Logic to assign office to dropping center
}

private static function findCoordinator($officeId) {
    // Logic to find coordinator for office
}

private static function assignCoordinator($droppingCenterId, $coordinatorId) {
    // Logic to assign coordinator to dropping center
}

This refactoring improves the overall structure of the code, making it more maintainable and easier to test individual components.

Also, ensure consistent error handling throughout the code. Replace error logging with exceptions:

-      \CRM_Core_Error::debug_log_message('Cannot assign Goonj Office to dropping center: ' . $collectionCamp['id']);
-      \CRM_Core_Error::debug_log_message('No state provided on the intent for dropping center: ' . $collectionCamp['id']);
-      return FALSE;
+      throw new \Exception("Cannot assign Goonj Office to dropping center: {$droppingCenterId}. No state provided.");

Comment on lines +156 to +160
$droppingCenterId = $stateField['entity_id'];

$collectionCamp = EckEntity::get('Collection_Camp', FALSE)
->addSelect('Dropping_Centre.Will_your_dropping_center_be_open_for_general_public_as_well_out')
->addWhere('id', '=', $droppingCenterId)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure consistency between variable names and entity names

The variable name has been changed from 'collectionCamp' to 'droppingCenter', which aligns better with the class name and purpose. However, the entity name in the API call remains 'Collection_Camp'. This inconsistency could lead to confusion.

Consider one of the following options:

  1. Change the entity name in the API call to match the new variable name:
-    $collectionCamp = EckEntity::get('Collection_Camp', FALSE)
+    $droppingCenter = EckEntity::get('Dropping_Center', FALSE)
  1. If 'Collection_Camp' is the correct entity name, revert the variable name change:
-    $droppingCenterId = $stateField['entity_id'];
+    $collectionCampId = $stateField['entity_id'];

-    $collectionCamp = EckEntity::get('Collection_Camp', FALSE)
+    $collectionCamp = EckEntity::get('Collection_Camp', FALSE)

Choose the option that best reflects the intended entity and maintains consistency throughout the codebase.

Committable suggestion was skipped due to low confidence.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Outside diff range and nitpick comments (1)
wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php (1)

Line range hint 568-637: Improved method specificity and error handling, but consider refactoring

The changes to the setOfficeDetails method have improved its specificity by adding a check for the entity subtype, and enhanced error handling when no coordinator is available. These are positive improvements.

However, this method is quite long and handles multiple responsibilities:

  1. Checking operation type and subtype name
  2. Finding the state field
  3. Updating collection camp details
  4. Assigning coordinators

Consider refactoring this method into smaller, focused methods to improve readability and maintainability. Each method should handle a single responsibility.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 6624c48 and bf24490.

📒 Files selected for processing (2)
  • wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php (3 hunks)
  • wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php (5 hunks)
🧰 Additional context used
🔇 Additional comments (6)
wp-content/civi-extensions/goonjcustom/Civi/DroppingCenterService.php (4)

5-8: LGTM: New use statements are properly organized.

The added use statements for Contact, CustomField, and Relationship classes are necessary for the new functionality and are well-organized.


21-23: LGTM: New constants improve code maintainability.

The addition of RELATIONSHIP_TYPE_NAME and FALLBACK_OFFICE_NAME constants enhances code readability and maintainability. The naming convention is consistent with existing constants.


32-32: LGTM: New hook subscription added.

The new subscription for 'hook_civicrm_custom' is consistent with existing ones and correctly links to the new setOfficeDetails method.


149-153: ⚠️ Potential issue

Ensure consistency between variable names and entity names.

The variable name has been changed from 'collectionCamp' to 'droppingCenter', which aligns better with the class name and purpose. However, the entity name in the API call remains 'Collection_Camp'. This inconsistency could lead to confusion.

Consider one of the following options:

  1. Change the entity name in the API call to match the new variable name:
-    $collectionCamp = EckEntity::get('Collection_Camp', FALSE)
+    $droppingCenter = EckEntity::get('Dropping_Center', FALSE)
  1. If 'Collection_Camp' is the correct entity name, revert the variable name change:
-    $droppingCenterId = $stateField['entity_id'];
+    $collectionCampId = $stateField['entity_id'];

-    $collectionCamp = EckEntity::get('Collection_Camp', FALSE)
+    $collectionCamp = EckEntity::get('Collection_Camp', FALSE)

Choose the option that best reflects the intended entity and maintains consistency throughout the codebase.

Likely invalid or redundant comment.

wp-content/civi-extensions/goonjcustom/Civi/CollectionCampService.php (2)

707-727: Excellent refactoring of findStateField method

The refactoring of the findStateField method is a significant improvement:

  1. The new implementation is more concise and easier to understand.
  2. It reduces the complexity of the previous implementation by using a direct approach to find the state field.
  3. The method now returns early if the custom field is not found, which is a good practice for error handling and code readability.

These changes enhance the maintainability and performance of the code. Well done!


Line range hint 1-1183: Overall improvements and approval of changes

The changes made to this file have significantly improved its quality:

  1. The setOfficeDetails method now has better specificity and error handling.
  2. The findStateField method has been refactored for improved readability and efficiency.
  3. Previous review comments have been addressed or are no longer applicable due to the changes.

These improvements enhance the overall maintainability and robustness of the code. While there's always room for further refactoring (as mentioned in the previous comments), the current changes are a step in the right direction.

Comment on lines +83 to +106
/**
*
*/
private static function findStateField(array $array) {
$stateFieldId = CustomField::get(FALSE)
->addSelect('id')
->addWhere('name', '=', 'State')
->addWhere('custom_group_id:name', '=', 'Dropping_Centre')
->execute()
->first()['id'] ?? NULL;

if (!$stateFieldId) {
return FALSE;
}

foreach ($array as $item) {
if ($item['entity_table'] === 'civicrm_eck_collection_camp' &&
$item['custom_field_id'] === $stateFieldId) {
return $item;
}
}

return FALSE;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Simplify findStateField method for better readability and performance.

The current implementation of findStateField is overly complex. We can simplify it to improve both readability and performance.

Consider refactoring the method as follows:

private static function findStateField(array $array) {
    $stateFieldId = CustomField::get(FALSE)
        ->addSelect('id')
        ->addWhere('name', '=', 'State')
        ->addWhere('custom_group_id:name', '=', 'Dropping_Centre')
        ->execute()
        ->first()['id'] ?? null;

    if (!$stateFieldId) {
        return FALSE;
    }

    foreach ($array as $item) {
        if ($item['entity_table'] === 'civicrm_eck_collection_camp' && 
            $item['custom_field_id'] == $stateFieldId) {
            return $item;
        }
    }

    return FALSE;
}

This refactoring:

  1. Retrieves the state field ID first, returning early if not found.
  2. Uses a simple loop to find the matching item, which is more readable and potentially more performant than the current array operations.
  3. Eliminates the need for multiple array operations, simplifying the logic.

Comment on lines +108 to +118
/**
*
*/
private static function getFallbackOffice() {
$fallbackOffices = Contact::get(FALSE)
->addSelect('id')
->addWhere('organization_name', 'CONTAINS', self::FALLBACK_OFFICE_NAME)
->execute();

return $fallbackOffices->first();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling to getFallbackOffice method.

The method successfully retrieves the fallback office, but it doesn't handle the case where no fallback office is found. This could lead to issues in other parts of the code that depend on this method.

Consider adding error handling as follows:

   private static function getFallbackOffice() {
     $fallbackOffices = Contact::get(FALSE)
       ->addSelect('id')
       ->addWhere('organization_name', 'CONTAINS', self::FALLBACK_OFFICE_NAME)
       ->execute();

-    return $fallbackOffices->first();
+    $fallbackOffice = $fallbackOffices->first();
+    if (!$fallbackOffice) {
+      throw new \Exception("Fallback office '" . self::FALLBACK_OFFICE_NAME . "' not found.");
+    }
+    return $fallbackOffice;
   }

This change ensures that an exception is thrown if no fallback office is found, allowing for proper error handling in the calling code.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
*
*/
private static function getFallbackOffice() {
$fallbackOffices = Contact::get(FALSE)
->addSelect('id')
->addWhere('organization_name', 'CONTAINS', self::FALLBACK_OFFICE_NAME)
->execute();
return $fallbackOffices->first();
}
/**
*
*/
private static function getFallbackOffice() {
$fallbackOffices = Contact::get(FALSE)
->addSelect('id')
->addWhere('organization_name', 'CONTAINS', self::FALLBACK_OFFICE_NAME)
->execute();
$fallbackOffice = $fallbackOffices->first();
if (!$fallbackOffice) {
throw new \Exception("Fallback office '" . self::FALLBACK_OFFICE_NAME . "' not found.");
}
return $fallbackOffice;
}

Comment on lines +64 to +81
/**
*
*/
private static function getFallbackCoordinator() {
$fallbackOffice = self::getFallbackOffice();
$fallbackCoordinators = Relationship::get(FALSE)
->addWhere('contact_id_b', '=', $fallbackOffice['id'])
->addWhere('relationship_type_id:name', '=', self::RELATIONSHIP_TYPE_NAME)
->addWhere('is_current', '=', FALSE)
->execute();

$coordinatorCount = $fallbackCoordinators->count();

$randomIndex = rand(0, $coordinatorCount - 1);
$coordinator = $fallbackCoordinators->itemAt($randomIndex);

return $coordinator;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve error handling and check 'is_current' condition.

While the method logic for retrieving a random coordinator is good, there are two issues that need attention:

  1. The is_current = FALSE condition in the relationship query might be incorrect. Typically, you'd want to retrieve current relationships.
  2. There's no error handling if no coordinators are found, which could lead to an undefined index error.

Consider the following improvements:

  1. Change the is_current condition:
-      ->addWhere('is_current', '=', FALSE)
+      ->addWhere('is_current', '=', TRUE)
  1. Add error handling:
     $coordinatorCount = $fallbackCoordinators->count();
+    if ($coordinatorCount === 0) {
+      throw new \Exception('No fallback coordinators found.');
+    }
     $randomIndex = rand(0, $coordinatorCount - 1);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
*
*/
private static function getFallbackCoordinator() {
$fallbackOffice = self::getFallbackOffice();
$fallbackCoordinators = Relationship::get(FALSE)
->addWhere('contact_id_b', '=', $fallbackOffice['id'])
->addWhere('relationship_type_id:name', '=', self::RELATIONSHIP_TYPE_NAME)
->addWhere('is_current', '=', FALSE)
->execute();
$coordinatorCount = $fallbackCoordinators->count();
$randomIndex = rand(0, $coordinatorCount - 1);
$coordinator = $fallbackCoordinators->itemAt($randomIndex);
return $coordinator;
}
/**
*
*/
private static function getFallbackCoordinator() {
$fallbackOffice = self::getFallbackOffice();
$fallbackCoordinators = Relationship::get(FALSE)
->addWhere('contact_id_b', '=', $fallbackOffice['id'])
->addWhere('relationship_type_id:name', '=', self::RELATIONSHIP_TYPE_NAME)
->addWhere('is_current', '=', TRUE)
->execute();
$coordinatorCount = $fallbackCoordinators->count();
if ($coordinatorCount === 0) {
throw new \Exception('No fallback coordinators found.');
}
$randomIndex = rand(0, $coordinatorCount - 1);
$coordinator = $fallbackCoordinators->itemAt($randomIndex);
return $coordinator;
}


$collectionCampId = $stateField['entity_id'];

$collectionCamp = EckEntity::get('Collection_Camp', FALSE)
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.

My bad. It should be

Suggested change
$collectionCamp = EckEntity::get('Collection_Camp', FALSE)
$droppingCenter = EckEntity::get('Collection_Camp', FALSE)

Comment on lines +154 to +156
->execute();

$collectionCampData = $collectionCamp->first();
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.

Suggested change
->execute();
$collectionCampData = $collectionCamp->first();
->execute()->single();

@belwalshubham belwalshubham merged commit 8bc2480 into develop Oct 14, 2024
@belwalshubham belwalshubham deleted the feat/assign-goonj-office-by-state branch October 14, 2024 10:14
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.

2 participants