Skip to content

Commit

Permalink
Version 1.4.0
Browse files Browse the repository at this point in the history
  • Loading branch information
agileware-fj authored and agileware-dev committed Apr 9, 2024
2 parents d309a2d + 5fde13b commit 9bfa57a
Show file tree
Hide file tree
Showing 17 changed files with 880 additions and 167 deletions.
145 changes: 145 additions & 0 deletions CRM/Membershiputils/ProcessHooks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

use Civi\Api4\PriceFieldValue;
use Civi\Api4\SavedSearch;
use Civi\Api4\SearchDisplay;
use Civi\Core\Event\GenericHookEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

use CRM_Membershiputils_ExtensionUtil as E;

class CRM_Membershiputils_ProcessHooks implements EventSubscriberInterface {

const comparisonSearch = 'Excluded_from_Renewal';

protected CRM_Core_Form $form;

protected array $type_ids;

public static function getSubscribedEvents(): array {
return [
'hook_civicrm_buildForm' => 'buildForm',
];
}

protected function hasContactRenewed() {
$contact = $this->form->getContactID();

$contacts_changed = SearchDisplay::run(FALSE)
->setSavedSearch(self::comparisonSearch)
->setFilters([
'contact_id' => $contact,
'membership_type_id' => $this->type_ids,
])
->execute();

return $contacts_changed->count()
? ($contacts_changed->first()['data']['membership_type_id:label'] ?? TRUE)
: FALSE;
}

protected function hasMembershipPriceFields(): bool {
$priceSetID = $this->form->getPriceSetID() ?: 0;

$this->type_ids = array_unique(array_map(
fn($pfv) => $pfv['membership_type_id'],
PriceFieldValue::get(FALSE)
->addSelect('membership_type_id')
->addWhere('price_field_id.price_set_id', '=', $priceSetID)
->addWhere('membership_type_id', 'IS NOT EMPTY')
->execute()
->getArrayCopy()
));

return count($this->type_ids) > 0;
}

public function validateForm(GenericHookEvent $event): void {}

public function buildForm(GenericHookEvent $event): void {
$this->form = &$event->form;

if ($this->form instanceof CRM_Contribute_Form_Contribution_Main
&& Civi::settings()->get('membershiputils_prevent_double_renewal')) {
$this->buildForm_Contribute_Form_Contribution_main($event);
}
elseif ($this->form instanceof CRM_Admin_Form_Generic) {
$this->buildForm_Admin_Form_Generic($event);
}
}

public function buildForm_Contribute_Form_Contribution_main(GenericHookEvent $event): void {
if (!$this->hasMembershipPriceFields()) {
return;
}

$type = $this->hasContactRenewed();

if (!$type) {
return;
}

if (is_array($type)) {
$type = $type[0];
}

throw new CRM_Core_Exception(E::ts(
'A renewal has already been submitted for your %1 Membership',
[1 => is_string($type) ? "<em>$type</em>" : '']
));
}

public function buildForm_Admin_Form_Generic(GenericHookEvent $event): void {
if (!$this->form->elementExists('membershiputils_prevent_double_renewal')) {
return;
}

$field_prevent_double_renewal = $this->form->getElement('membershiputils_prevent_double_renewal');

$field_prevent_double_renewal->setComment(
E::ts('When enabled, prevents Contribution Pages from loading that include Membership renewal if the using contact has a membership of one of the included types that has already been renewed. You can customise how it is determined that memberships have already been used by changing the parameters of the <a href="%1">Saved Search, "%2"</a>',
$this->search_admin_params())
);
}

private function search_admin_params() {
try {
$excluded_from_renewal_search = SavedSearch::get(FALSE)
->addWhere('name', '=', 'Excluded_from_Renewal')
->addSelect('id', 'label')
->execute();

if ($excluded_from_renewal_search->count()) {
$excluded_from_renewal_search = $excluded_from_renewal_search->first();
return [
1 => CRM_Utils_System::url(
'civicrm/admin/search',
'',
FALSE,
'/edit/' . $excluded_from_renewal_search['id'],
TRUE,
FALSE,
TRUE
),
2 => $excluded_from_renewal_search['label'],
];
}
}
catch (Exception $e) {
}

return [
1 => CRM_Utils_System::url(
'civicrm/admin/search',
'',
FALSE,
'/list?tab=packaged',
TRUE,
FALSE,
TRUE
),
2 => E::ts('Excluded from Renewal'),
];
}

}
43 changes: 43 additions & 0 deletions CRM/Membershiputils/ResourceHooks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

use CRM_Membershiputils_ExtensionUtil as E;

use Civi\Core\Event\GenericHookEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class CRM_Membershiputils_ResourceHooks implements EventSubscriberInterface {

/**
* Returns an array of events this subscriber wants to listen to.
*
* @return array
*/
public static function getSubscribedEvents() {
return [
'hook_civicrm_pageRun' => 'pageRun',
'hook_civicrm_buildForm' => 'buildForm',
];
}

private function addTypeChangeResources() {
Civi::resources()->addVars('membershipUtils', [
'typeChangeMessage' => Civi::settings()->get('membershiputils_type_change_message'),
'typeChangeNotification' => (bool) Civi::settings()->get('membershiputils_type_change_notification'),
]);

Civi::resources()->addScriptFile(E::LONG_NAME, 'js/typechange.js');
}

public function pageRun(GenericHookEvent $event) {
if ($event->page instanceof CRM_Contribute_Page_ContributionPage) {
$this->addTypeChangeResources();
}
}

public function buildForm(GenericHookEvent $event) {
if ($event->form instanceof CRM_Contribute_Form_Contribution_Main) {
$this->addTypeChangeResources();
}
}

}
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ This feature can be enabled or disabled on the `CiviCRM > Administer > Membershi

If you have existing memberships that need to be updated, then execute the Scheduled Job, `Set specific Membership End Date` (API: membershiputils.Specificmembershipenddate). This will update the memberships with a status of either: **New**, **Current** and **Grace**, setting the end date to the date specified in the settings.

## Notification of Membership Type change

The user can be notified when renewing their Membership if the price option they selected will alter the type of their existing membership.

This feature can be enabled or disabled and the message shown to members can be customised on the `CiviCRM > Administer > Membership Utilities Settings` page, `/wp-admin/admin.php?page=CiviCRM&q=civicrm%2Fadmin%2Fsetting%2Fmembershiputils`.

## Prevent Duplicate Renewal submission

Prevent Contribution pages from being loaded if a member attempts to use one to renew a membership that is not due for renewal or already has a Pending renewal payment.

This feature can be enabled or disabled on the `CiviCRM > Administer > Membership Utilities Settings` page, `/wp-admin/admin.php?page=CiviCRM&q=civicrm%2Fadmin%2Fsetting%2Fmembershiputils`.

Whether a membership is not due for renewal can be customised by changing the filters on the packaged Searchkit Saved Search "Excluded from Renewal" to find members who should not be able to renew.
By default, this search will exclude members whose membership expiry is more than 30 days in the future, or who have a related pending contribution that is less than 30 days old.

# Installation

1. Install and enable this CiviCRM extension like any normal CiviCRM extension.
Expand Down
7 changes: 7 additions & 0 deletions ang/afsearchMembersWhoHaveChangedTheirMembershipType.aff.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div af-fieldset="" af-title="Members who have changed their membership type">
<details class="af-container" af-title="Filters" open="">
<af-field name="Contact_ActivityContact_Activity_01.activity_date_time" defn="{input_type: 'Select', search_range: true, default_date_type: 'fixed', input_attrs: {}, afform_default: 'ending_60.day', options: [{id: '{}', label: 'Choose Date Range'}, {id: 'this.day', label: 'Today'}, {id: 'this.week', label: 'This week'}, {id: 'this.month', label: 'This calendar month'}, {id: 'this.quarter', label: 'This quarter'}, {id: 'this.fiscal_year', label: 'This fiscal year'}, {id: 'this.year', label: 'This calendar year'}, {id: 'previous.day', label: 'Yesterday'}, {id: 'previous.week', label: 'Previous week'}, {id: 'previous.month', label: 'Previous calendar month'}, {id: 'previous.quarter', label: 'Previous quarter'}, {id: 'previous.fiscal_year', label: 'Previous fiscal year'}, {id: 'previous.year', label: 'Previous calendar year'}, {id: 'ending.week', label: 'Last 7 days including today'}, {id: 'ending_30.day', label: 'Last 30 days including today'}, {id: 'ending_60.day', label: 'Last 60 days including today'}, {id: 'ending_90.day', label: 'Last 90 days including today'}, {id: 'ending.year', label: 'Last 12 months including today'}, {id: 'ending_2.year', label: 'Last 2 years including today'}, {id: 'ending_3.year', label: 'Last 3 years including today'}, {id: 'starting.day', label: 'Tomorrow'}, {id: 'next.week', label: 'Next week'}, {id: 'next.month', label: 'Next calendar month'}, {id: 'next.quarter', label: 'Next quarter'}, {id: 'next.fiscal_year', label: 'Next fiscal year'}, {id: 'next.year', label: 'Next calendar year'}, {id: 'starting.week', label: 'Next 7 days including today'}, {id: 'starting.month', label: 'Next 30 days including today'}, {id: 'starting_2.month', label: 'Next 60 days including today'}, {id: 'starting.quarter', label: 'Next 90 days including today'}, {id: 'starting.year', label: 'Next 12 months including today'}, {id: 'current.week', label: 'Current week to-date'}, {id: 'current.month', label: 'Current calendar month to-date'}, {id: 'current.quarter', label: 'Current quarter to-date'}, {id: 'current.year', label: 'Current calendar year to-date'}, {id: 'earlier.day', label: 'To end of yesterday'}, {id: 'earlier.week', label: 'To end of previous week'}, {id: 'earlier.month', label: 'To end of previous calendar month'}, {id: 'earlier.quarter', label: 'To end of previous quarter'}, {id: 'earlier.year', label: 'To end of previous calendar year'}, {id: 'greater.day', label: 'From start of current day'}, {id: 'greater.week', label: 'From start of current week'}, {id: 'greater.month', label: 'From start of current calendar month'}, {id: 'greater.quarter', label: 'From start of current quarter'}, {id: 'greater.year', label: 'From start of current calendar year'}, {id: 'less.week', label: 'To end of current week'}, {id: 'less.month', label: 'To end of current calendar month'}, {id: 'less.quarter', label: 'To end of current quarter'}, {id: 'less.year', label: 'To end of current calendar year'}, {id: 'previous_2.day', label: 'Previous 2 days'}, {id: 'previous_2.week', label: 'Previous 2 weeks'}, {id: 'previous_2.month', label: 'Previous 2 calendar months'}, {id: 'previous_2.quarter', label: 'Previous 2 quarters'}, {id: 'previous_2.year', label: 'Previous 2 calendar years'}, {id: 'previous_before.day', label: 'Day prior to yesterday'}, {id: 'previous_before.week', label: 'Week prior to previous week'}, {id: 'previous_before.month', label: 'Month prior to previous calendar month'}, {id: 'previous_before.quarter', label: 'Quarter prior to previous quarter'}, {id: 'previous_before.year', label: 'Year prior to previous calendar year'}, {id: 'greater_previous.week', label: 'From end of previous week'}, {id: 'greater_previous.month', label: 'From end of previous calendar month'}, {id: 'greater_previous.quarter', label: 'From end of previous quarter'}, {id: 'greater_previous.year', label: 'From end of previous calendar year'}, {id: 'previous_2.fiscal_year', label: 'Previous 2 fiscal years'}, {id: 'previous_before.fiscal_year', label: 'Fiscal year prior to previous fiscal year'}]}" />
<button class="af-button btn btn-warning" type="reset" crm-icon="fa-undo">Reset</button>
</details>
<crm-search-display-table search-name="Members_that_changed_type" display-name="Members_who_have_changed_type_Table_1"></crm-search-display-table>
</div>
15 changes: 15 additions & 0 deletions ang/afsearchMembersWhoHaveChangedTheirMembershipType.aff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php
use CRM_Membershiputils_ExtensionUtil as E;

return [
'type' => 'search',
'title' => E::ts('Members who have changed their membership type'),
'icon' => 'fa-list-alt',
'server_route' => 'civicrm/member/type-changed-report',
'permission' => [
'access CiviMember',
],
'search_displays' => [
'Members_that_changed_type.Members_who_have_changed_type_Table_1',
],
];
48 changes: 24 additions & 24 deletions api/v3/Membershiputils/Adjustmembershipenddate.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,32 +19,32 @@
*/
function civicrm_api3_membershiputils_Adjustmembershipenddate($params): array {
try {
// If this option is enabled then action, otherwise skip
if ( Civi::settings()->get( 'adjust_membership_end_date' ) ) {
// Get all memberships
$memberships = Membership::get()
->addSelect( 'id', 'end_date' )
->addWhere( 'status_id:name', 'IN', [
'New',
'Current',
'Grace'
] )
->execute()->getArrayCopy();
foreach ( $memberships as $membership ) {
// Calculate the end of month date for the membership
$end_date = date_create( $membership['end_date'] );
$start_month = date_create( $end_date->format( 'Y-m' ) . '-01' );
$new_end_date = date_modify( $start_month, '+1 month -1 day' );
// If this option is enabled then action, otherwise skip
if (Civi::settings()->get('adjust_membership_end_date')) {
// Get all memberships
$memberships = Membership::get()
->addSelect('id', 'end_date')
->addWhere('status_id:name', 'IN', [
'New',
'Current',
'Grace',
])
->execute()->getArrayCopy();
foreach ($memberships as $membership) {
// Calculate the end of month date for the membership
$end_date = date_create($membership['end_date']);
$start_month = date_create($end_date->format('Y-m') . '-01');
$new_end_date = date_modify($start_month, '+1 month -1 day');

// Update the membership
Membership::update()
->addValue( 'end_date', $new_end_date->format( 'Y-m-d' ) )
->addWhere( 'id', '=', $membership['id'] )
->execute();
}
// Update the membership
Membership::update()
->addValue('end_date', $new_end_date->format('Y-m-d'))
->addWhere('id', '=', $membership['id'])
->execute();
}

return civicrm_api3_create_success( TRUE, $params, 'membershiputils', 'Adjustmembershipenddate' );
}
return civicrm_api3_create_success(TRUE, $params, 'membershiputils', 'Adjustmembershipenddate');
}
}
catch (Exception $e) {
throw new CRM_Core_Exception('Error updating adjusting membership end date. Error: ' . $e->getMessage());
Expand Down
16 changes: 8 additions & 8 deletions api/v3/Membershiputils/Findduplicatememberships.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@
*/
function civicrm_api3_membershiputils_Findduplicatememberships(): array {
/* Example query to locate duplicate memberships
SELECT `duplicate`.*, `current`.`id` `current_id`, `current`.`status_id` `current_status_id`, `current`.`join_date` `current_join_date`, `current`.`start_date` `current_start_date`, `current`.`end_date` `current_end_date`
FROM civicrm_membership `duplicate` INNER JOIN civicrm_membership `current`
ON `duplicate`.`id` <> `current`.`id` AND `duplicate`.`membership_type_id` = `current`.`membership_type_id` and `duplicate`.`contact_id` = `current`.`contact_id`
WHERE `duplicate`.`end_date` < `current`.`end_date` and `duplicate`.`status_id` = [Current, Grace or Expired]
*/
SELECT `duplicate`.*, `current`.`id` `current_id`, `current`.`status_id` `current_status_id`, `current`.`join_date` `current_join_date`, `current`.`start_date` `current_start_date`, `current`.`end_date` `current_end_date`
FROM civicrm_membership `duplicate` INNER JOIN civicrm_membership `current`
ON `duplicate`.`id` <> `current`.`id` AND `duplicate`.`membership_type_id` = `current`.`membership_type_id` and `duplicate`.`contact_id` = `current`.`contact_id`
WHERE `duplicate`.`end_date` < `current`.`end_date` and `duplicate`.`status_id` = [Current, Grace or Expired]
*/

// Mark any memberships which are Current, Expired or Grace and have an End Date less than a Current membership end date as a Duplicate

$duplicateStatusId = array_search('Duplicate', CRM_Member_PseudoConstant::membershipStatus());
$currentStatusId = array_search('Current', CRM_Member_PseudoConstant::membershipStatus());
$expiredStatusId = array_search('Expired', CRM_Member_PseudoConstant::membershipStatus());
$graceStatusId = array_search('Grace', CRM_Member_PseudoConstant::membershipStatus());
$currentStatusId = array_search('Current', CRM_Member_PseudoConstant::membershipStatus());
$expiredStatusId = array_search('Expired', CRM_Member_PseudoConstant::membershipStatus());
$graceStatusId = array_search('Grace', CRM_Member_PseudoConstant::membershipStatus());

try {
CRM_Core_DAO::executeQuery("UPDATE civicrm_membership as `duplicate` INNER JOIN civicrm_membership as `current` ON `duplicate`.`id` <> `current`.`id` AND `duplicate`.`membership_type_id`=`current`.`membership_type_id` AND `duplicate`.`contact_id`=`current`.`contact_id` SET `duplicate`.`status_id`=%1, `duplicate`.`is_override`=1 WHERE `duplicate`.`end_date` < `current`.`end_date` AND `duplicate`.`status_id` IN (%2,%3,%4) AND `duplicate`.`owner_membership_id` IS NULL", [
Expand Down
45 changes: 23 additions & 22 deletions api/v3/Membershiputils/Specificmembershipenddate.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,31 @@
*/
function civicrm_api3_membershiputils_Specificmembershipenddate($params): array {
try {
// If this option is enabled then action, otherwise skip
if ( Civi::settings()->get( 'use_specific_membership_end_date' ) ) {
// Get all memberships
$memberships = Membership::get()
->addSelect( 'id', 'end_date' )
->addWhere( 'status_id:name', 'IN', [
'New',
'Current',
'Grace'
] )
->execute()->getArrayCopy();
foreach ( $memberships as $membership ) {
// Set a specific end date
$new_end_date = date_create_from_format('Y-m-d', Civi::settings()->get('specific_membership_end_date'));
// If this option is enabled then action, otherwise skip
if (Civi::settings()->get('use_specific_membership_end_date')) {
// Get all memberships
$memberships = Membership::get()
->addSelect('id', 'end_date')
->addWhere('status_id:name', 'IN', [
'New',
'Current',
'Grace',
])
->execute()->getArrayCopy();
foreach ($memberships as $membership) {
// Set a specific end date
$new_end_date = date_create_from_format('Y-m-d', Civi::settings()
->get('specific_membership_end_date'));

// Update the membership
Membership::update()
->addValue( 'end_date', $new_end_date->format( 'Y-m-d' ) )
->addWhere( 'id', '=', $membership['id'] )
->execute();
}
// Update the membership
Membership::update()
->addValue('end_date', $new_end_date->format('Y-m-d'))
->addWhere('id', '=', $membership['id'])
->execute();
}

return civicrm_api3_create_success( TRUE, $params, 'membershiputils', 'Specificmembershipenddate' );
}
return civicrm_api3_create_success(TRUE, $params, 'membershiputils', 'Specificmembershipenddate');
}
}
catch (Exception $e) {
throw new CRM_Core_Exception('Error setting specific membership end date. Error: ' . $e->getMessage());
Expand Down
5 changes: 3 additions & 2 deletions info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
<url desc="Support">https://agileware.com.au/contact</url>
<url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
</urls>
<releaseDate>2024-01-25</releaseDate>
<version>1.3.4</version>
<releaseDate>2024-04-09</releaseDate>
<version>1.4.0</version>
<develStage>stable</develStage>
<compatibility>
<ver>5.27</ver>
Expand All @@ -32,5 +32,6 @@
<mixins>
<mixin>menu-xml@1.0.0</mixin>
<mixin>setting-php@1.0.0</mixin>
<mixin>mgd-php@1.0.0</mixin>
</mixins>
</extension>
Loading

0 comments on commit 9bfa57a

Please sign in to comment.