From 955112671458d1f39a60af6eecc168a67af40a63 Mon Sep 17 00:00:00 2001 From: Dmitrii Metelkin Date: Tue, 2 Apr 2024 16:17:03 +1100 Subject: [PATCH] issue #40: add support for date field --- classes/condition_base.php | 20 +++ .../condition/cohort_field.php | 39 ++---- .../condition/fields_trait.php | 126 ++++++++++++++++-- .../condition/user_custom_profile.php | 18 ++- .../condition/user_profile.php | 31 +---- lang/en/tool_dynamic_cohorts.php | 2 + .../condition/cohort_field_test.php | 55 ++++++++ .../condition/user_custom_profile_test.php | 34 ++++- version.php | 4 +- 9 files changed, 257 insertions(+), 72 deletions(-) diff --git a/classes/condition_base.php b/classes/condition_base.php index a5a1ff2..75df2c2 100644 --- a/classes/condition_base.php +++ b/classes/condition_base.php @@ -45,6 +45,16 @@ abstract class condition_base { */ public const FIELD_DATA_TYPE_CHECKBOX = 'checkbox'; + /** + * Value for date field types. + */ + public const FIELD_DATA_TYPE_DATE = 'date'; + + /** + * Value for date field types. + */ + public const FIELD_DATA_TYPE_DATETIME = 'datetime'; + /** * Value for operator text contains. */ @@ -85,6 +95,16 @@ abstract class condition_base { */ public const TEXT_IS_NOT_EQUAL_TO = 8; + /** + * Value for operator date is after. + */ + public const DATE_IS_AFTER = 1; + + /** + * Value for operator date is before. + */ + public const DATE_IS_BEFORE = 2; + /** * Condition persistent object. * diff --git a/classes/local/tool_dynamic_cohorts/condition/cohort_field.php b/classes/local/tool_dynamic_cohorts/condition/cohort_field.php index 9cc1bf6..2e624ed 100644 --- a/classes/local/tool_dynamic_cohorts/condition/cohort_field.php +++ b/classes/local/tool_dynamic_cohorts/condition/cohort_field.php @@ -85,7 +85,8 @@ protected function get_cohort_operators(): array { * @return array */ protected function get_supported_custom_fields(): array { - return [self::FIELD_DATA_TYPE_TEXT, self::FIELD_DATA_TYPE_SELECT, self::FIELD_DATA_TYPE_CHECKBOX]; + return [self::FIELD_DATA_TYPE_TEXT, self::FIELD_DATA_TYPE_SELECT, + self::FIELD_DATA_TYPE_CHECKBOX, self::FIELD_DATA_TYPE_DATE]; } /** @@ -154,6 +155,9 @@ protected function get_fields_info(): array { case self::FIELD_DATA_TYPE_CHECKBOX: $fields[$shortname]->param1 = array_combine([0, 1], [get_string('no'), get_string('yes')]); break; + case self::FIELD_DATA_TYPE_DATE: + $fields[$shortname]->paramtype = PARAM_INT; + break; default: throw new coding_exception('Invalid field type ' . $fields[$shortname]->datatype); } @@ -217,6 +221,9 @@ public function config_form_add(\MoodleQuickForm $mform): void { case self::FIELD_DATA_TYPE_CHECKBOX: $this->add_checkbox_field($mform, $group, $field, $shortname); break; + case self::FIELD_DATA_TYPE_DATE: + $this->add_date_field($mform, $group, $field, $shortname); + break; default: throw new coding_exception('Invalid field type ' . $field->datatype); } @@ -244,33 +251,6 @@ public function config_form_add(\MoodleQuickForm $mform): void { } } - /** - * Validate config form elements. - * - * @param array $data Data to validate. - * @return array - */ - public function config_form_validate(array $data): array { - $errors = []; - - $fields = $this->get_fields_info(); - if (empty($data[static::get_form_field()]) || !isset($fields[$data[static::get_form_field()]])) { - $errors['fieldgroup'] = get_string('pleaseselectfield', 'tool_dynamic_cohorts'); - } - - $fieldvalue = $data[static::get_form_field()] . '_value'; - $operator = $data[static::get_form_field()] . '_operator'; - $datatype = $fields[$data[static::get_form_field()]]->datatype ?? ''; - - if (empty($data[$fieldvalue])) { - if ($datatype == 'text' && !in_array($data[$operator], [self::TEXT_IS_EMPTY, self::TEXT_IS_NOT_EMPTY])) { - $errors['fieldgroup'] = get_string('invalidfieldvalue', 'tool_dynamic_cohorts'); - } - } - - return $errors; - } - /** * Gets required config data from submitted condition form data. * @@ -405,6 +385,9 @@ public function get_sql(): condition_sql { case self::FIELD_DATA_TYPE_SELECT: $fieldsqldata = $this->get_menu_sql($fieldstable, $dbcolumn); break; + case self::FIELD_DATA_TYPE_DATE: + $fieldsqldata = $this->get_date_sql($fieldstable, $dbcolumn); + break; default: throw new coding_exception('Invalid field type ' . $datatype); } diff --git a/classes/local/tool_dynamic_cohorts/condition/fields_trait.php b/classes/local/tool_dynamic_cohorts/condition/fields_trait.php index af941fb..4b9727d 100644 --- a/classes/local/tool_dynamic_cohorts/condition/fields_trait.php +++ b/classes/local/tool_dynamic_cohorts/condition/fields_trait.php @@ -57,6 +57,20 @@ protected function get_menu_operators(): array { ]; } + /** + * Gets a list of comparison operators for date fields. + * + * @return array A list of operators. + */ + protected function get_date_operators(): array { + return [ + self::DATE_IS_AFTER => get_string('isafter', 'tool_dynamic_cohorts'), + self::DATE_IS_BEFORE => get_string('isbefore', 'tool_dynamic_cohorts'), + self::TEXT_IS_EMPTY => get_string('isempty', 'filters'), + self::TEXT_IS_NOT_EMPTY => get_string('isnotempty', 'tool_dynamic_cohorts'), + ]; + } + /** * Returns a field name for the configured field. * @@ -109,7 +123,9 @@ protected function get_field_value_text(): ?string { case self::FIELD_DATA_TYPE_CHECKBOX: $fieldvalue = $fieldinfo[$fieldname]->param1[$fieldvalue]; break; - + case self::FIELD_DATA_TYPE_DATE: + case self::FIELD_DATA_TYPE_DATETIME: + $fieldvalue = userdate($fieldvalue); } } @@ -136,15 +152,16 @@ protected function get_operator_value(): int { * @return string */ protected function get_operator_text(string $fielddatatype): string { - if ($fielddatatype == self::FIELD_DATA_TYPE_TEXT) { - return $this->get_text_operators()[$this->get_operator_value()]; - } - - if ($fielddatatype == self::FIELD_DATA_TYPE_MENU) { - return $this->get_menu_operators()[$this->get_operator_value()]; + switch ($fielddatatype) { + case self::FIELD_DATA_TYPE_MENU: + case self::FIELD_DATA_TYPE_SELECT: + return $this->get_menu_operators()[$this->get_operator_value()]; + case self::FIELD_DATA_TYPE_DATETIME: + case self::FIELD_DATA_TYPE_DATE: + return $this->get_date_operators()[$this->get_operator_value()]; + default: + return $this->get_text_operators()[$this->get_operator_value()]; } - - return $this->get_text_operators()[$this->get_operator_value()]; } /** @@ -205,6 +222,57 @@ protected function add_checkbox_field(\MoodleQuickForm $mform, array &$group, \s $mform->hideIf($shortname, self::get_form_field(), 'neq', $shortname); } + /** + * Adds a date field to the form. + * + * @param \MoodleQuickForm $mform Form to add the field to. + * @param array $group A group to add the field to. + * @param \stdClass $field Field info. + * @param string $shortname A field shortname. + */ + protected function add_date_field(\MoodleQuickForm $mform, array &$group, \stdClass $field, string $shortname): void { + $elements = []; + $elements[] = $mform->createElement('select', $shortname . '_operator', null, $this->get_date_operators()); + + $elements[] = $mform->createElement('date_time_selector', $shortname . '_value'); + $mform->setDefault($shortname . '_value', usergetmidnight(time())); + $mform->hideIf($shortname . '_value', $shortname . '_operator', 'in', self::TEXT_IS_EMPTY . '|' . self::TEXT_IS_NOT_EMPTY); + + $group[] = $mform->createElement('group', $shortname, '', $elements, '', false); + + $mform->hideIf($shortname . '_operator', static::get_form_field(), 'neq', $shortname); + $mform->hideIf($shortname . '_value', static::get_form_field(), 'neq', $shortname); + $mform->hideIf($shortname . '_value1', static::get_form_field(), 'neq', $shortname); + $mform->hideIf($shortname, static::get_form_field(), 'neq', $shortname); + } + + /** + * Validate config form elements. + * + * @param array $data Data to validate. + * @return array + */ + public function config_form_validate(array $data): array { + $errors = []; + + $fields = $this->get_fields_info(); + if (empty($data[static::get_form_field()]) || !isset($fields[$data[static::get_form_field()]])) { + $errors['fieldgroup'] = get_string('pleaseselectfield', 'tool_dynamic_cohorts'); + } + + $fieldvalue = $data[static::get_form_field()] . '_value'; + $operator = $data[static::get_form_field()] . '_operator'; + $datatype = $fields[$data[static::get_form_field()]]->datatype ?? ''; + + if (empty($data[$fieldvalue])) { + if ($datatype == 'text' && !in_array($data[$operator], [self::TEXT_IS_EMPTY, self::TEXT_IS_NOT_EMPTY])) { + $errors['fieldgroup'] = get_string('invalidfieldvalue', 'tool_dynamic_cohorts'); + } + } + + return $errors; + } + /** * Get SQl data for text type fields. * @@ -303,4 +371,44 @@ protected function get_menu_sql(string $tablealias, string $fieldname): conditio return new condition_sql('', $where, $params); } + + /** + * Get SQL data for date type fields. + * + * @param string $tablealias Alias for a table. + * @param string $fieldname Field name. + * @return condition_sql + */ + protected function get_date_sql(string $tablealias, string $fieldname): condition_sql { + $fieldvalue = $this->get_field_value(); + $operatorvalue = $this->get_operator_value(); + + if ($this->is_broken()) { + return new condition_sql('', '', []); + } + + $param = condition_sql::generate_param_alias(); + switch ($operatorvalue) { + case self::TEXT_IS_EMPTY: + $where = "$tablealias.$fieldname = :$param OR $tablealias.$fieldname IS NULL"; + $params[$param] = 0; + break; + case self::TEXT_IS_NOT_EMPTY: + $where = "$tablealias.$fieldname <> :$param"; + $params[$param] = (int) $fieldvalue; + break; + case self::DATE_IS_BEFORE: + $where = "$tablealias.$fieldname <= :$param"; + $params[$param] = (int) $fieldvalue; + break; + case self::DATE_IS_AFTER: + $where = "$tablealias.$fieldname >= :$param"; + $params[$param] = (int) $fieldvalue; + break; + default: + return new condition_sql('', '', []); + } + + return new condition_sql('', $where, $params); + } } diff --git a/classes/local/tool_dynamic_cohorts/condition/user_custom_profile.php b/classes/local/tool_dynamic_cohorts/condition/user_custom_profile.php index 34e0594..958356f 100644 --- a/classes/local/tool_dynamic_cohorts/condition/user_custom_profile.php +++ b/classes/local/tool_dynamic_cohorts/condition/user_custom_profile.php @@ -53,7 +53,8 @@ public function get_name(): string { * @return array */ protected function get_supported_custom_fields(): array { - return [self::FIELD_DATA_TYPE_TEXT, self::FIELD_DATA_TYPE_MENU, self::FIELD_DATA_TYPE_CHECKBOX]; + return [self::FIELD_DATA_TYPE_TEXT, self::FIELD_DATA_TYPE_MENU, + self::FIELD_DATA_TYPE_CHECKBOX, self::FIELD_DATA_TYPE_DATETIME]; } /** @@ -87,6 +88,9 @@ protected function get_fields_info(): array { case self::FIELD_DATA_TYPE_CHECKBOX: $field->param1 = array_combine([0, 1], [get_string('no'), get_string('yes')]); break; + case self::FIELD_DATA_TYPE_DATETIME: + $field->paramtype = PARAM_INT; + break; default: throw new coding_exception('Invalid field type ' . $field->datatype); } @@ -119,16 +123,19 @@ public function config_form_add(\MoodleQuickForm $mform): void { case self::FIELD_DATA_TYPE_TEXT: $this->add_text_field($mform, $group, $field, $shortname); break; - case self::FIELD_DATA_TYPE_MENU: + case self::FIELD_DATA_TYPE_MENU: $this->add_menu_field($mform, $group, $field, $shortname); break; - case self::FIELD_DATA_TYPE_CHECKBOX: + case self::FIELD_DATA_TYPE_CHECKBOX: $this->add_checkbox_field($mform, $group, $field, $shortname); break; + case self::FIELD_DATA_TYPE_DATETIME: + $this->add_date_field($mform, $group, $field, $shortname); + break; } } - $mform->addGroup($group, 'profilefieldgroup', get_string('profilefield', 'tool_dynamic_cohorts'), '', false); + $mform->addGroup($group, 'fieldgroup', get_string('profilefield', 'tool_dynamic_cohorts'), '', false); $mform->addElement( 'checkbox', @@ -182,6 +189,9 @@ public function get_sql(): condition_sql { case self::FIELD_DATA_TYPE_MENU: $result = $this->get_menu_sql($ud, 'data'); break; + case self::FIELD_DATA_TYPE_DATETIME: + $result = $this->get_date_sql($ud, 'data'); + break; } if (!empty($result->get_params())) { diff --git a/classes/local/tool_dynamic_cohorts/condition/user_profile.php b/classes/local/tool_dynamic_cohorts/condition/user_profile.php index 6ce49ef..0a5c1a0 100644 --- a/classes/local/tool_dynamic_cohorts/condition/user_profile.php +++ b/classes/local/tool_dynamic_cohorts/condition/user_profile.php @@ -84,39 +84,14 @@ public function config_form_add(\MoodleQuickForm $mform): void { case self::FIELD_DATA_TYPE_CHECKBOX: $this->add_checkbox_field($mform, $group, $field, $shortname); break; + case self::FIELD_DATA_TYPE_DATE: + $this->add_date_field($mform, $group, $field, $shortname); default: throw new coding_exception('Invalid field type ' . $field->datatype); } } - $mform->addGroup($group, 'profilefieldgroup', get_string('profilefield', 'tool_dynamic_cohorts'), '', false); - } - - /** - * Validate config form elements. - * - * @param array $data Data to validate. - * @return array - */ - public function config_form_validate(array $data): array { - $errors = []; - - $fields = $this->get_fields_info(); - if (empty($data[static::get_form_field()]) || !isset($fields[$data[static::get_form_field()]])) { - $errors['profilefieldgroup'] = get_string('pleaseselectfield', 'tool_dynamic_cohorts'); - } - - $fieldvalue = $data[static::get_form_field()] . '_value'; - $operator = $data[static::get_form_field()] . '_operator'; - $datatype = $fields[$data[static::get_form_field()]]->datatype ?? ''; - - if (empty($data[$fieldvalue])) { - if ($datatype == 'text' && !in_array($data[$operator], [self::TEXT_IS_EMPTY, self::TEXT_IS_NOT_EMPTY])) { - $errors['profilefieldgroup'] = get_string('invalidfieldvalue', 'tool_dynamic_cohorts'); - } - } - - return $errors; + $mform->addGroup($group, 'fieldgroup', get_string('profilefield', 'tool_dynamic_cohorts'), '', false); } /** diff --git a/lang/en/tool_dynamic_cohorts.php b/lang/en/tool_dynamic_cohorts.php index 839e928..966e888 100644 --- a/lang/en/tool_dynamic_cohorts.php +++ b/lang/en/tool_dynamic_cohorts.php @@ -82,6 +82,8 @@ $string['ismemberof'] = 'is member of'; $string['isnotmemberof'] = 'is not member of'; $string['isnotempty'] = 'is not empty'; +$string['isafter'] = 'is after'; +$string['isbefore'] = 'is before'; $string['logical_operator'] = 'Logical operator'; $string['logical_operator_help'] = 'A logical operator to be applied to conditions for this rule. Operator "AND" means a user has to match all conditions to be added to a cohort. "OR" means a user has to match any of conditions to be added to a cohort.'; $string['managerules'] = 'Manage rules'; diff --git a/tests/local/tool_dynamic_cohorts/condition/cohort_field_test.php b/tests/local/tool_dynamic_cohorts/condition/cohort_field_test.php index e293e5d..1e25830 100644 --- a/tests/local/tool_dynamic_cohorts/condition/cohort_field_test.php +++ b/tests/local/tool_dynamic_cohorts/condition/cohort_field_test.php @@ -345,6 +345,33 @@ public function test_config_description_custom_field() { 'A user is member of cohorts with field \'Custom field\' is equal to No', $condition->get_config_description() ); + + $now = time(); + $datefield = $this->create_cohort_custom_field('datefield', 'date'); + $datefieldfieldname = cohort_field::CUSTOM_FIELD_PREFIX . $datefield->get('shortname'); + $condition = $this->get_condition([ + 'cohort_field_operator' => cohort_field::OPERATOR_IS_MEMBER_OF, + 'cohort_field_field' => $datefieldfieldname, + $datefieldfieldname . '_operator' => condition_base::DATE_IS_AFTER, + $datefieldfieldname . '_value' => $now, + ]); + + $this->assertSame( + 'A user is member of cohorts with field \'Custom field\' is after ' . userdate($now), + $condition->get_config_description() + ); + + $condition = $this->get_condition([ + 'cohort_field_operator' => cohort_field::OPERATOR_IS_MEMBER_OF, + 'cohort_field_field' => $datefieldfieldname, + $datefieldfieldname . '_operator' => condition_base::DATE_IS_BEFORE, + $datefieldfieldname . '_value' => $now, + ]); + + $this->assertSame( + 'A user is member of cohorts with field \'Custom field\' is before ' . userdate($now), + $condition->get_config_description() + ); } /** @@ -362,12 +389,15 @@ public function test_get_sql_data_custom_fields() { // We need admin to be able to add custom fields data for cohorts. $this->setAdminUser(); + $now = time(); $textfield = $this->create_cohort_custom_field(); $checkboxfield = $this->create_cohort_custom_field('checkboxfield', 'checkbox'); + $datefield = $this->create_cohort_custom_field('datefield', 'date'); $cohort1 = $this->getDataGenerator()->create_cohort([ 'customfield_' . $textfield->get('shortname') => 'Test value 1', 'customfield_' . $checkboxfield->get('shortname') => '1', + 'customfield_' . $datefield->get('shortname') => $now - WEEKSECS, ]); $cohort2 = $this->getDataGenerator()->create_cohort(); @@ -382,6 +412,7 @@ public function test_get_sql_data_custom_fields() { $totalusers = $DB->count_records('user'); $textfieldname = cohort_field::CUSTOM_FIELD_PREFIX . $textfield->get('shortname'); $checkboxfieldname = cohort_field::CUSTOM_FIELD_PREFIX . $checkboxfield->get('shortname'); + $datefieldname = cohort_field::CUSTOM_FIELD_PREFIX . $datefield->get('shortname'); // User 1 and user 2 as they are members of cohort 1. $condition = $this->get_condition([ @@ -443,6 +474,30 @@ public function test_get_sql_data_custom_fields() { $result = $condition->get_sql(); $sql = "SELECT u.id FROM {user} u {$result->get_join()} WHERE {$result->get_where()}"; $this->assertCount(2, $DB->get_records_sql($sql, $result->get_params())); + + // User 1 and user 2 as they are members of cohort 1. + $condition = $this->get_condition([ + 'cohort_field_operator' => cohort_field::OPERATOR_IS_MEMBER_OF, + 'cohort_field_field' => $datefieldname, + $datefieldname . '_operator' => condition_base::DATE_IS_BEFORE, + $datefieldname . '_value' => $now, + ]); + + $result = $condition->get_sql(); + $sql = "SELECT u.id FROM {user} u {$result->get_join()} WHERE {$result->get_where()}"; + $this->assertCount(2, $DB->get_records_sql($sql, $result->get_params())); + + // All users except user 3 as he is a members of cohort 2. + $condition = $this->get_condition([ + 'cohort_field_operator' => cohort_field::OPERATOR_IS_NOT_MEMBER_OF, + 'cohort_field_field' => $datefieldname, + $datefieldname . '_operator' => condition_base::TEXT_IS_EMPTY, + $datefieldname . '_value' => $now, + ]); + + $result = $condition->get_sql(); + $sql = "SELECT u.id FROM {user} u {$result->get_join()} WHERE {$result->get_where()}"; + $this->assertCount($totalusers - 1, $DB->get_records_sql($sql, $result->get_params())); } /** diff --git a/tests/local/tool_dynamic_cohorts/condition/user_custom_profile_test.php b/tests/local/tool_dynamic_cohorts/condition/user_custom_profile_test.php index c9c4c3e..360bb99 100644 --- a/tests/local/tool_dynamic_cohorts/condition/user_custom_profile_test.php +++ b/tests/local/tool_dynamic_cohorts/condition/user_custom_profile_test.php @@ -201,7 +201,7 @@ public function test_config_description(int $operator, string $expected, bool $s } /** - * Test setting and getting config data. + * Test sql data generation. */ public function test_get_sql_data() { global $DB; @@ -211,16 +211,21 @@ public function test_get_sql_data() { $fieldtext1 = $this->add_user_profile_field('field1', 'text'); $fieldtext2 = $this->add_user_profile_field('field2', 'text', ['param1' => "Opt 1\nOpt 2\nOpt 3"]); $fieldcheckbox = $this->add_user_profile_field('field3', 'checkbox'); + $fielddate = $this->add_user_profile_field('field4', 'datetime', ['param1' => 2000, 'param2' => 5000]); + + $now = time(); $user1 = $this->getDataGenerator()->create_user(['username' => 'user1']); profile_save_data((object)['id' => $user1->id, 'profile_field_' . $fieldtext1->shortname => 'User 1 Field 1']); profile_save_data((object)['id' => $user1->id, 'profile_field_' . $fieldtext2->shortname => 'Opt 1']); profile_save_data((object)['id' => $user1->id, 'profile_field_' . $fieldcheckbox->shortname => '1']); + profile_save_data((object)['id' => $user1->id, 'profile_field_' . $fielddate->shortname => $now - WEEKSECS]); $user2 = $this->getDataGenerator()->create_user(['username' => 'user2']); profile_save_data((object)['id' => $user2->id, 'profile_field_' . $fieldtext1->shortname => 'User 2 Field 1']); profile_save_data((object)['id' => $user2->id, 'profile_field_' . $fieldtext2->shortname => 'Opt 2']); profile_save_data((object)['id' => $user2->id, 'profile_field_' . $fieldcheckbox->shortname => '0']); + profile_save_data((object)['id' => $user2->id, 'profile_field_' . $fielddate->shortname => $now + WEEKSECS]); $totalusers = $DB->count_records('user'); @@ -283,6 +288,33 @@ public function test_get_sql_data() { $result = $condition->get_sql(); $sql = "SELECT u.id FROM {user} u {$result->get_join()} WHERE {$result->get_where()}"; $this->assertCount(1, $DB->get_records_sql($sql, $result->get_params())); + + $test = $DB->get_records('user_info_data'); + + $fieldname = 'profile_field_' . $fielddate->shortname; + $condition->set_config_data([ + 'profilefield' => $fieldname, + $fieldname . '_operator' => condition_base::DATE_IS_BEFORE, + $fieldname . '_value' => $now, + 'include_missing_data' => 0, + ]); + + $result = $condition->get_sql(); + $sql = "SELECT u.id FROM {user} u {$result->get_join()} WHERE {$result->get_where()}"; + $this->assertCount(1, $DB->get_records_sql($sql, $result->get_params())); + + $fieldname = 'profile_field_' . $fielddate->shortname; + $condition->set_config_data([ + 'profilefield' => $fieldname, + $fieldname . '_operator' => condition_base::DATE_IS_AFTER, + $fieldname . '_value' => $now, + 'include_missing_data' => 0, + ]); + + $result = $condition->get_sql(); + $sql = "SELECT u.id FROM {user} u {$result->get_join()} WHERE {$result->get_where()}"; + $this->assertCount(1, $DB->get_records_sql($sql, $result->get_params())); + } /** diff --git a/version.php b/version.php index 1553fc8..d6388c0 100644 --- a/version.php +++ b/version.php @@ -25,8 +25,8 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'tool_dynamic_cohorts'; -$plugin->release = 2024032600; -$plugin->version = 2024032600; +$plugin->release = 2024040200; +$plugin->version = 2024040200; $plugin->requires = 2022112800; $plugin->supported = [401, 403]; $plugin->maturity = MATURITY_STABLE;