Compare commits

..

4 commits

Author SHA1 Message Date
Jens Schuppe
4977c375ef Version 1.4.1 2024-11-28 14:05:19 +01:00
Jens Schuppe
f7572ba8dc Upgrade Civix-generated code to Civix version 23.02.1 2024-11-28 14:04:18 +01:00
Jens Schuppe
373c9f83d3 Back to 1.4-dev 2024-09-03 13:15:44 +02:00
Jens Schuppe
68e244d456 Version 1.4.0 2024-09-03 13:15:18 +02:00
86 changed files with 1458 additions and 11104 deletions

View file

@ -1,240 +0,0 @@
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 120
tab_width = 2
ij_continuation_indent_size = 2
ij_visual_guides = 80,120
[{*.php}]
ij_php_align_assignments = false
ij_php_align_class_constants = false
ij_php_align_enum_cases = false
ij_php_align_group_field_declarations = false
ij_php_align_inline_comments = false
ij_php_align_key_value_pairs = false
ij_php_align_match_arm_bodies = false
ij_php_align_multiline_array_initializer_expression = false
ij_php_align_multiline_binary_operation = false
ij_php_align_multiline_chained_methods = false
ij_php_align_multiline_extends_list = false
ij_php_align_multiline_for = true
ij_php_align_multiline_parameters = false
ij_php_align_multiline_parameters_in_calls = false
ij_php_align_multiline_ternary_operation = false
ij_php_align_named_arguments = false
ij_php_align_phpdoc_comments = false
ij_php_align_phpdoc_param_names = false
ij_php_anonymous_brace_style = end_of_line
ij_php_api_weight = 28
ij_php_array_initializer_new_line_after_left_brace = true
ij_php_array_initializer_right_brace_on_new_line = true
ij_php_array_initializer_wrap = on_every_item
ij_php_assignment_wrap = normal
ij_php_attributes_wrap = normal
ij_php_author_weight = 28
ij_php_binary_operation_sign_on_next_line = false
ij_php_binary_operation_wrap = normal
ij_php_blank_lines_after_class_header = 1
ij_php_blank_lines_after_function = 1
ij_php_blank_lines_after_imports = 1
ij_php_blank_lines_after_opening_tag = 0
ij_php_blank_lines_after_package = 1
ij_php_blank_lines_around_class = 1
ij_php_blank_lines_around_constants = 1
ij_php_blank_lines_around_enum_cases = 0
ij_php_blank_lines_around_field = 1
ij_php_blank_lines_around_method = 1
ij_php_blank_lines_before_class_end = 1
ij_php_blank_lines_before_imports = 1
ij_php_blank_lines_before_method_body = 0
ij_php_blank_lines_before_package = 1
ij_php_blank_lines_before_return_statement = 1
ij_php_blank_lines_between_imports = 0
ij_php_block_brace_style = end_of_line
ij_php_call_parameters_new_line_after_left_paren = true
ij_php_call_parameters_right_paren_on_new_line = true
ij_php_call_parameters_wrap = on_every_item
ij_php_catch_on_new_line = true
ij_php_category_weight = 28
ij_php_class_brace_style = end_of_line
ij_php_comma_after_last_argument = false
ij_php_comma_after_last_array_element = true
ij_php_comma_after_last_closure_use_var = false
ij_php_comma_after_last_match_arm = false
ij_php_comma_after_last_parameter = false
ij_php_concat_spaces = true
ij_php_copyright_weight = 28
ij_php_deprecated_weight = 4
ij_php_do_while_brace_force = always
ij_php_else_if_style = as_is
ij_php_else_on_new_line = true
ij_php_example_weight = 28
ij_php_extends_keyword_wrap = off
ij_php_extends_list_wrap = off
ij_php_fields_default_visibility = private
ij_php_filesource_weight = 28
ij_php_finally_on_new_line = true
ij_php_for_brace_force = always
ij_php_for_statement_new_line_after_left_paren = false
ij_php_for_statement_right_paren_on_new_line = false
ij_php_for_statement_wrap = off
ij_php_force_empty_methods_in_one_line = false
ij_php_force_short_declaration_array_style = true
ij_php_getters_setters_naming_style = camel_case
ij_php_getters_setters_order_style = getters_first
ij_php_global_weight = 28
ij_php_group_use_wrap = on_every_item
ij_php_if_brace_force = always
ij_php_if_lparen_on_next_line = false
ij_php_if_rparen_on_next_line = false
ij_php_ignore_weight = 28
ij_php_import_sorting = alphabetic
ij_php_indent_break_from_case = true
ij_php_indent_case_from_switch = true
ij_php_indent_code_in_php_tags = false
ij_php_internal_weight = 28
ij_php_keep_blank_lines_after_lbrace = 1
ij_php_keep_blank_lines_before_right_brace = 1
ij_php_keep_blank_lines_in_code = 1
ij_php_keep_blank_lines_in_declarations = 1
ij_php_keep_control_statement_in_one_line = false
ij_php_keep_first_column_comment = false
ij_php_keep_indents_on_empty_lines = false
ij_php_keep_line_breaks = false
ij_php_keep_rparen_and_lbrace_on_one_line = true
ij_php_keep_simple_classes_in_one_line = false
ij_php_keep_simple_methods_in_one_line = false
ij_php_lambda_brace_style = end_of_line
ij_php_license_weight = 28
ij_php_line_comment_add_space = false
ij_php_line_comment_at_first_column = true
ij_php_link_weight = 28
ij_php_lower_case_boolean_const = false
ij_php_lower_case_keywords = true
ij_php_lower_case_null_const = false
ij_php_method_brace_style = end_of_line
ij_php_method_call_chain_wrap = on_every_item
ij_php_method_parameters_new_line_after_left_paren = true
ij_php_method_parameters_right_paren_on_new_line = true
ij_php_method_parameters_wrap = on_every_item
ij_php_method_weight = 28
ij_php_modifier_list_wrap = false
ij_php_multiline_chained_calls_semicolon_on_new_line = true
ij_php_namespace_brace_style = 1
ij_php_new_line_after_php_opening_tag = true
ij_php_null_type_position = in_the_end
ij_php_package_weight = 28
ij_php_param_weight = 1
ij_php_parameters_attributes_wrap = normal
ij_php_parentheses_expression_new_line_after_left_paren = false
ij_php_parentheses_expression_right_paren_on_new_line = false
ij_php_phpdoc_blank_line_before_tags = true
ij_php_phpdoc_blank_lines_around_parameters = true
ij_php_phpdoc_keep_blank_lines = true
ij_php_phpdoc_param_spaces_between_name_and_description = 1
ij_php_phpdoc_param_spaces_between_tag_and_type = 1
ij_php_phpdoc_param_spaces_between_type_and_name = 1
ij_php_phpdoc_use_fqcn = true
ij_php_phpdoc_wrap_long_lines = true
ij_php_place_assignment_sign_on_next_line = false
ij_php_place_parens_for_constructor = 1
ij_php_property_read_weight = 28
ij_php_property_weight = 28
ij_php_property_write_weight = 28
ij_php_return_type_on_new_line = false
ij_php_return_weight = 2
ij_php_see_weight = 5
ij_php_since_weight = 28
ij_php_sort_phpdoc_elements = true
ij_php_space_after_colon = true
ij_php_space_after_colon_in_enum_backed_type = true
ij_php_space_after_colon_in_named_argument = true
ij_php_space_after_colon_in_return_type = true
ij_php_space_after_comma = true
ij_php_space_after_for_semicolon = true
ij_php_space_after_quest = true
ij_php_space_after_type_cast = true
ij_php_space_after_unary_not = false
ij_php_space_before_array_initializer_left_brace = false
ij_php_space_before_catch_keyword = true
ij_php_space_before_catch_left_brace = true
ij_php_space_before_catch_parentheses = true
ij_php_space_before_class_left_brace = true
ij_php_space_before_closure_left_parenthesis = true
ij_php_space_before_colon = true
ij_php_space_before_colon_in_enum_backed_type = false
ij_php_space_before_colon_in_named_argument = false
ij_php_space_before_colon_in_return_type = false
ij_php_space_before_comma = false
ij_php_space_before_do_left_brace = true
ij_php_space_before_else_keyword = true
ij_php_space_before_else_left_brace = true
ij_php_space_before_finally_keyword = true
ij_php_space_before_finally_left_brace = true
ij_php_space_before_for_left_brace = true
ij_php_space_before_for_parentheses = true
ij_php_space_before_for_semicolon = false
ij_php_space_before_if_left_brace = true
ij_php_space_before_if_parentheses = true
ij_php_space_before_method_call_parentheses = false
ij_php_space_before_method_left_brace = true
ij_php_space_before_method_parentheses = false
ij_php_space_before_quest = true
ij_php_space_before_short_closure_left_parenthesis = false
ij_php_space_before_switch_left_brace = true
ij_php_space_before_switch_parentheses = true
ij_php_space_before_try_left_brace = true
ij_php_space_before_unary_not = false
ij_php_space_before_while_keyword = true
ij_php_space_before_while_left_brace = true
ij_php_space_before_while_parentheses = true
ij_php_space_between_ternary_quest_and_colon = false
ij_php_spaces_around_additive_operators = true
ij_php_spaces_around_arrow = false
ij_php_spaces_around_assignment_in_declare = true
ij_php_spaces_around_assignment_operators = true
ij_php_spaces_around_bitwise_operators = true
ij_php_spaces_around_equality_operators = true
ij_php_spaces_around_logical_operators = true
ij_php_spaces_around_multiplicative_operators = true
ij_php_spaces_around_null_coalesce_operator = true
ij_php_spaces_around_pipe_in_union_type = false
ij_php_spaces_around_relational_operators = true
ij_php_spaces_around_shift_operators = true
ij_php_spaces_around_unary_operator = false
ij_php_spaces_around_var_within_brackets = false
ij_php_spaces_within_array_initializer_braces = false
ij_php_spaces_within_brackets = false
ij_php_spaces_within_catch_parentheses = false
ij_php_spaces_within_for_parentheses = false
ij_php_spaces_within_if_parentheses = false
ij_php_spaces_within_method_call_parentheses = false
ij_php_spaces_within_method_parentheses = false
ij_php_spaces_within_parentheses = false
ij_php_spaces_within_short_echo_tags = true
ij_php_spaces_within_switch_parentheses = false
ij_php_spaces_within_while_parentheses = false
ij_php_special_else_if_treatment = false
ij_php_subpackage_weight = 28
ij_php_ternary_operation_signs_on_next_line = true
ij_php_ternary_operation_wrap = on_every_item
ij_php_throws_weight = 3
ij_php_todo_weight = 6
ij_php_treat_multiline_arrays_and_lambdas_multiline = false
ij_php_unknown_tag_weight = 28
ij_php_upper_case_boolean_const = true
ij_php_upper_case_null_const = true
ij_php_uses_weight = 28
ij_php_var_weight = 0
ij_php_variable_naming_style = camel_case
ij_php_version_weight = 28
ij_php_while_brace_force = always
ij_php_while_on_new_line = false
[{*.neon,*.neon.dist,*neon.template}]
indent_style = tab
tab_width = 4

View file

@ -1,42 +0,0 @@
name: PHP_CodeSniffer
on:
pull_request:
paths:
- '**.php'
- tools/phpcs/composer.json
- phpcs.xml.dist
jobs:
phpcs:
runs-on: ubuntu-latest
name: PHP_CodeSniffer
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.1
coverage: none
tools: cs2pr
env:
fail-fast: true
- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('tools/phpcs/composer.json') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer composer-phpcs -- update --no-progress --prefer-dist
- name: Run PHP_CodeSniffer
run: composer phpcs -- -q --report=checkstyle | cs2pr

View file

@ -1,52 +0,0 @@
name: PHPStan
on:
pull_request:
paths:
- '**.php'
- composer.json
- tools/phpstan/composer.json
- ci/composer.json
- phpstan.ci.neon
- phpstan.neon.dist
jobs:
phpstan:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ['7.4', '8.0', '8.1']
prefer: ['prefer-stable', 'prefer-lowest']
name: PHPStan with PHP ${{ matrix.php-versions }} ${{ matrix.prefer }}
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
coverage: none
env:
fail-fast: true
- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.prefer }}-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-composer-${{ matrix.prefer }}-
- name: Install dependencies
run: |
composer update --no-progress --prefer-dist --${{ matrix.prefer }} &&
composer composer-phpunit -- update --no-progress --prefer-dist &&
composer composer-phpstan -- update --no-progress --prefer-dist --optimize-autoloader &&
composer --working-dir=ci update --no-progress --prefer-dist --${{ matrix.prefer }} --optimize-autoloader --ignore-platform-req=ext-gd
- name: Run PHPStan
run: composer phpstan -- analyse -c phpstan.ci.neon

View file

@ -1,37 +0,0 @@
name: PHPUnit
on:
pull_request:
paths:
- '**.php'
- composer.json
- tools/phpunit/composer.json
- phpunit.xml.dist
- tests/docker-prepare.sh
env:
# On github CI machine creating the "/vendor" volume fails otherwise with: read-only file system: unknown
BIND_VOLUME_PERMISSIONS: rw
jobs:
phpunit:
runs-on: ubuntu-latest
strategy:
matrix:
civicrm-image-tags: [ '5-drupal-php8.1', '5-drupal-php7.4', '5.56-drupal-php7.4' ]
name: PHPUnit with Docker image michaelmcandrew/civicrm:${{ matrix.civicrm-image-tags }}
env:
CIVICRM_IMAGE_TAG: ${{ matrix.civicrm-image-tags }}
steps:
- uses: actions/checkout@v3
- name: Pull images
run: docker compose -f tests/docker-compose.yml pull --quiet
- name: Start containers
run: docker compose -f tests/docker-compose.yml up -d
- name: Prepare environment
run: docker compose -f tests/docker-compose.yml exec civicrm sites/default/files/civicrm/ext/de.systopia.twingle/tests/docker-prepare.sh
- name: Run PHPUnit
run: docker compose -f tests/docker-compose.yml exec civicrm sites/default/files/civicrm/ext/de.systopia.twingle/tests/docker-phpunit.sh
- name: Remove containers
run: docker compose -f tests/docker-compose.yml down -v

10
.gitignore vendored
View file

@ -1,10 +0,0 @@
/.phpcs.cache
/.phpunit.result.cache
/.phpstan/
/ci/composer.lock
/ci/vendor/
/composer.lock
/phpstan.neon
/tools/*/vendor/
/tools/*/composer.lock
/vendor/

View file

@ -1,676 +0,0 @@
<?php
use Civi\Api4\PriceField;
use Civi\Api4\PriceFieldValue;
use Civi\Twingle\Shop\Exceptions\ProductException;
use Civi\Twingle\Shop\Exceptions\ShopException;
use CRM_Twingle_ExtensionUtil as E;
use function Civi\Twingle\Shop\Utils\convert_int_to_bool;
use function Civi\Twingle\Shop\Utils\convert_str_to_date;
use function Civi\Twingle\Shop\Utils\convert_str_to_int;
use function Civi\Twingle\Shop\Utils\convert_empty_string_to_null;
use function Civi\Twingle\Shop\Utils\filter_attributes;
use function Civi\Twingle\Shop\Utils\validate_data_types;
require_once E::path() . '/Civi/Twingle/Shop/Utils/TwingleShopUtils.php';
/**
* TwingleProduct BAO class.
* This class is used to implement the logic for the TwingleProduct entity.
*/
class CRM_Twingle_BAO_TwingleProduct extends CRM_Twingle_DAO_TwingleProduct {
/**
* Name of this product.
*/
public $name;
/**
* Is this product active?
*/
public $is_active;
/**
* Price of this product.
*/
public $price;
/**
* Sort order of this product.
*/
public $sort;
/**
* Short description of this Product.
*/
public $text;
/**
* Long description of this Product.
*/
public $description;
/**
* ID of the corresponding Twingle Shop.
*/
public $project_id;
/**
* ID of the financial type of this product.
*/
public $financial_type_id;
/**
* Timestamp of the last update in Twingle db.
*/
public $tw_updated_at;
/**
* The values of this attributes can be 0.
* (For filtering purposes)
*/
protected const CAN_BE_ZERO = [
"price",
"sort",
];
/**
* Attributes that need to be converted to int.
*/
protected const STR_TO_INT_CONVERSION = [
"id",
"twingle_shop_id",
"financial_type_id",
"price_field_id",
"project_id",
"external_id",
"tw_updated_at",
"tw_created_at",
"price",
"sort",
];
/**
* Attributes that need to be converted to boolean.
*/
protected const INT_TO_BOOL_CONVERSION = [
"is_active",
];
/**
* String to date conversion.
*/
protected const STR_TO_DATE_CONVERSION = [
"created_at",
"updated_at",
];
/**
* Empty string to null conversion.
*/
protected const EMPTY_STRING_TO_NULL = [
"price",
];
/**
* Allowed product attributes.
* Attributes that we currently don't support are commented out.
*/
protected const ALLOWED_ATTRIBUTES = [
"id" => CRM_Utils_Type::T_INT,
"external_id" => CRM_Utils_Type::T_INT,
"name" => CRM_Utils_Type::T_STRING,
"is_active" => CRM_Utils_Type::T_BOOLEAN,
"description" => CRM_Utils_Type::T_STRING,
"price" => CRM_Utils_Type::T_INT,
"created_at" => CRM_Utils_Type::T_INT,
"tw_created_at" => CRM_Utils_Type::T_INT,
"updated_at" => CRM_Utils_Type::T_INT,
"tw_updated_at" => CRM_Utils_Type::T_INT,
"is_orphaned" => CRM_Utils_Type::T_BOOLEAN,
"is_outdated" => CRM_Utils_Type::T_BOOLEAN,
"project_id" => CRM_Utils_Type::T_INT,
"sort" => CRM_Utils_Type::T_INT,
"financial_type_id" => CRM_Utils_Type::T_INT,
"twingle_shop_id" => CRM_Utils_Type::T_INT,
"price_field_id" => CRM_Utils_Type::T_INT,
# "text" => \CRM_Utils_Type::T_STRING,
# "images" => \CRM_Utils_Type::T_STRING,
# "categories" = \CRM_Utils_Type::T_STRING,
# "internal_id" => \CRM_Utils_Type::T_STRING,
# "has_zero_price" => \CRM_Utils_Type::T_BOOLEAN,
# "name_plural" => \CRM_Utils_Type::T_STRING,
# "max_count" => \CRM_Utils_Type::T_INT,
# "has_textinput" => \CRM_Utils_Type::T_BOOLEAN,
# "count" => \CRM_Utils_Type::T_INT,
];
/**
* Change attribute names to match the database column names.
*
* @param array $values
* Array with product data from Twingle API
*
* @return array
*/
public static function renameTwingleAttrs(array $values) {
$new_values = [];
foreach ($values as $key => $value) {
// replace 'id' with 'external_id'
if ($key == 'id') {
$key = 'external_id';
}
// replace 'updated_at' with 'tw_updated_at'
if ($key == 'updated_at') {
$key = 'tw_updated_at';
}
// replace 'created_at' with 'tw_created_at'
if ($key == 'created_at') {
$key = 'tw_created_at';
}
$new_values[$key] = $value;
}
return $new_values;
}
/**
* Load product data.
*
* @param array $product_data
* Array with product data
*
* @return void
*
* @throws ProductException
* @throws \Exception
*/
public function load(array $product_data): void {
// Filter for allowed attributes
filter_attributes(
$product_data,
self::ALLOWED_ATTRIBUTES,
self::CAN_BE_ZERO,
);
// Does this product allow to enter a custom price?
$custom_price = array_key_exists('price', $product_data) && $product_data['price'] === Null;
if (!$custom_price && isset($product_data['price_field_id'])) {
try {
$price_field = civicrm_api3('PriceField', 'getsingle', [
'id' => $product_data['price_field_id'],
'return' => 'is_enter_qty',
]);
$custom_price = (bool) $price_field['is_enter_qty'];
}
catch (CRM_Core_Exception $e) {
throw new ProductException(
E::ts("Could not find PriceField for Twingle Product ['id': %1, 'external_id': %2]: %3",
[
1 => $product_data['id'],
2 => $product_data['external_id'],
3 => $e->getMessage(),
]),
ProductException::ERROR_CODE_PRICE_FIELD_NOT_FOUND);
}
}
// Amend data from corresponding PriceFieldValue
if (isset($product_data['price_field_id'])) {
try {
$price_field_value = civicrm_api3('PriceFieldValue', 'getsingle', [
'price_field_id' => $product_data['price_field_id'],
]);
}
catch (CRM_Core_Exception $e) {
throw new ProductException(
E::ts("Could not find PriceFieldValue for Twingle Product ['id': %1, 'external_id': %2]: %3",
[
1 => $product_data['id'],
2 => $product_data['external_id'],
3 => $e->getMessage(),
]),
ProductException::ERROR_CODE_PRICE_FIELD_VALUE_NOT_FOUND);
}
$product_data['name'] = $product_data['name'] ?? $price_field_value['label'];
$product_data['price'] = $custom_price ? Null : $product_data['price'] ?? $price_field_value['amount'];
$product_data['financial_type_id'] = $product_data['financial_type_id'] ?? $price_field_value['financial_type_id'];
$product_data['is_active'] = $product_data['is_active'] ?? $price_field_value['is_active'];
$product_data['sort'] = $product_data['sort'] ?? $price_field_value['weight'];
$product_data['description'] = $product_data['description'] ?? $price_field_value['description'];
}
// Change data types
try {
convert_str_to_int($product_data, self::STR_TO_INT_CONVERSION);
convert_int_to_bool($product_data, self::INT_TO_BOOL_CONVERSION);
convert_str_to_date($product_data, self::STR_TO_DATE_CONVERSION);
convert_empty_string_to_null($product_data, self::EMPTY_STRING_TO_NULL);
}
catch (\Exception $e) {
throw new ProductException($e->getMessage(), ProductException::ERROR_CODE_ATTRIBUTE_WRONG_DATA_TYPE);
}
// Validate data types
try {
validate_data_types($product_data, self::ALLOWED_ATTRIBUTES);
}
catch (\Exception $e) {
throw new ProductException($e->getMessage(), ProductException::ERROR_CODE_ATTRIBUTE_WRONG_DATA_TYPE);
}
// Set attributes
foreach ($product_data as $key => $value) {
$this->$key = $value;
}
}
/**
* Creates a price field to represents this product in CiviCRM.
*
* @param string $mode
* 'create' or 'edit'
* @throws \Civi\Twingle\Shop\Exceptions\ProductException
*/
public function createPriceField() {
// Define mode for PriceField
$mode = $this->price_field_id ? 'edit' : 'create';
$action = $mode == 'create' ? 'create' : 'update';
// Check if PriceSet for this Shop already exists
try {
$price_field = civicrm_api3('PriceField', 'get', [
'name' => 'tw_product_' . $this->external_id,
]);
if ($price_field['count'] > 0 && $mode == 'create') {
throw new ProductException(
E::ts('PriceField for this Twingle Product already exists.'),
ProductException::ERROR_CODE_PRICE_FIELD_ALREADY_EXISTS,
);
} elseif ($price_field['count'] == 0 && $mode == 'edit') {
throw new ProductException(
E::ts('PriceField for this Twingle Product does not exist and cannot be edited.'),
ProductException::ERROR_CODE_PRICE_FIELD_NOT_FOUND,
);
}
}
catch (CRM_Core_Exception $e) {
throw new ProductException(
E::ts('Could not check if PriceField for this Twingle Product already exists.'),
ProductException::ERROR_CODE_PRICE_FIELD_NOT_FOUND,
);
}
// Try to find corresponding price set via TwingleShop
try {
$shop = civicrm_api3('TwingleShop', 'getsingle', [
'id' => $this->twingle_shop_id,
]);
$this->price_set_id = (int) $shop['price_set_id'];
}
catch (CRM_Core_Exception $e) {
throw new ProductException(
E::ts('Could not find PriceSet for this Twingle Product.'),
ProductException::ERROR_CODE_PRICE_SET_NOT_FOUND,
);
}
// Create PriceField
$price_field_data = [
'price_set_id' => $this->price_set_id,
'name' => 'tw_product_' . $this->external_id,
'label' => $this->name,
'is_active' => $this->is_active,
'weight' => $this->sort,
'html_type' => 'Text',
'is_required' => false,
];
// If the product has no fixed price, allow the user to enter a custom price
if ($this->price === Null) {
$price_field_data['is_enter_qty'] = true;
$price_field_data['is_display_amounts'] = false;
}
// Add id if in edit mode
if ($mode == 'edit') {
$price_field_data['id'] = $this->price_field_id;
}
try {
$price_field = civicrm_api4(
'PriceField',
$action,
['values' => $price_field_data],
)->first();
$this->price_field_id = (int) $price_field['id'];
}
catch (CRM_Core_Exception $e) {
throw new ProductException(
E::ts('Could not create PriceField for this Twingle Product: %1',
[1 => $e->getMessage()]),
ProductException::ERROR_CODE_COULD_NOT_CREATE_PRICE_FIELD);
}
// Try to find existing PriceFieldValue if in edit mode
$price_field_value = NULL;
if ($mode == 'edit') {
try {
$price_field_value = civicrm_api3('PriceFieldValue', 'getsingle', [
'price_field_id' => $this->price_field_id,
]);
}
catch (CRM_Core_Exception $e) {
throw new ProductException(
E::ts('Could not find PriceFieldValue for this Twingle Product: %1',
[1 => $e->getMessage()]),
ProductException::ERROR_CODE_PRICE_FIELD_VALUE_NOT_FOUND);
}
}
// Create PriceFieldValue
$price_field_value_data = [
'price_field_id' => $this->price_field_id,
'financial_type_id' => $this->financial_type_id,
'label' => $this->name,
'amount' => $this->price === Null ? 1 : $this->price,
'is_active' => $this->is_active,
'description' => $this->description,
];
// Add id if in edit mode
if ($mode == 'edit' && $price_field_value) {
$price_field_value_data['id'] = $price_field_value['id'];
}
try {
civicrm_api4(
'PriceFieldValue',
$action,
['values' => $price_field_value_data],
);
}
catch (CRM_Core_Exception $e) {
throw new ProductException(
E::ts('Could not create PriceFieldValue for this Twingle Product: %1',
[1 => $e->getMessage()]),
ProductException::ERROR_CODE_COULD_NOT_CREATE_PRICE_FIELD_VALUE);
}
}
/**
* Returns this TwingleProduct's attributes.
*
* @return array
* @throws \CRM_Core_Exception
*/
public function getAttributes() {
// Filter for allowed attributes
return array_intersect_key(
get_object_vars($this),
$this::ALLOWED_ATTRIBUTES
) // Add financial type id of this product if it exists
+ ['financial_type_id' => $this->getFinancialTypeId()];
}
/**
* Find TwingleProduct by its external ID.
*
* @param int $external_id
* External id of the product (by Twingle)
*
* @return CRM_Twingle_BAO_TwingleProduct|null
* TwingleProduct object or NULL if not found
*
* @throws \Civi\Twingle\Shop\Exceptions\ProductException
* @throws \Civi\Core\Exception\DBQueryException
*/
public static function findByExternalId($external_id) {
$dao = CRM_Twingle_BAO_TwingleShop::executeQuery("SELECT * FROM civicrm_twingle_product WHERE external_id = %1",
[1 => [$external_id, 'String']]);
if ($dao->fetch()) {
$product = new self();
$product->load($dao->toArray());
return $product;
}
return NULL;
}
/**
* Add Twingle Product
*
* @param string $mode
* 'create' or 'edit'
* @return array
* @throws \Civi\Twingle\Shop\Exceptions\ProductException
* @throws \Exception
*/
public function add($mode = 'create') {
$tx = new CRM_Core_Transaction();
// Define mode
$mode = $this->id ? 'edit' : 'create';
// Try to lookup object in database
try {
$dao = CRM_Twingle_BAO_TwingleShop::executeQuery("SELECT * FROM civicrm_twingle_product WHERE external_id = %1",
[1 => [$this->external_id, 'String']]);
if ($dao->fetch()) {
$this->copyValues(array_merge($dao->toArray(), $this->getAttributes()));
}
}
catch (\Civi\Core\Exception\DBQueryException $e) {
throw new ProductException(
E::ts('Could not find TwingleProduct in database: %1', [1 => $e->getMessage()]),
ShopException::ERROR_CODE_COULD_NOT_FIND_SHOP_IN_DB);
}
// Register pre-hook
$twingle_product_values = $this->getAttributes();
try {
\CRM_Utils_Hook::pre($mode, 'TwingleProduct', $this->id, $twingle_product_values);
}
catch (\Exception $e) {
$tx->rollback();
throw $e;
}
$this->load($twingle_product_values);
// Set latest tw_updated_at as new updated_at
$this->updated_at = \CRM_Utils_Time::date('Y-m-d H:i:s', $this->tw_updated_at);
// Convert created_at to date string
$this->created_at = \CRM_Utils_Time::date('Y-m-d H:i:s', $this->created_at);
// Save object to database
try {
$this->save();
}
catch (\Exception $e) {
$tx->rollback();
throw new ProductException(
E::ts('Could not save TwingleProduct to database: %1', [1 => $e->getMessage()]),
ProductException::ERROR_CODE_COULD_NOT_CREATE_PRODUCT);
}
$result = self::findById($this->id);
/** @var self $result */
$this->load($result->getAttributes());
// Register post-hook
$twingle_product_values = $this->getAttributes();
try {
\CRM_Utils_Hook::post($mode, 'TwingleProduct', $this->id, $twingle_product_values);
}
catch (\Exception $e) {
$tx->rollback();
throw $e;
}
$this->load($twingle_product_values);
return $result->toArray();
}
/**
* Delete TwingleProduct along with associated PriceField and PriceFieldValue.
*
* @override \CRM_Twingle_DAO_TwingleProduct::delete
* @throws \CRM_Core_Exception
* @throws \Civi\Twingle\Shop\Exceptions\ProductException
*/
function delete($useWhere = FALSE) {
// Register post-hook
$twingle_product_values = $this->getAttributes();
\CRM_Utils_Hook::pre('delete', 'TwingleProduct', $this->id, $twingle_product_values);
$this->load($twingle_product_values);
// Delete TwingleProduct
parent::delete($useWhere);
// Register post-hook
\CRM_Utils_Hook::post('delete', 'TwingleProduct', $this->id, $instance);
// Free global arrays associated with this object
$this->free();
return true;
}
/**
* Complements the data with the data that was fetched from Twingle.
*
* @throws \Civi\Twingle\Shop\Exceptions\ProductException
*/
public function complementWithDataFromTwingle($product_from_twingle) {
// Complement with data from Twingle
$this->load([
'project_id' => $product_from_twingle['project_id'],
'tw_updated_at' => $product_from_twingle['updated_at'],
'tw_created_at' => $product_from_twingle['created_at'],
]);
}
/**
* Check if the product is outdated.
*
* @param $product_from_twingle
*
* @return void
* @throws \Civi\Twingle\Shop\Exceptions\ProductException
*/
public function checkOutdated($product_from_twingle) {
// Mark outdated products which have a newer timestamp in Twingle
if ($this->updated_at < intval($product_from_twingle['updated_at'])) {
// Overwrite the product with the data from Twingle
$this->load(self::renameTwingleAttrs($product_from_twingle));
$this->is_outdated = TRUE;
}
}
/**
* Compare two products
*
* @param CRM_Twingle_BAO_TwingleProduct $product_to_compare_with
* Product from database
*
* @return bool
*/
public function equals($product_to_compare_with) {
return
$this->name === $product_to_compare_with->name &&
$this->description === $product_to_compare_with->description &&
$this->text === $product_to_compare_with->text &&
$this->price === $product_to_compare_with->price &&
$this->external_id === $product_to_compare_with->external_id;
}
/**
* Returns the financial type id of this product.
*
* @return int|null
* @throws \CRM_Core_Exception
*/
public function getFinancialTypeId(): ?int {
if (!empty($this->price_field_id)) {
$price_set = PriceField::get()
->addSelect('financial_type_id')
->addWhere('id', '=', $this->price_field_id)
->execute()
->first();
return $price_set['financial_type_id'];
}
return NULL;
}
/**
* Returns the price field value id of this product.
*
* @return int|null
* @throws \CRM_Core_Exception
*/
public function getPriceFieldValueId() {
if (!empty($this->price_field_id)) {
$price_field_value = PriceFieldValue::get()
->addSelect('id')
->addWhere('price_field_id', '=', $this->price_field_id)
->execute()
->first();
return $price_field_value['id'];
}
return NULL;
}
/**
* Delete PriceField and PriceFieldValue of this TwingleProduct if they exist.
*
* @return void
* @throws \Civi\Twingle\Shop\Exceptions\ProductException
*/
public function deletePriceField(): void {
// Before we can delete the PriceField we need to delete the associated
// PriceFieldValue
try {
$result = civicrm_api3('PriceFieldValue', 'getsingle',
['price_field_id' => $this->price_field_id]);
}
catch (CRM_Core_Exception $e) {
throw new ProductException(
E::ts('An Error occurred while searching for the associated PriceFieldValue: %1', [1 => $e->getMessage()]),
ProductException::ERROR_CODE_PRICE_FIELD_VALUE_NOT_FOUND);
}
try {
civicrm_api3('PriceFieldValue', 'delete', ['id' => $result['id']]);
}
catch (CRM_Core_Exception $e) {
throw new ProductException(
E::ts('Could not delete associated PriceFieldValue: %1', [1 => $e->getMessage()]),
ProductException::ERROR_CODE_COULD_NOT_DELETE_PRICE_FIELD_VALUE);
}
// Try to delete PriceField
// If no PriceField is found, we assume that it has already been deleted
try {
civicrm_api3('PriceField', 'delete',
['id' => $this->price_field_id]);
}
catch (CRM_Core_Exception $e) {
// Check if PriceField yet exists
try {
$result = civicrm_api3('PriceField', 'get',
['id' => $this->price_field_id]);
// Throw exception if PriceField still exists
if ($result['count'] > 0) {
throw new ProductException(
E::ts('PriceField for this Twingle Product still exists.'),
ProductException::ERROR_CODE_PRICE_FIELD_STILL_EXISTS);
}
}
catch (CRM_Core_Exception $e) {
throw new ProductException(
E::ts('An Error occurred while searching for the associated PriceField: %1', [1 => $e->getMessage()]),
ProductException::ERROR_CODE_PRICE_FIELD_NOT_FOUND);
}
throw new ProductException(
E::ts('Could not delete associated PriceField: %1', [1 => $e->getMessage()]),
ProductException::ERROR_CODE_COULD_NOT_DELETE_PRICE_FIELD);
}
$this->price_field_id = NULL;
}
}

View file

@ -1,475 +0,0 @@
<?php
// phpcs:disable
use CRM_Twingle_ExtensionUtil as E;
use Civi\Twingle\Shop\ApiCall;
use Civi\Twingle\Shop\Exceptions\ShopException;
use Civi\Twingle\Shop\Exceptions\ProductException;
use function Civi\Twingle\Shop\Utils\filter_attributes;
use function Civi\Twingle\Shop\Utils\convert_str_to_int;
use function Civi\Twingle\Shop\Utils\validate_data_types;
// phpcs:enable
require_once E::path() . '/Civi/Twingle/Shop/Utils/TwingleShopUtils.php';
class CRM_Twingle_BAO_TwingleShop extends CRM_Twingle_DAO_TwingleShop {
public const ALLOWED_ATTRIBUTES = [
'id' => \CRM_Utils_Type::T_INT,
'project_identifier' => \CRM_Utils_Type::T_STRING,
'numerical_project_id' => \CRM_Utils_Type::T_INT,
'name' => \CRM_Utils_Type::T_STRING,
'price_set_id' => \CRM_Utils_Type::T_INT,
'financial_type_id' => \CRM_Utils_Type::T_INT,
];
public const STR_TO_INT_CONVERSION = [
'id',
'numerical_project_id',
'price_set_id',
'financial_type_id',
];
/**
* @var array $products
* Array of Twingle Shop products (Cache)
*/
public $products;
/**
* FK to Financial Type
*
* @var int
*/
public $financial_type_id;
/**
* TwingleShop constructor
*/
public function __construct() {
parent::__construct();
// Get TwingleApiCall singleton
$this->twingleApi = ApiCall::singleton();
}
/**
* Get Twingle Shop from database by its project identifier
* (like 'tw620214349ac97')
*
* @param string $project_identifier
* Twingle project identifier
*
* @return CRM_Twingle_BAO_TwingleShop
*
* @throws ShopException
* @throws \Civi\Twingle\Shop\Exceptions\ApiCallError
* @throws \CRM_Core_Exception
*/
public static function findByProjectIdentifier(string $project_identifier) {
$shop = new CRM_Twingle_BAO_TwingleShop();
$shop->get('project_identifier', $project_identifier);
if (!$shop->id) {
$shop->fetchDataFromTwingle($project_identifier);
}
else {
$shop->price_set_id = civicrm_api3('PriceSet', 'getvalue',
['return' => 'id', 'name' => $project_identifier]);
}
return $shop;
}
/**
* Load Twingle Shop data
*
* @param array $shop_data
* Array with shop data
*
* @return void
*
* @throws ShopException
*/
public function load(array $shop_data): void {
// Filter for allowed attributes
filter_attributes($shop_data, self::ALLOWED_ATTRIBUTES);
// Convert string to int
try {
convert_str_to_int($shop_data, self::STR_TO_INT_CONVERSION);
}
catch (Exception $e) {
throw new ShopException($e->getMessage(), ShopException::ERROR_CODE_ATTRIBUTE_WRONG_DATA_TYPE);
}
// Validate data types
try {
validate_data_types($shop_data, self::ALLOWED_ATTRIBUTES);
}
catch (Exception $e) {
throw new ShopException($e->getMessage(), ShopException::ERROR_CODE_ATTRIBUTE_WRONG_DATA_TYPE);
}
// Set attributes
foreach ($shop_data as $key => $value) {
$this->$key = $value;
}
}
/**
* Get attributes
*
* @return array
*/
function getAttributes(): array {
return [
'id' => $this->id,
'project_identifier' => $this->project_identifier,
'numerical_project_id' => $this->numerical_project_id,
'name' => $this->name,
'price_set_id' => $this->price_set_id,
'financial_type_id' => $this->financial_type_id,
];
}
/**
* Add Twingle Shop
*
* @param string $mode
* 'create' or 'edit'
* @return array
* @throws \Civi\Twingle\Shop\Exceptions\ShopException
*/
public function add($mode = 'create') {
// Try to lookup object in database
try {
$dao = self::executeQuery("SELECT * FROM civicrm_twingle_shop WHERE project_identifier = %1",
[1 => [$this->project_identifier, 'String']]);
if ($dao->fetch()) {
$this->load($dao->toArray());
}
}
catch (\Civi\Core\Exception\DBQueryException $e) {
throw new ShopException(
E::ts('Could not find TwingleShop in database: %1', [1 => $e->getMessage()]),
ShopException::ERROR_CODE_COULD_NOT_FIND_SHOP_IN_DB);
}
// Register pre-hook
$twingle_shop_values = $this->getAttributes();
\CRM_Utils_Hook::pre($mode, 'TwingleShop', $this->id, $twingle_shop_values);
$this->load($twingle_shop_values);
// Save object to database
$result = $this->save();
// Register post-hook
\CRM_Utils_Hook::post($mode, 'TwingleShop', $this->id, $instance);
return $result->toArray();
}
/**
* Delete object by deleting the associated PriceSet and letting the foreign
* key constraint do the rest.
*
* @throws \Civi\Twingle\Shop\Exceptions\ShopException*
* @throws \Civi\Twingle\Shop\Exceptions\ProductException
*/
function deleteByConstraint() {
// Register post-hook
$twingle_shop_values = $this->getAttributes();
\CRM_Utils_Hook::pre('delete', 'TwingleShop', $this->id, $twingle_shop_values);
$this->load($twingle_shop_values);
// Delete associated products
$this->deleteProducts();
// Try to get single PriceSet
try {
civicrm_api3('PriceSet', 'getsingle',
['id' => $this->price_set_id]);
}
catch (\CRM_Core_Exception $e) {
if ($e->getMessage() != 'Expected one PriceSet but found 0') {
throw new ShopException(
E::ts('Could not find associated PriceSet: %1', [1 => $e->getMessage()]),
ShopException::ERROR_CODE_PRICE_SET_NOT_FOUND);
}
else {
// If no PriceSet is found, we can simply delete the TwingleShop
return $this->delete();
}
}
// Deleting the associated PriceSet will also lead to the deletion of this
// TwingleShop because of the foreign key constraint and cascading.
try {
$result = civicrm_api3('PriceSet', 'delete',
['id' => $this->price_set_id]);
} catch (\CRM_Core_Exception $e) {
throw new ShopException(
E::ts('Could not delete associated PriceSet: %1', [1 => $e->getMessage()]),
ShopException::ERROR_CODE_COULD_NOT_DELETE_PRICE_SET);
}
// Register post-hook
\CRM_Utils_Hook::post('delete', 'TwingleShop', $this->id, $instance);
// Free global arrays associated with this object
$this->free();
return $result['is_error'] == 0;
}
/**
* Fetch Twingle Shop products from Twingle
*
* @return array
* array of CRM_Twingle_Shop_BAO_Product
*
* @throws \Civi\Twingle\Shop\Exceptions\ApiCallError;
* @throws \Civi\Twingle\Shop\Exceptions\ProductException;
* @throws \Civi\Core\Exception\DBQueryException
* @throws \CRM_Core_Exception
*/
public function fetchProducts(): array {
// Establish connection, if not already connected
if (!$this->twingleApi->isConnected) {
$this->twingleApi->connect();
}
// Fetch products from Twingle API
$products_from_twingle = $this->twingleApi->get(
'project',
$this->numerical_project_id,
'products',
);
// Fetch products from database
if ($this->id) {
$products_from_db = $this->getProducts();
$products_from_twingle = array_reduce($products_from_twingle, function($carry, $product) {
$carry[$product['id']] = $product;
return $carry;
}, []);
foreach ($products_from_db as $product) {
/* @var CRM_Twingle_BAO_TwingleProduct $product */
// Find orphaned products which are in the database but not in Twingle
$found = array_key_exists($product->external_id, $products_from_twingle);
if (!$found) {
$product->is_orphaned = TRUE;
}
else {
// Complement with data from Twingle
$product->complementWithDataFromTwingle($products_from_twingle[$product->external_id]);
// Mark outdated products which have a newer version in Twingle
$product->checkOutdated($products_from_twingle[$product->external_id]);
}
$this->products[] = $product;
}
}
// Create array with external_id as key
$products = array_reduce($this->products ?? [], function($carry, $product) {
$carry[$product->external_id] = $product;
return $carry;
}, []);
// Add new products from Twingle
foreach ($products_from_twingle as $product_from_twingle) {
$found = array_key_exists($product_from_twingle['id'], $products);
if (!$found) {
$product = new CRM_Twingle_BAO_TwingleProduct();
$product->load(CRM_Twingle_BAO_TwingleProduct::renameTwingleAttrs($product_from_twingle));
$product->twingle_shop_id = $this->id;
$this->products[] = $product;
}
}
return $this->products;
}
/**
* Get associated products.
*
* @return list<CRM_Twingle_BAO_TwingleProduct>
* @throws \Civi\Core\Exception\DBQueryException
* @throws \Civi\Twingle\Shop\Exceptions\ProductException
*/
public function getProducts() {
$products = [];
$result = CRM_Twingle_BAO_TwingleProduct::executeQuery(
"SELECT * FROM civicrm_twingle_product WHERE twingle_shop_id = %1",
[1 => [$this->id, 'Integer']]
);
while ($result->fetch()) {
$product = new CRM_Twingle_BAO_TwingleProduct();
$product->load($result->toArray());
$products[] = $product;
}
return $products;
}
/**
* Creates Twingle Shop as a price set in CiviCRM.
*
* @param string $mode
* 'create' or 'edit'
* @throws \Civi\Twingle\Shop\Exceptions\ShopException
*/
public function createPriceSet($mode = 'create') {
// Define mode
$mode = $this->price_set_id ? 'edit' : 'create';
// Check if PriceSet for this Shop already exists
try {
$price_set = civicrm_api3('PriceSet', 'get', [
'name' => $this->project_identifier,
]);
if ($price_set['count'] > 0 && $mode == 'create') {
throw new ShopException(
E::ts('PriceSet for this Twingle Shop already exists.'),
ShopException::ERROR_CODE_PRICE_SET_ALREADY_EXISTS,
);
}
elseif ($price_set['count'] == 0 && $mode == 'edit') {
throw new ShopException(
E::ts('PriceSet for this Twingle Shop does not exist and cannot be edited.'),
ShopException::ERROR_CODE_PRICE_SET_NOT_FOUND,
);
}
} catch (\CRM_Core_Exception $e) {
throw new ShopException(
E::ts('Could not check if PriceSet for this TwingleShop already exists.'),
ShopException::ERROR_CODE_PRICE_SET_NOT_FOUND,
);
}
// Create PriceSet
$price_set_data = [
'name' => $this->project_identifier,
'title' => "$this->name ($this->project_identifier)",
'is_active' => 1,
'extends' => 2,
'financial_type_id' => $this->financial_type_id,
];
// Set id if in edit mode
if ($mode == 'edit') {
$price_set_data['id'] = $this->price_set_id;
}
try {
$price_set = civicrm_api4('PriceSet', 'create',
['values' => $price_set_data])->first();
$this->price_set_id = (int) $price_set['id'];
} catch (\CRM_Core_Exception $e) {
throw new ShopException(
E::ts('Could not create PriceSet for this TwingleShop.'),
ShopException::ERROR_CODE_COULD_NOT_CREATE_PRICE_SET,
);
}
}
/**
* Retrieves the numerical project ID and the name of this shop from Twingle.
*
* @throws \Civi\Twingle\Shop\Exceptions\ShopException
* @throws \Civi\Twingle\Shop\Exceptions\ApiCallError
*/
private function fetchDataFromTwingle() {
// Establish connection, if not already connected
if (!$this->twingleApi->isConnected) {
$this->twingleApi->connect();
}
// Get shops from Twingle if not cached
$shops = \Civi::cache('long')->get('twingle_shops');
if (empty($shops)) {
$this::fetchShops($this->twingleApi);
$shops = \Civi::cache('long')->get('twingle_shops');
}
// Set Shop ID and name
foreach ($shops as $shop) {
if (isset($shop['identifier']) && $shop['identifier'] == $this->project_identifier) {
$this->numerical_project_id = $shop['id'];
$this->name = $shop['name'];
}
}
// Throw an Exception if this Twingle Project is not of type 'shop'
if (!isset($this->numerical_project_id)) {
throw new ShopException(
E::ts('This Twingle Project is not a shop.'),
ShopException::ERROR_CODE_NOT_A_SHOP,
);
}
}
/**
* Retrieves all Twingle projects of the type 'shop'.
*
* @throws \Civi\Twingle\Shop\Exceptions\ShopException
*/
static private function fetchShops(ApiCall $api): void {
$organisationId = $api->organisationId;
try {
$projects = $api->get(
'project',
NULL,
'by-organisation',
$organisationId,
);
$shops = array_filter(
$projects,
function($project) {
return isset($project['type']) && $project['type'] == 'shop';
}
);
\Civi::cache('long')->set('twingle_shops', $shops);
}
catch (Exception $e) {
throw new ShopException(
E::ts('Could not retrieve Twingle projects from API.
Please check your API credentials.'),
ShopException::ERROR_CODE_COULD_NOT_GET_PROJECTS,
);
}
}
/**
* Deletes all associated products.
*
* @return void
* @throws \Civi\Twingle\Shop\Exceptions\ProductException
*/
public function deleteProducts() {
try {
$products = $this->getProducts();
}
catch (\Civi\Core\Exception\DBQueryException $e) {
throw new ProductException(
E::ts('Could not retrieve associated products: %1', [1 => $e->getMessage()]),
ProductException::ERROR_CODE_COULD_NOT_GET_PRODUCTS
);
}
try {
foreach ($products as $product) {
$product->delete();
}
}
catch (ProductException $e) {
throw new ProductException(
E::ts('Could not delete associated products: %1', [1 => $e->getMessage()]),
ProductException::ERROR_CODE_COULD_NOT_DELETE_PRICE_SET,
);
}
}
}

View file

@ -13,28 +13,25 @@
| written permission from the original author(s). |
+-------------------------------------------------------------*/
declare(strict_types = 1);
use CRM_Twingle_ExtensionUtil as E;
class CRM_Twingle_Config {
public const RCUR_PROTECTION_OFF = 0;
public const RCUR_PROTECTION_EXCEPTION = 1;
public const RCUR_PROTECTION_ACTIVITY = 2;
const RCUR_PROTECTION_OFF = 0;
const RCUR_PROTECTION_EXCEPTION = 1;
const RCUR_PROTECTION_ACTIVITY = 2;
/**
* Get the options for protecting a recurring contribution linked Twingle
* against ending or cancellation (because Twingle would keep on collecting them)
*
* @return array<int, string>
* @return array
*/
public static function getRecurringProtectionOptions() {
return [
self::RCUR_PROTECTION_OFF => E::ts('No'),
self::RCUR_PROTECTION_EXCEPTION => E::ts('Raise Exception'),
self::RCUR_PROTECTION_ACTIVITY => E::ts('Create Activity'),
self::RCUR_PROTECTION_OFF => E::ts("No"),
self::RCUR_PROTECTION_EXCEPTION => E::ts("Raise Exception"),
self::RCUR_PROTECTION_ACTIVITY => E::ts("Create Activity"),
];
}
}

View file

@ -1,325 +0,0 @@
<?php
/**
* @package CRM
* @copyright CiviCRM LLC https://civicrm.org/licensing
*
* Generated from de.systopia.twingle/xml/schema/CRM/Twingle/TwingleProduct.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
* (GenCodeChecksum:b8ced533ba6f4249619ffe5353965a4f)
*/
use CRM_Twingle_ExtensionUtil as E;
/**
* Database access object for the TwingleProduct entity.
*/
class CRM_Twingle_DAO_TwingleProduct extends CRM_Core_DAO {
const EXT = E::LONG_NAME;
const TABLE_ADDED = '';
/**
* Static instance to hold the table name.
*
* @var string
*/
public static $_tableName = 'civicrm_twingle_product';
/**
* Should CiviCRM log any modifications to this table in the civicrm_log table.
*
* @var bool
*/
public static $_log = FALSE;
/**
* Unique TwingleProduct ID
*
* @var int|string|null
* (SQL type: int unsigned)
* Note that values will be retrieved from the database as a string.
*/
public $id;
/**
* The ID of this product in the Twingle database
*
* @var int|string
* (SQL type: int unsigned)
* Note that values will be retrieved from the database as a string.
*/
public $external_id;
/**
* FK to Price Field
*
* @var int|string
* (SQL type: int unsigned)
* Note that values will be retrieved from the database as a string.
*/
public $price_field_id;
/**
* FK to Twingle Shop
*
* @var int|string|null
* (SQL type: int unsigned)
* Note that values will be retrieved from the database as a string.
*/
public $twingle_shop_id;
/**
* Timestamp of when the product was created in the database
*
* @var string
* (SQL type: datetime)
* Note that values will be retrieved from the database as a string.
*/
public $created_at;
/**
* Timestamp of when the product was last updated in the database
*
* @var string
* (SQL type: datetime)
* Note that values will be retrieved from the database as a string.
*/
public $updated_at;
/**
* Class constructor.
*/
public function __construct() {
$this->__table = 'civicrm_twingle_product';
parent::__construct();
}
/**
* Returns localized title of this entity.
*
* @param bool $plural
* Whether to return the plural version of the title.
*/
public static function getEntityTitle($plural = FALSE) {
return $plural ? E::ts('Twingle Products') : E::ts('Twingle Product');
}
/**
* Returns foreign keys and entity references.
*
* @return array
* [CRM_Core_Reference_Interface]
*/
public static function getReferenceColumns() {
if (!isset(\Civi::$statics[__CLASS__]['links'])) {
\Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__);
\Civi::$statics[__CLASS__]['links'][] = new \CRM_Core_Reference_Basic(self::getTableName(), 'price_field_id', 'civicrm_contact', 'id');
\Civi::$statics[__CLASS__]['links'][] = new \CRM_Core_Reference_Basic(self::getTableName(), 'twingle_shop_id', 'civicrm_twingle_shop', 'id');
\CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', \Civi::$statics[__CLASS__]['links']);
}
return \Civi::$statics[__CLASS__]['links'];
}
/**
* Returns all the column names of this table
*
* @return array
*/
public static function &fields() {
if (!isset(\Civi::$statics[__CLASS__]['fields'])) {
\Civi::$statics[__CLASS__]['fields'] = [
'id' => [
'name' => 'id',
'type' => \CRM_Utils_Type::T_INT,
'title' => E::ts('ID'),
'description' => E::ts('Unique TwingleProduct ID'),
'required' => TRUE,
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_twingle_product.id',
'table_name' => 'civicrm_twingle_product',
'entity' => 'TwingleProduct',
'bao' => 'CRM_Twingle_DAO_TwingleProduct',
'localizable' => 0,
'html' => [
'type' => 'Number',
],
'readonly' => TRUE,
'add' => NULL,
],
'external_id' => [
'name' => 'external_id',
'type' => \CRM_Utils_Type::T_INT,
'title' => E::ts('External ID'),
'description' => E::ts('The ID of this product in the Twingle database'),
'required' => TRUE,
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_twingle_product.external_id',
'table_name' => 'civicrm_twingle_product',
'entity' => 'TwingleProduct',
'bao' => 'CRM_Twingle_DAO_TwingleProduct',
'localizable' => 0,
'html' => [
'type' => 'Number',
],
'add' => NULL,
],
'price_field_id' => [
'name' => 'price_field_id',
'type' => \CRM_Utils_Type::T_INT,
'title' => E::ts('Price Field ID'),
'description' => E::ts('FK to Price Field'),
'required' => TRUE,
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_twingle_product.price_field_id',
'table_name' => 'civicrm_twingle_product',
'entity' => 'TwingleProduct',
'bao' => 'CRM_Twingle_DAO_TwingleProduct',
'localizable' => 0,
'FKClassName' => 'CRM_Contact_DAO_Contact',
'add' => NULL,
],
'twingle_shop_id' => [
'name' => 'twingle_shop_id',
'type' => \CRM_Utils_Type::T_INT,
'title' => E::ts('Twingle Shop ID'),
'description' => E::ts('FK to Twingle Shop'),
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_twingle_product.twingle_shop_id',
'table_name' => 'civicrm_twingle_product',
'entity' => 'TwingleProduct',
'bao' => 'CRM_Twingle_DAO_TwingleProduct',
'localizable' => 0,
'FKClassName' => 'CRM_Twingle_DAO_TwingleShop',
'add' => NULL,
],
'created_at' => [
'name' => 'created_at',
'type' => \CRM_Utils_Type::T_DATE + \CRM_Utils_Type::T_TIME,
'title' => E::ts('Created At'),
'description' => E::ts('Timestamp of when the product was created in the database'),
'required' => TRUE,
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_twingle_product.created_at',
'table_name' => 'civicrm_twingle_product',
'entity' => 'TwingleProduct',
'bao' => 'CRM_Twingle_DAO_TwingleProduct',
'localizable' => 0,
'add' => NULL,
],
'updated_at' => [
'name' => 'updated_at',
'type' => \CRM_Utils_Type::T_DATE + \CRM_Utils_Type::T_TIME,
'title' => E::ts('Updated At'),
'description' => E::ts('Timestamp of when the product was last updated in the database'),
'required' => TRUE,
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_twingle_product.updated_at',
'table_name' => 'civicrm_twingle_product',
'entity' => 'TwingleProduct',
'bao' => 'CRM_Twingle_DAO_TwingleProduct',
'localizable' => 0,
'add' => NULL,
],
];
\CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', \Civi::$statics[__CLASS__]['fields']);
}
return \Civi::$statics[__CLASS__]['fields'];
}
/**
* Return a mapping from field-name to the corresponding key (as used in fields()).
*
* @return array
* Array(string $name => string $uniqueName).
*/
public static function &fieldKeys() {
if (!isset(\Civi::$statics[__CLASS__]['fieldKeys'])) {
\Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(\CRM_Utils_Array::collect('name', self::fields()));
}
return \Civi::$statics[__CLASS__]['fieldKeys'];
}
/**
* Returns the names of this table
*
* @return string
*/
public static function getTableName() {
return self::$_tableName;
}
/**
* Returns if this table needs to be logged
*
* @return bool
*/
public function getLog() {
return self::$_log;
}
/**
* Returns the list of fields that can be imported
*
* @param bool $prefix
*
* @return array
*/
public static function &import($prefix = FALSE) {
$r = \CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'twingle_product', $prefix, []);
return $r;
}
/**
* Returns the list of fields that can be exported
*
* @param bool $prefix
*
* @return array
*/
public static function &export($prefix = FALSE) {
$r = \CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'twingle_product', $prefix, []);
return $r;
}
/**
* Returns the list of indices
*
* @param bool $localize
*
* @return array
*/
public static function indices($localize = TRUE) {
$indices = [];
return ($localize && !empty($indices)) ? \CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices;
}
}

View file

@ -1,305 +0,0 @@
<?php
/**
* @package CRM
* @copyright CiviCRM LLC https://civicrm.org/licensing
*
* Generated from de.systopia.twingle/xml/schema/CRM/Twingle/TwingleShop.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
* (GenCodeChecksum:95a46935c30b3da52e4df2bec871b962)
*/
use CRM_Twingle_ExtensionUtil as E;
/**
* Database access object for the TwingleShop entity.
*/
class CRM_Twingle_DAO_TwingleShop extends CRM_Core_DAO {
const EXT = E::LONG_NAME;
const TABLE_ADDED = '';
/**
* Static instance to hold the table name.
*
* @var string
*/
public static $_tableName = 'civicrm_twingle_shop';
/**
* Should CiviCRM log any modifications to this table in the civicrm_log table.
*
* @var bool
*/
public static $_log = FALSE;
/**
* Unique TwingleShop ID
*
* @var int|string|null
* (SQL type: int unsigned)
* Note that values will be retrieved from the database as a string.
*/
public $id;
/**
* Twingle Project Identifier
*
* @var string
* (SQL type: varchar(32))
* Note that values will be retrieved from the database as a string.
*/
public $project_identifier;
/**
* Numerical Twingle Project Identifier
*
* @var int|string
* (SQL type: int unsigned)
* Note that values will be retrieved from the database as a string.
*/
public $numerical_project_id;
/**
* FK to Price Set
*
* @var int|string|null
* (SQL type: int unsigned)
* Note that values will be retrieved from the database as a string.
*/
public $price_set_id;
/**
* name of the shop
*
* @var string
* (SQL type: varchar(64))
* Note that values will be retrieved from the database as a string.
*/
public $name;
/**
* Class constructor.
*/
public function __construct() {
$this->__table = 'civicrm_twingle_shop';
parent::__construct();
}
/**
* Returns localized title of this entity.
*
* @param bool $plural
* Whether to return the plural version of the title.
*/
public static function getEntityTitle($plural = FALSE) {
return $plural ? E::ts('Twingle Shops') : E::ts('Twingle Shop');
}
/**
* Returns foreign keys and entity references.
*
* @return array
* [CRM_Core_Reference_Interface]
*/
public static function getReferenceColumns() {
if (!isset(\Civi::$statics[__CLASS__]['links'])) {
\Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__);
\Civi::$statics[__CLASS__]['links'][] = new \CRM_Core_Reference_Basic(self::getTableName(), 'price_set_id', 'civicrm_price_set', 'id');
\CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', \Civi::$statics[__CLASS__]['links']);
}
return \Civi::$statics[__CLASS__]['links'];
}
/**
* Returns all the column names of this table
*
* @return array
*/
public static function &fields() {
if (!isset(\Civi::$statics[__CLASS__]['fields'])) {
\Civi::$statics[__CLASS__]['fields'] = [
'id' => [
'name' => 'id',
'type' => \CRM_Utils_Type::T_INT,
'title' => E::ts('ID'),
'description' => E::ts('Unique TwingleShop ID'),
'required' => TRUE,
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_twingle_shop.id',
'table_name' => 'civicrm_twingle_shop',
'entity' => 'TwingleShop',
'bao' => 'CRM_Twingle_DAO_TwingleShop',
'localizable' => 0,
'html' => [
'type' => 'Number',
],
'readonly' => TRUE,
'add' => NULL,
],
'project_identifier' => [
'name' => 'project_identifier',
'type' => \CRM_Utils_Type::T_STRING,
'title' => E::ts('Project Identifier'),
'description' => E::ts('Twingle Project Identifier'),
'required' => TRUE,
'maxlength' => 32,
'size' => \CRM_Utils_Type::MEDIUM,
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_twingle_shop.project_identifier',
'table_name' => 'civicrm_twingle_shop',
'entity' => 'TwingleShop',
'bao' => 'CRM_Twingle_DAO_TwingleShop',
'localizable' => 0,
'html' => [
'type' => 'Text',
],
'add' => NULL,
],
'numerical_project_id' => [
'name' => 'numerical_project_id',
'type' => \CRM_Utils_Type::T_INT,
'title' => E::ts('Numerical Project ID'),
'description' => E::ts('Numerical Twingle Project Identifier'),
'required' => TRUE,
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_twingle_shop.numerical_project_id',
'table_name' => 'civicrm_twingle_shop',
'entity' => 'TwingleShop',
'bao' => 'CRM_Twingle_DAO_TwingleShop',
'localizable' => 0,
'html' => [
'type' => 'Number',
],
'add' => NULL,
],
'price_set_id' => [
'name' => 'price_set_id',
'type' => \CRM_Utils_Type::T_INT,
'title' => E::ts('Price Set ID'),
'description' => E::ts('FK to Price Set'),
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_twingle_shop.price_set_id',
'table_name' => 'civicrm_twingle_shop',
'entity' => 'TwingleShop',
'bao' => 'CRM_Twingle_DAO_TwingleShop',
'localizable' => 0,
'FKClassName' => 'CRM_Price_DAO_PriceSet',
'add' => NULL,
],
'name' => [
'name' => 'name',
'type' => \CRM_Utils_Type::T_STRING,
'title' => E::ts('Name'),
'description' => E::ts('name of the shop'),
'required' => TRUE,
'maxlength' => 64,
'size' => \CRM_Utils_Type::BIG,
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_twingle_shop.name',
'table_name' => 'civicrm_twingle_shop',
'entity' => 'TwingleShop',
'bao' => 'CRM_Twingle_Shop_DAO_TwingleShop',
'localizable' => 0,
'html' => [
'type' => 'Text',
],
'add' => NULL,
],
];
\CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', \Civi::$statics[__CLASS__]['fields']);
}
return \Civi::$statics[__CLASS__]['fields'];
}
/**
* Return a mapping from field-name to the corresponding key (as used in fields()).
*
* @return array
* Array(string $name => string $uniqueName).
*/
public static function &fieldKeys() {
if (!isset(\Civi::$statics[__CLASS__]['fieldKeys'])) {
\Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(\CRM_Utils_Array::collect('name', self::fields()));
}
return \Civi::$statics[__CLASS__]['fieldKeys'];
}
/**
* Returns the names of this table
*
* @return string
*/
public static function getTableName() {
return self::$_tableName;
}
/**
* Returns if this table needs to be logged
*
* @return bool
*/
public function getLog() {
return self::$_log;
}
/**
* Returns the list of fields that can be imported
*
* @param bool $prefix
*
* @return array
*/
public static function &import($prefix = FALSE) {
$r = \CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'twingle_shop', $prefix, []);
return $r;
}
/**
* Returns the list of fields that can be exported
*
* @param bool $prefix
*
* @return array
*/
public static function &export($prefix = FALSE) {
$r = \CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'twingle_shop', $prefix, []);
return $r;
}
/**
* Returns the list of indices
*
* @param bool $localize
*
* @return array
*/
public static function indices($localize = TRUE) {
$indices = [];
return ($localize && !empty($indices)) ? \CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices;
}
}

File diff suppressed because it is too large Load diff

View file

@ -13,8 +13,6 @@
| written permission from the original author(s). |
+-------------------------------------------------------------*/
declare(strict_types = 1);
use CRM_Twingle_ExtensionUtil as E;
/**
@ -25,72 +23,69 @@ use CRM_Twingle_ExtensionUtil as E;
class CRM_Twingle_Form_Settings extends CRM_Core_Form {
/**
* @var array<string>
* List of all settings options.
* @var array list of all settings options
*/
public static $SETTINGS_LIST = [
'twingle_prefix',
'twingle_use_sepa',
'twingle_dont_use_reference',
'twingle_protect_recurring',
'twingle_protect_recurring_activity_type',
'twingle_protect_recurring_activity_subject',
'twingle_protect_recurring_activity_status',
'twingle_protect_recurring_activity_assignee',
'twingle_use_shop',
'twingle_access_key',
'twingle_prefix',
'twingle_use_sepa',
'twingle_dont_use_reference',
'twingle_protect_recurring',
'twingle_protect_recurring_activity_type',
'twingle_protect_recurring_activity_subject',
'twingle_protect_recurring_activity_status',
'twingle_protect_recurring_activity_assignee',
];
/**
* @inheritdoc
*/
public function buildQuickForm(): void {
function buildQuickForm() {
// Set redirect destination.
$this->controller->_destination = CRM_Utils_System::url('civicrm/admin/settings/twingle', 'reset=1');
$this->add(
'text',
'twingle_prefix',
E::ts('Twingle ID Prefix')
E::ts("Twingle ID Prefix")
);
$this->add(
'checkbox',
'twingle_use_sepa',
E::ts('Use CiviSEPA')
E::ts("Use CiviSEPA")
);
$this->add(
'checkbox',
'twingle_dont_use_reference',
E::ts('Use CiviSEPA generated reference')
E::ts("Use CiviSEPA generated reference")
);
$this->add(
'select',
'twingle_protect_recurring',
E::ts('Protect Recurring Contributions'),
E::ts("Protect Recurring Contributions"),
CRM_Twingle_Config::getRecurringProtectionOptions()
);
$this->add(
'select',
'twingle_protect_recurring_activity_type',
E::ts('Activity Type'),
E::ts("Activity Type"),
$this->getOptionValueList('activity_type', [0])
);
$this->add(
'text',
'twingle_protect_recurring_activity_subject',
E::ts('Subject'),
E::ts("Subject"),
['class' => 'huge']
);
$this->add(
'select',
'twingle_protect_recurring_activity_status',
E::ts('Status'),
E::ts("Status"),
$this->getOptionValueList('activity_status')
);
@ -107,30 +102,18 @@ class CRM_Twingle_Form_Settings extends CRM_Core_Form {
]
);
$this->add(
'checkbox',
'twingle_use_shop',
E::ts('Use Twingle Shop Integration')
);
$this->add(
'text',
'twingle_access_key',
E::ts('Twingle Access Key')
);
$this->addButtons([
[
'type' => 'submit',
'name' => E::ts('Save'),
'isDefault' => TRUE,
],
]);
$this->addButtons(array(
array (
'type' => 'submit',
'name' => E::ts('Save'),
'isDefault' => TRUE,
)
));
// set defaults
foreach (self::$SETTINGS_LIST as $setting) {
$this->setDefaults([
$setting => Civi::settings()->get($setting),
$setting => Civi::settings()->get($setting)
]);
}
@ -138,73 +121,64 @@ class CRM_Twingle_Form_Settings extends CRM_Core_Form {
}
/**
* Custom form validation, as some fields are mandatory only when others are active.
* Custom form validation, because the activity creation fields
* are only mandatory if activity creation is active
* @return bool
*/
public function validate() {
parent::validate();
// if activity creation is active, make sure the fields are set
$protection_mode = $this->_submitValues['twingle_protect_recurring'] ?? NULL;
$protection_mode = CRM_Utils_Array::value('twingle_protect_recurring', $this->_submitValues);
if ($protection_mode == CRM_Twingle_Config::RCUR_PROTECTION_ACTIVITY) {
foreach ([
'twingle_protect_recurring_activity_type',
'twingle_protect_recurring_activity_subject',
'twingle_protect_recurring_activity_status',
'twingle_protect_recurring_activity_assignee',
] as $activity_field) {
if (NULL !== ($this->_submitValues[$activity_field] ?? NULL)) {
$this->_errors[$activity_field] = E::ts('This is required for activity creation');
foreach (['twingle_protect_recurring_activity_type',
'twingle_protect_recurring_activity_subject',
'twingle_protect_recurring_activity_status',
'twingle_protect_recurring_activity_assignee',] as $activity_field) {
$current_value = CRM_Utils_Array::value($activity_field, $this->_submitValues);
if (empty($current_value)) {
$this->_errors[$activity_field] = E::ts("This is required for activity creation");
}
}
}
// Twingle Access Key is required if Shop Integration is enabled
if (
CRM_Utils_Array::value('twingle_use_shop', $this->_submitValues) &&
!CRM_Utils_Array::value('twingle_access_key', $this->_submitValues, FALSE)
) {
$this->_errors['twingle_access_key'] = E::ts('An Access Key is required to enable Twingle Shop Integration');
}
return (0 == count($this->_errors));
}
/**
* @inheritdoc
*/
public function postProcess(): void {
function postProcess() {
$values = $this->exportValues();
// store settings
foreach (self::$SETTINGS_LIST as $setting) {
Civi::settings()->set($setting, $values[$setting] ?? NULL);
Civi::settings()->set($setting, CRM_Utils_Array::value($setting, $values));
}
parent::postProcess();
}
/**
* Get a list of option group items
* @param string $group_id
* Group ID or name.
* @param array<int> $reserved
* @return array<int|string, string> list of ID(value) => label
* @throws \CRM_Core_Exception
* @param $group_id string group ID or name
* @return array list of ID(value) => label
* @throws CiviCRM_API3_Exception
*/
protected function getOptionValueList(string $group_id, array $reserved = [0, 1]): array {
$list = ['' => E::ts('-select-')];
protected function getOptionValueList($group_id, $reserved = [0,1]) {
$list = ['' => E::ts("-select-")];
$query = civicrm_api3('OptionValue', 'get', [
'option_group_id' => $group_id,
'option.limit' => 0,
'is_active' => 1,
'is_reserved' => ['IN' => $reserved],
'return' => 'value,label',
'option_group_id' => $group_id,
'option.limit' => 0,
'is_active' => 1,
'is_reserved' => ['IN' => $reserved],
'return' => 'value,label',
]);
foreach ($query['values'] as $value) {
$list[$value['value']] = $value['label'];
}
return $list;
}
}

View file

@ -13,13 +13,11 @@
| written permission from the original author(s). |
+-------------------------------------------------------------*/
declare(strict_types = 1);
use CRM_Twingle_ExtensionUtil as E;
class CRM_Twingle_Page_Configuration extends CRM_Core_Page {
public function run(): void {
public function run() {
parent::run();
}

View file

@ -13,30 +13,21 @@
| written permission from the original author(s). |
+-------------------------------------------------------------*/
declare(strict_types = 1);
use CRM_Twingle_ExtensionUtil as E;
class CRM_Twingle_Page_Profiles extends CRM_Core_Page {
public function run():void {
CRM_Utils_System::setTitle(E::ts('Twingle API Profiles'));
public function run() {
CRM_Utils_System::setTitle(E::ts("Twingle API Profiles"));
$profiles = [];
foreach (CRM_Twingle_Profile::getProfiles() as $profile_id => $profile) {
$profiles[$profile_id]['id'] = $profile_id;
$profiles[$profile_id]['name'] = $profile->getName();
$profiles[$profile_id]['is_default'] = $profile->is_default();
$profiles[$profile_id]['selectors'] = $profile->getProjectIds();
foreach (CRM_Twingle_Profile::getProfiles() as $profile_name => $profile) {
$profiles[$profile_name]['name'] = $profile_name;
foreach (CRM_Twingle_Profile::allowedAttributes() as $attribute) {
$profiles[$profile_id][$attribute] = $profile->getAttribute($attribute);
$profiles[$profile_name][$attribute] = $profile->getAttribute($attribute);
}
}
$this->assign('profiles', $profiles);
$this->assign('profile_stats', CRM_Twingle_Profile::getProfileStats());
$this->assign('twingle_use_shop', (int) Civi::settings()->get('twingle_use_shop'));
// Add custom css
Civi::resources()->addStyleFile(E::LONG_NAME, 'css/twingle.css');
parent::run();
}

View file

@ -13,11 +13,7 @@
| written permission from the original author(s). |
+-------------------------------------------------------------*/
declare(strict_types = 1);
use CRM_Twingle_ExtensionUtil as E;
use Civi\Twingle\Exceptions\ProfileException as ProfileException;
use Civi\Twingle\Exceptions\ProfileValidationError;
/**
* Profiles define how incoming submissions from the Twingle API are
@ -26,44 +22,26 @@ use Civi\Twingle\Exceptions\ProfileValidationError;
class CRM_Twingle_Profile {
/**
* @var int
* The id of the profile.
*/
protected ?int $id;
/**
* @var string
* @var string $name
* The name of the profile.
*/
protected string $name;
protected $name = NULL;
/**
* @var array<string, mixed>
* @var array $data
* The properties of the profile.
*/
protected $data;
/**
* @var array $check_box_fields
* List of check box fields
*/
public $check_box_fields = [
'newsletter_double_opt_in',
'enable_shop_integration',
'shop_map_products',
];
protected $data = NULL;
/**
* CRM_Twingle_Profile constructor.
*
* @param string $name
* The name of the profile.
* @param array<string, mixed> $data
* @param array $data
* The properties of the profile
* @param int|NULL $id
*/
public function __construct($name, $data, $id = NULL) {
$this->id = $id;
public function __construct($name, $data) {
$this->name = $name;
$allowed_attributes = self::allowedAttributes();
$this->data = $data + array_combine(
@ -75,65 +53,43 @@ class CRM_Twingle_Profile {
/**
* Logs (production) access to this profile
*/
public function logAccess(): void {
CRM_Core_DAO::executeQuery('
public function logAccess() {
CRM_Core_DAO::executeQuery("
UPDATE civicrm_twingle_profile
SET
last_access = NOW(),
access_counter = access_counter + 1
WHERE name = %1', [1 => [$this->name, 'String']]);
}
/**
* Copy this profile by returning a clone with all unique information removed.
*
* @return CRM_Twingle_Profile
*/
public function copy() {
$copy = clone $this;
// Remove unique data
$copy->id = NULL;
$copy->data['selector'] = NULL;
// Propose a new name for this profile.
$profile_name = $this->getName() . '_copy';
$copy->setName($profile_name);
return $copy;
WHERE name = %1", [1 => [$this->name, 'String']]);
}
/**
* Checks whether the profile's selector matches the given project ID.
*
* @param string|int $project_id
* @param string | int $project_id
*
* @return bool
*/
public function matches($project_id) {
return in_array($project_id, $this->getProjectIds(), TRUE);
$selector = $this->getAttribute('selector');
$project_ids = array_map(
function($project_id) {
return trim($project_id);
},
explode(',', $selector)
);
return in_array($project_id, $project_ids);
}
/**
* Retrieves the profile's configured custom field mapping.
*
* @return array<string, string>
* @return array
* The profile's configured custom field mapping
*/
public function getCustomFieldMapping() {
$custom_field_mapping = [];
if ('' !== ($custom_field_definition = $this->getAttribute('custom_field_mapping', ''))) {
/** @var string $custom_field_definition */
$custom_field_maps = preg_split(
'/\r\n|\r|\n/',
$custom_field_definition,
-1,
PREG_SPLIT_NO_EMPTY
);
if (FALSE !== $custom_field_maps) {
foreach ($custom_field_maps as $custom_field_map) {
[$twingle_field_name, $custom_field_name] = explode('=', $custom_field_map);
$custom_field_mapping[$twingle_field_name] = $custom_field_name;
}
if (!empty($custom_field_definition = $this->getAttribute('custom_field_mapping'))) {
foreach (preg_split('/\r\n|\r|\n/', $custom_field_definition, -1, PREG_SPLIT_NO_EMPTY) as $custom_field_map) {
[$twingle_field_name, $custom_field_name] = explode("=", $custom_field_map);
$custom_field_mapping[$twingle_field_name] = $custom_field_name;
}
}
return $custom_field_mapping;
@ -142,81 +98,30 @@ class CRM_Twingle_Profile {
/**
* Retrieves all data attributes of the profile.
*
* @return array<string, mixed>
* @return array
*/
public function getData() {
return $this->data;
}
/**
* Retrieves the profile id.
*
* @return int
*/
public function getId(): ?int {
return $this->id;
}
/**
* Set the profile id.
*
* @param int $id
*/
public function setId(int $id): void {
$this->id = $id;
}
/**
* Retrieves the profile name.
*
* @return string
*/
public function getName(): string {
public function getName() {
return $this->name;
}
/**
* Sets the profile name.
*
* @param string $name
* @param $name
*/
public function setName(string $name): void {
public function setName($name) {
$this->name = $name;
}
/**
* Is this the default profile?
*
* @return bool
*/
public function is_default(): bool {
return $this->name === 'default';
}
/**
* Retrieves the profile's project IDs.
*
* @return array<string>
*/
public function getProjectIds(): array {
return array_map(
function($project_id) {
return trim($project_id);
},
explode(',', $this->getAttribute('selector') ?? '')
);
}
/**
* Determine if Twingle Shop integration is enabled in general and
* specifically for this profile.
* @return bool
*/
public function isShopEnabled(): bool {
return Civi::settings()->get('twingle_use_shop') &&
$this->data['enable_shop_integration'];
}
/**
* Retrieves an attribute of the profile.
*
@ -226,9 +131,7 @@ class CRM_Twingle_Profile {
* @return mixed | NULL
*/
public function getAttribute($attribute_name, $default = NULL) {
return (isset($this->data[$attribute_name]) && $this->data[$attribute_name] !== '')
? $this->data[$attribute_name]
: $default;
return $this->data[$attribute_name] ?? $default;
}
/**
@ -237,15 +140,12 @@ class CRM_Twingle_Profile {
* @param string $attribute_name
* @param mixed $value
*
* @throws \Civi\Twingle\Exceptions\ProfileException
* @throws \Exception
* When the attribute name is not known.
*/
public function setAttribute($attribute_name, $value): void {
if (!in_array($attribute_name, self::allowedAttributes(), TRUE)) {
throw new ProfileException(
E::ts('Unknown attribute %1.', [1 => $attribute_name]),
ProfileException::ERROR_CODE_UNKNOWN_PROFILE_ATTRIBUTE
);
public function setAttribute($attribute_name, $value) {
if (!in_array($attribute_name, self::allowedAttributes())) {
throw new Exception(E::ts('Unknown attribute %1.', [1 => $attribute_name]));
}
// TODO: Check if value is acceptable.
$this->data[$attribute_name] = $value;
@ -254,347 +154,117 @@ class CRM_Twingle_Profile {
/**
* Get the CiviCRM transaction ID (to be used in contributions and recurring contributions)
*
* @param string $twingle_id Twingle ID
* @param $twingle_id string Twingle ID
* @return string CiviCRM transaction ID
*/
public function getTransactionID(string $twingle_id) {
public function getTransactionID($twingle_id) {
$prefix = Civi::settings()->get('twingle_prefix');
return ($prefix ?? '') . $twingle_id;
if (empty($prefix)) {
return $twingle_id;
} else {
return $prefix . $twingle_id;
}
}
/**
* Verifies whether the profile is valid (i.e. consistent and not colliding
* with other profiles).
*
* @throws \Civi\Twingle\Exceptions\ProfileValidationError
* @throws \Civi\Core\Exception\DBQueryException
* @throws Exception
* When the profile could not be successfully validated.
*/
public function validate(): void {
// Name cannot be empty
if ('' === $this->getName()) {
throw new ProfileValidationError(
'name',
E::ts('Profile name cannot be empty.'),
ProfileValidationError::ERROR_CODE_PROFILE_VALIDATION_FAILED
);
}
// Restrict profile names to alphanumeric characters, space and the underscore.
if (1 === preg_match('/[^A-Za-z0-9_\s]/', $this->getName())) {
throw new ProfileValidationError(
'name',
E::ts('Only alphanumeric characters, space and the underscore (_) are allowed for profile names.'),
ProfileValidationError::ERROR_CODE_PROFILE_VALIDATION_FAILED
);
}
// Check if profile name is already used for other profile
$profile_name_duplicates = array_filter(
CRM_Twingle_Profile::getProfiles(),
function($profile) {
return $profile->getName() == $this->getName() && $this->getId() != $profile->getId();
});
if ([] !== $profile_name_duplicates) {
throw new ProfileValidationError(
'name',
E::ts("A profile with the name '%1' already exists.", [1 => $this->getName()]),
ProfileValidationError::ERROR_CODE_PROFILE_VALIDATION_FAILED
);
}
// Check if project_id is already used in other profile
$profiles = $this::getProfiles();
foreach ($profiles as $profile) {
if ($profile->getId() == $this->getId() || $profile->is_default()) {
continue;
};
$project_ids = $this->getProjectIds();
$id_duplicates = array_intersect($profile->getProjectIds(), $project_ids);
if ([] !== $id_duplicates) {
throw new ProfileValidationError(
'selector',
E::ts(
"Project ID(s) [%1] already used in profile '%2'.",
[
1 => implode(', ', $id_duplicates),
2 => $profile->getName(),
]
),
ProfileValidationError::ERROR_CODE_PROFILE_VALIDATION_WARNING
);
}
}
// Validate custom field mapping.
$custom_field_mapping = $this->getAttribute('custom_field_mapping');
if (is_string($custom_field_mapping)) {
$custom_field_mapping = preg_split('/\r\n|\r|\n/', $custom_field_mapping, -1, PREG_SPLIT_NO_EMPTY);
$parsing_error = new ProfileValidationError(
'custom_field_mapping',
E::ts('Could not parse custom field mapping.'),
ProfileValidationError::ERROR_CODE_PROFILE_VALIDATION_FAILED
);
if (!is_array($custom_field_mapping)) {
throw $parsing_error;
}
foreach ($custom_field_mapping as $custom_field_map) {
$custom_field_map = explode('=', $custom_field_map);
if (count($custom_field_map) !== 2) {
throw $parsing_error;
}
[$twingle_field_name, $custom_field_name] = $custom_field_map;
$custom_field_id = substr($custom_field_name, strlen('custom_'));
// Check for custom field existence
try {
/**
* @phpstan-var array<string, mixed> $custom_field
*/
$custom_field = civicrm_api3(
'CustomField', 'getsingle', ['id' => $custom_field_id]
);
}
catch (CRM_Core_Exception $exception) {
throw new ProfileValidationError(
'custom_field_mapping',
E::ts(
'Custom field custom_%1 does not exist.',
[1 => $custom_field_id]
),
ProfileValidationError::ERROR_CODE_PROFILE_VALIDATION_FAILED,
$exception
);
}
// Only allow custom fields on relevant entities.
try {
civicrm_api3('CustomGroup', 'getsingle',
[
'id' => $custom_field['custom_group_id'],
'extends' => [
'IN' => [
'Contact',
'Individual',
'Organization',
'Contribution',
'ContributionRecur',
],
],
]);
}
catch (CRM_Core_Exception $exception) {
throw new ProfileValidationError(
'custom_field_mapping',
E::ts(
'Custom field custom_%1 is not in a CustomGroup that extends one of the supported CiviCRM entities.',
[1 => $custom_field['id']]
),
ProfileValidationError::ERROR_CODE_PROFILE_VALIDATION_FAILED,
$exception
);
}
}
}
public function verifyProfile() {
// TODO: check
// data of this profile consistent?
// conflicts with other profiles?
}
/**
* Persists the profile within the database.
*
* @throws \Civi\Twingle\Exceptions\ProfileException
* Persists the profile within the CiviCRM settings.
*/
public function saveProfile(): void {
try {
if (isset($this->id)) {
// existing profile -> just update the config
CRM_Core_DAO::executeQuery(
'UPDATE civicrm_twingle_profile SET config = %2, name = %3 WHERE id = %1',
[
1 => [$this->id, 'String'],
2 => [json_encode($this->data), 'String'],
3 => [$this->name, 'String'],
]);
}
else {
// new profile -> add new entry to the DB
CRM_Core_DAO::executeQuery(
<<<SQL
INSERT IGNORE INTO civicrm_twingle_profile(name,config,last_access,access_counter) VALUES (%1, %2, null, 0)
SQL,
[
1 => [$this->name, 'String'],
2 => [json_encode($this->data), 'String'],
]);
}
}
catch (Exception $exception) {
throw new ProfileException(
E::ts('Could not save/update profile: %1', [1 => $exception->getMessage()]),
ProfileException::ERROR_CODE_COULD_NOT_SAVE_PROFILE,
$exception
);
public function saveProfile() {
// make sure it's valid
$this->verifyProfile();
// check if the profile exists
$profile_id = CRM_Core_DAO::singleValueQuery(
"SELECT id FROM civicrm_twingle_profile WHERE name = %1", [1 => [$this->name, 'String']]);
if ($profile_id) {
// existing profile -> just update the config
CRM_Core_DAO::executeQuery(
"UPDATE civicrm_twingle_profile SET config = %2 WHERE name = %1",
[
1 => [$this->name, 'String'],
2 => [json_encode($this->data), 'String']
]);
} else {
// new profile -> add new entry to the DB
CRM_Core_DAO::executeQuery(
"INSERT IGNORE INTO civicrm_twingle_profile(name,config,last_access,access_counter) VALUES (%1, %2, null, 0)",
[
1 => [$this->name, 'String'],
2 => [json_encode($this->data), 'String']
]);
}
}
/**
* Deletes the profile from the database
*
* @throws \Civi\Twingle\Exceptions\ProfileException
*/
public function deleteProfile(): void {
// Do only reset default profile
if ($this->getName() == 'default') {
try {
$default_profile = CRM_Twingle_Profile::createDefaultProfile();
$default_profile->setId($this->getId());
$default_profile->saveProfile();
// Reset counter
CRM_Core_DAO::executeQuery(
'UPDATE civicrm_twingle_profile SET access_counter = 0, last_access = NULL WHERE id = %1',
[1 => [$this->id, 'Integer']]
);
}
catch (Exception $exception) {
throw new ProfileException(
E::ts('Could not reset default profile: %1', [1 => $exception->getMessage()]),
ProfileException::ERROR_CODE_COULD_NOT_RESET_PROFILE,
$exception
);
}
}
else {
try {
CRM_Core_DAO::executeQuery(
'DELETE FROM civicrm_twingle_profile WHERE id = %1',
[1 => [$this->id, 'Integer']]
);
}
catch (Exception $exception) {
throw new ProfileException(
E::ts('Could not delete profile: %1', [1 => $exception->getMessage()]),
ProfileException::ERROR_CODE_COULD_NOT_DELETE_PROFILE,
$exception
);
}
}
public function deleteProfile() {
CRM_Core_DAO::executeQuery("DELETE FROM civicrm_twingle_profile WHERE name = %1", [1 => [$this->name, 'String']]);
}
/**
* Returns an array of attributes allowed for a profile.
*
* @return array<string>|array<string, bool>
* @return array
*/
public static function allowedAttributes(bool $asMetadata = FALSE) {
$attributes = array_merge(
public static function allowedAttributes() {
return array_merge(
[
'selector' => [
'label' => E::ts('Project IDs'),
'required' => TRUE,
],
'xcm_profile' => ['required' => FALSE],
'location_type_id' => [
'label' => E::ts('Location type'),
'required' => TRUE,
],
'location_type_id_organisation' => [
'label' => E::ts('Location type for organisations'),
'required' => TRUE,
],
'financial_type_id' => [
'label' => E::ts('Financial type'),
'required' => TRUE,
],
'financial_type_id_recur' => [
'label' => E::ts('Financial type (recurring)'),
'required' => TRUE,
],
'sepa_creditor_id' => [
'label' => E::ts('CiviSEPA creditor'),
'required' => CRM_Twingle_Submission::civiSepaEnabled(),
],
'gender_male' => [
'label' => E::ts('Gender option for submitted value "male"'),
'required' => TRUE,
],
'gender_female' => [
'label' => E::ts('Gender option for submitted value "female"'),
'required' => TRUE,
],
'gender_other' => [
'label' => E::ts('Gender option for submitted value "other"'),
'required' => TRUE,
],
'prefix_male' => [
'label' => E::ts('Prefix option for submitted value "male"'),
'required' => TRUE,
],
'prefix_female' => [
'label' => E::ts('Prefix option for submitted value "female"'),
'required' => TRUE,
],
'prefix_other' => [
'label' => E::ts('Prefix option for submitted value "other"'),
'required' => TRUE,
],
'newsletter_groups' => ['required' => FALSE],
'postinfo_groups' => ['required' => FALSE],
'donation_receipt_groups' => ['required' => FALSE],
'campaign' => ['required' => FALSE],
'campaign_targets' => ['required' => FALSE],
'contribution_source' => ['required' => FALSE],
'custom_field_mapping' => ['required' => FALSE],
'membership_type_id' => ['required' => FALSE],
'membership_type_id_recur' => ['required' => FALSE],
'membership_postprocess_call' => ['required' => FALSE],
'newsletter_double_opt_in' => ['required' => FALSE],
'required_address_components' => ['required' => FALSE],
'map_as_contribution_notes' => ['required' => FALSE],
'map_as_contact_notes' => ['required' => FALSE],
'enable_shop_integration' => ['required' => FALSE],
'shop_financial_type' => ['required' => FALSE],
'shop_donation_financial_type' => ['required' => FALSE],
'shop_map_products' => ['required' => FALSE],
'selector',
'xcm_profile',
'location_type_id',
'location_type_id_organisation',
'financial_type_id',
'financial_type_id_recur',
'sepa_creditor_id',
'gender_male',
'gender_female',
'gender_other',
'prefix_male',
'prefix_female',
'prefix_other',
'newsletter_groups',
'postinfo_groups',
'donation_receipt_groups',
'campaign',
'campaign_targets',
'contribution_source',
'custom_field_mapping',
'membership_type_id',
'membership_type_id_recur',
'membership_postprocess_call',
'newsletter_double_opt_in',
'required_address_components',
],
// Add payment methods.
array_combine(
array_keys(static::paymentInstruments()),
array_map(
function ($value) {
return [
'label' => $value,
'required' => TRUE,
];
},
static::paymentInstruments()
)),
array_keys(static::paymentInstruments()),
// Add contribution status for all payment methods.
array_combine(
array_map(function($attribute) {
return $attribute . '_status';
}, array_keys(static::paymentInstruments())),
array_map(
function($value) {
return [
'label' => $value . ' - ' . E::ts('Contribution Status'),
'required' => TRUE,
];
},
static::paymentInstruments()
)),
array_map(function ($attribute) {
return $attribute . '_status';
}, array_keys(static::paymentInstruments()))
);
return $asMetadata ? $attributes : array_keys($attributes);
}
/**
* Retrieves a list of supported payment methods.
*
* @return array<string, string>
* @return array
*/
public static function paymentInstruments(): array {
public static function paymentInstruments() {
return [
'pi_banktransfer' => E::ts('Bank transfer'),
'pi_debit_manual' => E::ts('Debit manual'),
@ -605,13 +275,12 @@ class CRM_Twingle_Profile {
'pi_sofortueberweisung' => E::ts('SOFORT Überweisung'),
'pi_amazonpay' => E::ts('Amazon Pay'),
'pi_applepay' => E::ts('Apple Pay'),
'pi_googlepay' => E::ts('Google Pay'),
'pi_googlepay' => E::ts('Google Pay'),
'pi_paydirekt' => E::ts('Paydirekt'),
'pi_twint' => E::ts('Twint'),
'pi_ideal' => E::ts('iDEAL'),
'pi_post_finance' => E::ts('Postfinance'),
'pi_bancontact' => E::ts('Bancontact'),
'pi_generic' => E::ts('Generic Payment Method'),
];
}
@ -625,21 +294,16 @@ class CRM_Twingle_Profile {
*/
public static function createDefaultProfile($name = 'default') {
return new CRM_Twingle_Profile($name, [
'selector' => NULL,
'selector' => '',
'xcm_profile' => '',
'location_type_id' => CRM_Twingle_Submission::LOCATION_TYPE_ID_WORK,
'location_type_id_organisation' => CRM_Twingle_Submission::LOCATION_TYPE_ID_WORK,
// "Donation"
'financial_type_id' => 1,
// "Donation"
'financial_type_id_recur' => 1,
// "EFT"
'pi_banktransfer' => 5,
'financial_type_id' => 1, // "Donation"
'financial_type_id_recur' => 1, // "Donation"
'pi_banktransfer' => 5, // "EFT"
'pi_debit_manual' => NULL,
// Debit
'pi_debit_automatic' => 2,
// "Credit Card"
'pi_creditcard' => 1,
'pi_debit_automatic' => 2, // Debit
'pi_creditcard' => 1, // "Credit Card"
'pi_mobilephone_germany' => NULL,
'pi_paypal' => NULL,
'pi_sofortueberweisung' => NULL,
@ -651,7 +315,6 @@ class CRM_Twingle_Profile {
'pi_ideal' => NULL,
'pi_post_finance' => NULL,
'pi_bancontact' => NULL,
'pi_generic' => NULL,
'sepa_creditor_id' => NULL,
'gender_male' => 2,
'gender_female' => 1,
@ -672,18 +335,11 @@ class CRM_Twingle_Profile {
'city',
'country',
],
'map_as_contribution_notes' => [],
'map_as_contact_notes' => [],
'enable_shop_integration' => FALSE,
'shop_financial_type' => 1,
'shop_donation_financial_type' => 1,
'shop_map_products' => FALSE,
]
// Add contribution status for all payment methods.
// phpcs:ignore Drupal.Formatting.SpaceUnaryOperator.PlusMinus
+ array_fill_keys(array_map(function($attribute) {
return $attribute . '_status';
}, array_keys(static::paymentInstruments())), CRM_Twingle_Submission::CONTRIBUTION_STATUS_COMPLETED));
// Add contribution status for all payment methods.
+ array_fill_keys(array_map(function($attribute) {
return $attribute . '_status';
}, array_keys(static::paymentInstruments())), CRM_Twingle_Submission::CONTRIBUTION_STATUS_COMPLETED));
}
/**
@ -691,26 +347,23 @@ class CRM_Twingle_Profile {
* which is responsible for processing the project's data.
* Returns the default profile if no match was found.
*
* @param string $project_id
* @param $project_id
*
* @return CRM_Twingle_Profile
* @throws \Civi\Twingle\Exceptions\ProfileException
* @throws \CRM\Twingle\Exceptions\ProfileException
* @throws \Civi\Core\Exception\DBQueryException
*/
public static function getProfileForProject($project_id) {
$profiles = self::getProfiles();
$default_profile = NULL;
foreach ($profiles as $profile) {
if ($profile->matches($project_id)) {
return $profile;
}
if ($profile->is_default()) {
$default_profile = $profile;
}
}
// If none matches, use the default profile.
$default_profile = $profiles['default'];
if (!empty($default_profile)) {
return $default_profile;
}
@ -723,55 +376,36 @@ class CRM_Twingle_Profile {
}
/**
* Retrieves the profile with the given ID.
* Retrieves the profile with the given name.
*
* @param int|NULL $id
* @param string $name
*
* @return CRM_Twingle_Profile | NULL
* @throws \Civi\Core\Exception\DBQueryException
* @throws \Civi\Twingle\Exceptions\ProfileException
*/
public static function getProfile(int $id = NULL) {
if (isset($id)) {
/**
* @var CRM_Core_DAO $profile_data
*/
$profile_data = CRM_Core_DAO::executeQuery(
'SELECT id, name, config FROM civicrm_twingle_profile WHERE id = %1',
[1 => [$id, 'Integer']]
);
if ($profile_data->fetch()) {
return new CRM_Twingle_Profile(
$profile_data->name,
json_decode($profile_data->config, TRUE),
(int) $profile_data->id
);
public static function getProfile($name) {
if (!empty($name)) {
$profile_data = CRM_Core_DAO::singleValueQuery("SELECT config FROM civicrm_twingle_profile WHERE name = %1", [
1 => [$name, 'String']]);
if ($profile_data) {
return new CRM_Twingle_Profile($name, json_decode($profile_data, 1));
}
}
throw new ProfileException('Profile not found.', ProfileException::ERROR_CODE_PROFILE_NOT_FOUND);
return NULL;
}
/**
* Retrieves the list of all profiles persisted within the current CiviCRM
* settings, including the default profile.
*
* @return array<int, \CRM_Twingle_Profile>
* An array of profiles with profile IDs as keys and profile objects as values.
* @throws \Civi\Core\Exception\DBQueryException
* @return array
* profile_name => CRM_Twingle_Profile
*/
public static function getProfiles(): array {
public static function getProfiles() {
// todo: cache?
$profiles = [];
/**
* @var CRM_Core_DAO $profile_data
*/
$profile_data = CRM_Core_DAO::executeQuery('SELECT id, name, config FROM civicrm_twingle_profile');
$profile_data = CRM_Core_DAO::executeQuery("SELECT name, config FROM civicrm_twingle_profile");
while ($profile_data->fetch()) {
$profiles[(int) $profile_data->id] = new CRM_Twingle_Profile(
$profile_data->name,
json_decode($profile_data->config, TRUE),
(int) $profile_data->id
);
$profiles[$profile_data->name] = new CRM_Twingle_Profile($profile_data->name, json_decode($profile_data->config, 1));
}
return $profiles;
}
@ -779,32 +413,21 @@ class CRM_Twingle_Profile {
/**
* Get the stats (access_count, last_access) for all twingle profiles
*
* @return array<string, array<string, mixed>>
* @throws \Civi\Core\Exception\DBQueryException
* @return CRM_Twingle_Profile[]
*/
public static function getProfileStats() {
$stats = [];
/**
* @var CRM_Core_DAO $profile_data
*/
$profile_data = CRM_Core_DAO::executeQuery(
'SELECT name, last_access, access_counter FROM civicrm_twingle_profile'
);
$profile_data = CRM_Core_DAO::executeQuery("SELECT name, last_access, access_counter FROM civicrm_twingle_profile");
while ($profile_data->fetch()) {
// phpcs:disable Drupal.Arrays.Array.ArrayIndentation
$stats[(string) $profile_data->name] = [
$stats[$profile_data->name] = [
'name' => $profile_data->name,
'last_access' => $profile_data->last_access,
'last_access_txt' => $profile_data->last_access
? date('Y-m-d H:i:s', strtotime($profile_data->last_access))
: E::ts('never'),
'last_access_txt' => $profile_data->last_access ? date('Y-m-d H:i:s', strtotime($profile_data->last_access)) : E::ts("never"),
'access_counter' => $profile_data->access_counter,
'access_counter_txt' => $profile_data->access_counter
? ((int) $profile_data->access_counter) . 'x'
: E::ts('never'),
'access_counter_txt' => $profile_data->access_counter ? ((int) $profile_data->access_counter) . 'x' : E::ts("never"),
];
// phpcs:enable
}
return $stats;
}
}

View file

@ -13,45 +13,29 @@
| written permission from the original author(s). |
+-------------------------------------------------------------*/
declare(strict_types = 1);
use CRM_Twingle_ExtensionUtil as E;
use Civi\Twingle\Exceptions\BaseException;
use Civi\Twingle\Shop\Exceptions\LineItemException;
class CRM_Twingle_Submission {
/**
* The default ID of the "Work" location type.
*/
public const LOCATION_TYPE_ID_WORK = 2;
const LOCATION_TYPE_ID_WORK = 2;
/**
* The option value name of the group type for newsletter subscribers.
*/
public const GROUP_TYPE_NEWSLETTER = 'Mailing List';
const GROUP_TYPE_NEWSLETTER = 'Mailing List';
/**
* The option value for the contribution type for completed contributions.
*/
public const CONTRIBUTION_STATUS_COMPLETED = 'Completed';
const CONTRIBUTION_STATUS_COMPLETED = 'Completed';
/**
* The default ID of the "Employer of" relationship type.
*/
public const EMPLOYER_RELATIONSHIP_TYPE_ID = 5;
/**
* List of allowed product attributes.
*/
const ALLOWED_PRODUCT_ATTRIBUTES = [
'id',
'name',
'internal_id',
'price',
'count',
'total_value',
];
const EMPLOYER_RELATIONSHIP_TYPE_ID = 5;
/**
* @param array &$params
@ -61,23 +45,23 @@ class CRM_Twingle_Submission {
* The Twingle profile to use for validation, defaults to the default
* profile.
*
* @throws \CRM_Core_Exception
* @throws \CiviCRM_API3_Exception
* When invalid parameters have been submitted.
*/
public static function validateSubmission(&$params, $profile = NULL): void {
if (!isset($profile)) {
public static function validateSubmission(&$params, $profile = NULL) {
if (!$profile) {
$profile = CRM_Twingle_Profile::createDefaultProfile();
}
// Validate donation rhythm.
if (!in_array($params['donation_rhythm'], [
if (!in_array($params['donation_rhythm'], array(
'one_time',
'halfyearly',
'quarterly',
'yearly',
'monthly',
], TRUE)) {
throw new CRM_Core_Exception(
))) {
throw new CiviCRM_API3_Exception(
E::ts('Invalid donation rhythm.'),
'invalid_format'
);
@ -85,9 +69,8 @@ class CRM_Twingle_Submission {
// Get the payment instrument defined within the profile, or return an error
// if none matches (i.e. an unknown payment method was submitted).
$payment_instrument_id = $profile->getAttribute('pi_' . $params['payment_method'], '');
if ('' === $payment_instrument_id) {
throw new CRM_Core_Exception(
if (!$payment_instrument_id = $profile->getAttribute('pi_' . $params['payment_method'])) {
throw new CiviCRM_API3_Exception(
E::ts('Payment method could not be matched to existing payment instrument.'),
'invalid_format'
);
@ -95,16 +78,16 @@ class CRM_Twingle_Submission {
$params['payment_instrument_id'] = $payment_instrument_id;
// Validate date for parameter "confirmed_at".
if (FALSE === DateTime::createFromFormat('YmdHis', $params['confirmed_at'])) {
throw new CRM_Core_Exception(
if (!DateTime::createFromFormat('YmdHis', $params['confirmed_at'])) {
throw new CiviCRM_API3_Exception(
E::ts('Invalid date for parameter "confirmed_at".'),
'invalid_format'
);
}
// Validate date for parameter "user_birthdate".
if (!empty($params['user_birthdate']) && FALSE === DateTime::createFromFormat('Ymd', $params['user_birthdate'])) {
throw new CRM_Core_Exception(
if (!empty($params['user_birthdate']) && !DateTime::createFromFormat('Ymd', $params['user_birthdate'])) {
throw new CiviCRM_API3_Exception(
E::ts('Invalid date for parameter "user_birthdate".'),
'invalid_format'
);
@ -112,10 +95,9 @@ class CRM_Twingle_Submission {
// Get the gender ID defined within the profile, or return an error if none
// matches (i.e. an unknown gender was submitted).
if (is_string($params['user_gender'])) {
$gender_id = $profile->getAttribute('gender_' . $params['user_gender']);
if (!is_numeric($gender_id)) {
throw new CRM_Core_Exception(
if (!empty($params['user_gender'])) {
if (!$gender_id = $profile->getAttribute('gender_' . $params['user_gender'])) {
throw new CiviCRM_API3_Exception(
E::ts('Gender could not be matched to existing gender.'),
'invalid_format'
);
@ -124,42 +106,26 @@ class CRM_Twingle_Submission {
}
// Validate custom fields parameter, if given.
if (isset($params['custom_fields'])) {
if (!empty($params['custom_fields'])) {
if (is_string($params['custom_fields'])) {
$params['custom_fields'] = json_decode($params['custom_fields'], TRUE);
$params['custom_fields'] = json_decode($params['custom_fields'], TRUE);
}
if (!is_array($params['custom_fields'])) {
throw new CRM_Core_Exception(
throw new CiviCRM_API3_Exception(
E::ts('Invalid format for custom fields.'),
'invalid_format'
);
}
}
// Validate products
if (!empty($params['products']) && $profile->isShopEnabled()) {
if (is_string($params['products'])) {
$products = json_decode($params['products'], TRUE);
$params['products'] = array_map(function ($product) {
return array_intersect_key($product, array_flip(self::ALLOWED_PRODUCT_ATTRIBUTES));
}, $products);
}
if (!is_array($params['products'])) {
throw new CiviCRM_API3_Exception(
E::ts('Invalid format for products.'),
'invalid_format'
);
}
}
// Validate campaign_id, if given.
if (isset($params['campaign_id'])) {
if (!empty($params['campaign_id'])) {
// Check whether campaign_id is a numeric string and cast it to an integer.
if (is_numeric($params['campaign_id'])) {
$params['campaign_id'] = intval($params['campaign_id']);
}
else {
throw new CRM_Core_Exception(
throw new CiviCRM_API3_Exception(
E::ts('campaign_id must be a numeric string. '),
'invalid_format'
);
@ -171,8 +137,7 @@ class CRM_Twingle_Submission {
'getsingle',
['id' => $params['campaign_id']]
);
}
catch (CRM_Core_Exception $e) {
} catch (CiviCRM_API3_Exception $e) {
unset($params['campaign_id']);
}
}
@ -184,33 +149,28 @@ class CRM_Twingle_Submission {
*
* @param string $contact_type
* The contact type to look for/to create.
* @param array<string, mixed> $contact_data
* @param array $contact_data
* Data to use for contact lookup/to create a contact with.
* @param CRM_Twingle_Profile $profile
* Profile used for this process
* @param array<string, mixed> $submission
* @param array $submission
* Submission data
*
* @return int|NULL
* @return int | NULL
* The ID of the matching/created contact, or NULL if no matching contact
* was found and no new contact could be created.
* @throws \CRM_Core_Exception
* @throws \CiviCRM_API3_Exception
* When invalid data was given.
*/
public static function getContact(
string $contact_type,
array $contact_data,
CRM_Twingle_Profile $profile,
array $submission = []
) {
public static function getContact($contact_type, $contact_data, $profile, $submission = []) {
// If no parameters are given, do nothing.
if ([] === $contact_data) {
if (empty($contact_data)) {
return NULL;
}
// add xcm profile
$xcm_profile = $profile->getAttribute('xcm_profile');
if (isset($xcm_profile) && '' !== $xcm_profile) {
if (!empty($xcm_profile)) {
$contact_data['xcm_profile'] = $xcm_profile;
}
@ -218,7 +178,7 @@ class CRM_Twingle_Submission {
CRM_Twingle_Submission::setCampaign($contact_data, 'contact', $submission, $profile);
// Prepare values: country.
if (isset($contact_data['country'])) {
if (!empty($contact_data['country'])) {
if (is_numeric($contact_data['country'])) {
// If a country ID is given, update the parameters.
$contact_data['country_id'] = $contact_data['country'];
@ -226,14 +186,14 @@ class CRM_Twingle_Submission {
}
else {
// Look up the country depending on the given ISO code.
$country = civicrm_api3('Country', 'get', ['iso_code' => $contact_data['country']]);
if (isset($country['id'])) {
$country = civicrm_api3('Country', 'get', array('iso_code' => $contact_data['country']));
if (!empty($country['id'])) {
$contact_data['country_id'] = $country['id'];
unset($contact_data['country']);
}
else {
throw new \CRM_Core_Exception(
E::ts('Unknown country %1.', [1 => $contact_data['country']]),
throw new \CiviCRM_API3_Exception(
E::ts('Unknown country %1.', array(1 => $contact_data['country'])),
'invalid_format'
);
}
@ -241,7 +201,7 @@ class CRM_Twingle_Submission {
}
// Prepare values: language.
if (is_string($contact_data['preferred_language']) && '' !== $contact_data['preferred_language']) {
if (!empty($contact_data['preferred_language'])) {
$mapping = CRM_Core_I18n_PseudoConstant::longForShortMapping();
// Override the default mapping for German.
$mapping['de'] = 'de_DE';
@ -251,46 +211,48 @@ class CRM_Twingle_Submission {
// Pass to XCM.
$contact_data['contact_type'] = $contact_type;
$contact = civicrm_api3('Contact', 'getorcreate', $contact_data);
if (empty($contact['id'])) {
return NULL;
}
return isset($contact['id']) ? (int) $contact['id'] : NULL;
return $contact['id'];
}
/**
* Shares an organisation's work address, unless the contact already has one.
*
* @param int $contact_id
* @param $contact_id
* The ID of the contact to share the organisation address with.
* @param int $organisation_id
* @param $organisation_id
* The ID of the organisation whose address to share with the contact.
* @param int $location_type_id
* @param $location_type_id
* The ID of the location type to use for address lookup.
*
* @return boolean
* Whether the organisation address has been shared with the contact.
*
* @throws \CRM_Core_Exception
* @throws \CiviCRM_API3_Exception
* When looking up or creating the shared address failed.
*/
public static function shareWorkAddress(
int $contact_id,
int $organisation_id,
int $location_type_id = self::LOCATION_TYPE_ID_WORK
) {
public static function shareWorkAddress($contact_id, $organisation_id, $location_type_id = self::LOCATION_TYPE_ID_WORK) {
if (empty($organisation_id)) {
// Only if organisation exists.
return FALSE;
}
// Check whether organisation has a WORK address.
$existing_org_addresses = civicrm_api3('Address', 'get', [
$existing_org_addresses = civicrm_api3('Address', 'get', array(
'contact_id' => $organisation_id,
'location_type_id' => $location_type_id,
]);
'location_type_id' => $location_type_id));
if ($existing_org_addresses['count'] <= 0) {
// Organisation does not have a WORK address.
return FALSE;
}
// Check whether contact already has a WORK address.
$existing_contact_addresses = civicrm_api3('Address', 'get', [
$existing_contact_addresses = civicrm_api3('Address', 'get', array(
'contact_id' => $contact_id,
'location_type_id' => $location_type_id,
]);
'location_type_id' => $location_type_id));
if ($existing_contact_addresses['count'] > 0) {
// Contact already has a WORK address.
return FALSE;
@ -314,25 +276,29 @@ class CRM_Twingle_Submission {
* @param int $organisation_id
* The ID of the employer contact.
*
* @throws \CRM_Core_Exception
* @throws \CiviCRM_API3_Exception
*/
public static function updateEmployerRelation(int $contact_id, int $organisation_id): void {
public static function updateEmployerRelation($contact_id, $organisation_id) {
if (empty($contact_id) || empty($organisation_id)) {
return;
}
// see if there is already one
$existing_relationship = civicrm_api3('Relationship', 'get', [
$existing_relationship = civicrm_api3('Relationship', 'get', array(
'relationship_type_id' => self::EMPLOYER_RELATIONSHIP_TYPE_ID,
'contact_id_a' => $contact_id,
'contact_id_b' => $organisation_id,
'is_active' => 1,
]);
));
if ($existing_relationship['count'] == 0) {
// There is currently no (active) relationship between these contacts.
$new_relationship_data = [
$new_relationship_data = array(
'relationship_type_id' => self::EMPLOYER_RELATIONSHIP_TYPE_ID,
'contact_id_a' => $contact_id,
'contact_id_b' => $organisation_id,
'is_active' => 1,
];
);
civicrm_api3('Relationship', 'create', $new_relationship_data);
}
@ -343,15 +309,16 @@ class CRM_Twingle_Submission {
* functionality is activated within the Twingle extension settings.
*
* @return bool
* @throws \CRM_Core_Exception
* @throws \CiviCRM_API3_Exception
*/
public static function civiSepaEnabled() {
$sepa_extension = civicrm_api3('Extension', 'get', [
$sepa_extension = civicrm_api3('Extension', 'get', array(
'full_name' => 'org.project60.sepa',
'is_active' => 1,
]);
return (bool) Civi::settings()->get('twingle_use_sepa')
&& $sepa_extension['count'] >= 0;
));
return
Civi::settings()->get('twingle_use_sepa')
&& $sepa_extension['count'];
}
/**
@ -362,30 +329,30 @@ class CRM_Twingle_Submission {
* The submitted "donation_rhythm" paramter according to the API action
* specification.
*
* @return array{'frequency_unit'?: string, 'frequency_interval'?: int}
* @return array
* An array with "frequency_unit" and "frequency_interval" keys, to be added
* to contribution parameter arrays.
*/
public static function getFrequencyMapping($donation_rhythm) {
$mapping = [
'halfyearly' => [
$mapping = array(
'halfyearly' => array(
'frequency_unit' => 'month',
'frequency_interval' => 6,
],
'quarterly' => [
),
'quarterly' => array(
'frequency_unit' => 'month',
'frequency_interval' => 3,
],
'yearly' => [
),
'yearly' => array(
'frequency_unit' => 'month',
'frequency_interval' => 12,
],
'monthly' => [
),
'monthly' => array(
'frequency_unit' => 'month',
'frequency_interval' => 1,
],
'one_time' => [],
];
),
'one_time' => array(),
);
return $mapping[$donation_rhythm];
}
@ -403,21 +370,19 @@ class CRM_Twingle_Submission {
* @return int
* The next possible day of this or the next month to start collecting.
*/
public static function getSEPACycleDay($start_date, $creditor_id): int {
$buffer_days = (int) CRM_Sepa_Logic_Settings::getSetting('pp_buffer_days');
$frst_notice_days = (int) CRM_Sepa_Logic_Settings::getSetting('batching.FRST.notice', $creditor_id);
if (FALSE === ($earliest_rcur_date = strtotime("$start_date + $frst_notice_days days + $buffer_days days"))) {
throw new BaseException(E::ts('Could not calculate SEPA cycle day from configuration.'));
}
public static function getSEPACycleDay($start_date, $creditor_id) {
$buffer_days = (int) CRM_Sepa_Logic_Settings::getSetting("pp_buffer_days");
$frst_notice_days = (int) CRM_Sepa_Logic_Settings::getSetting("batching.FRST.notice", $creditor_id);
$earliest_rcur_date = strtotime("$start_date + $frst_notice_days days + $buffer_days days");
// Find the next cycle day
$cycle_days = CRM_Sepa_Logic_Settings::getListSetting('cycledays', range(1, 28), $creditor_id);
$cycle_days = CRM_Sepa_Logic_Settings::getListSetting("cycledays", range(1, 28), $creditor_id);
$earliest_cycle_day = $earliest_rcur_date;
while (!in_array(date('j', $earliest_cycle_day), $cycle_days, TRUE)) {
$earliest_cycle_day = strtotime('+ 1 day', $earliest_cycle_day);
while (!in_array(date('j', $earliest_cycle_day), $cycle_days)) {
$earliest_cycle_day = strtotime("+ 1 day", $earliest_cycle_day);
}
return (int) date('j', $earliest_cycle_day);
return date('j', $earliest_cycle_day);
}
/**
@ -426,167 +391,33 @@ class CRM_Twingle_Submission {
* from the submission data. Should that be empty, the profile's default
* campaign is used.
*
* @param array<string, mixed> $entity_data
* @param array $entity_data
* the data set where the campaign_id should be set
* @param string $context
* defines the type of the entity_data: one of 'contribution', 'membership','mandate', 'recurring', 'contact'
* @param array<string, mixed> $submission
* @param array $submission
* the submitted data
* @param CRM_Twingle_Profile $profile
* the twingle profile used
*/
public static function setCampaign(
array &$entity_data,
string $context,
array $submission,
CRM_Twingle_Profile $profile
): void {
public static function setCampaign(&$entity_data, $context, $submission, $profile) {
// first: make sure it's not set from other workflows
unset($entity_data['campaign_id']);
// then: check if campaign should be set it this context
$enabled_contexts = $profile->getAttribute('campaign_targets');
if ($enabled_contexts === NULL || !is_array($enabled_contexts)) {
if ($enabled_contexts === null || !is_array($enabled_contexts)) {
// backward compatibility:
$enabled_contexts = ['contribution', 'contact'];
}
if (in_array($context, $enabled_contexts, TRUE)) {
if (in_array($context, $enabled_contexts)) {
// use the submitted campaign if set
if (is_numeric($submission['campaign_id'])) {
if (!empty($submission['campaign_id'])) {
$entity_data['campaign_id'] = $submission['campaign_id'];
}
// otherwise use the profile's
elseif (is_numeric($campaign = $profile->getAttribute('campaign'))) {
} // otherwise use the profile's
elseif (!empty($campaign = $profile->getAttribute('campaign'))) {
$entity_data['campaign_id'] = $campaign;
}
}
}
/**
* @param $values
* Processed data
* @param $submission
* Submission data
* @param $profile
* The twingle profile used
*
* @throws \CiviCRM_API3_Exception
* @throws \CRM_Core_Exception
* @throws \Civi\Twingle\Shop\Exceptions\LineItemException
*/
public static function createLineItems($values, $submission, $profile): array {
$line_items = [];
$sum_line_items = 0;
$contribution_id = $values['contribution']['id'];
if (empty($contribution_id)) {
throw new LineItemException(
"Could not find contribution id for line item assignment.",
LineItemException::ERROR_CODE_CONTRIBUTION_NOT_FOUND
);
}
foreach ($submission['products'] as $product) {
$line_item_data = [
'entity_table' => "civicrm_contribution",
'contribution_id' => $contribution_id,
'entity_id' => $contribution_id,
'label' => $product['name'],
'qty' => $product['count'],
'unit_price' => $product['price'],
'line_total' => $product['total_value'],
'sequential' => 1,
];
// Try to find the TwingleProduct with its corresponding PriceField
// for this product
try {
$price_field = CRM_Twingle_BAO_TwingleProduct::findByExternalId($product['id']);
}
catch (Exception $e) {
Civi::log()->error(E::LONG_NAME .
": An error occurred when searching for TwingleShop with the external ID " .
$product['id'], ['exception' => $e]);
$price_field = NULL;
}
// If found, use the financial type and price field id from the price field
if ($price_field) {
// Log warning if price is not variable and differs from the submission
if ($price_field->price !== Null && $price_field->price != (int) $product['price']) {
Civi::log()->warning(E::LONG_NAME .
": Price for product " . $product['name'] . " differs from the PriceField. " .
"Using the price from the submission.", ['price_field' => $price_field->price, 'submission' => $product['price']]);
}
// Log warning if name differs from the submission
if ($price_field->name != $product['name']) {
Civi::log()->warning(E::LONG_NAME .
": Name for product " . $product['name'] . " differs from the PriceField " .
"Using the name from the submission.", ['price_field' => $price_field->name, 'submission' => $product['name']]);
}
// Set the financial type and price field id
$line_item_data['financial_type_id'] = $price_field->financial_type_id;
$line_item_data['price_field_value_id'] = $price_field->getPriceFieldValueId();
$line_item_data['price_field_id'] = $price_field->price_field_id;
$line_item_data['description'] = $price_field->description;
}
// If not found, use the shops default financial type
else {
$financial_type_id = $profile->getAttribute('shop_financial_type', 1);
$line_item_data['financial_type_id'] = $financial_type_id;
}
// Create the line item
$line_item = civicrm_api3('LineItem', 'create', $line_item_data);
if (!empty($line_item['is_error'])) {
$line_item_name = $line_item_data['name'];
throw new CiviCRM_API3_Exception(
E::ts("Could not create line item for product '%1'", [1 => $line_item_name]),
'api_error'
);
}
$line_items[] = array_pop($line_item['values']);
$sum_line_items += $product['total_value'];
}
// Create line item for donation part
$donation_sum = (float) $values['contribution']['total_amount'] - $sum_line_items;
if ($donation_sum > 0) {
$donation_financial_type_id = $profile->getAttribute('shop_donation_financial_type', 1);
$donation_label = civicrm_api3('FinancialType', 'getsingle', [
'return' => ['name'],
'id' => $donation_financial_type_id,
])['name'];
$donation_line_item_data = [
'entity_table' => "civicrm_contribution",
'contribution_id' => $contribution_id,
'entity_id' => $contribution_id,
'label' => $donation_label,
'qty' => 1,
'unit_price' => $donation_sum,
'line_total' => $donation_sum,
'financial_type_id' => $donation_financial_type_id,
'sequential' => 1,
];
$donation_line_item = civicrm_api3('LineItem', 'create', $donation_line_item_data);
if (!empty($donation_line_item['is_error'])) {
throw new CiviCRM_API3_Exception(
E::ts("Could not create line item for donation"),
'api_error'
);
}
$line_items[] = array_pop($donation_line_item['values']);
}
return $line_items;
}
}

View file

@ -13,10 +13,7 @@
| written permission from the original author(s). |
+-------------------------------------------------------------*/
declare(strict_types = 1);
use CRM_Twingle_ExtensionUtil as E;
use Civi\Twingle\Exceptions\BaseException;
class CRM_Twingle_Tools {
@ -30,42 +27,31 @@ class CRM_Twingle_Tools {
* Check if the attempted modification of the recurring contribution is allowed.
* If not, an exception will be raised
*
* @param int $recurring_contribution_id
* @param array<mixed> $change
* @param $recurring_contribution_id int
* @param $change array
* @throws Exception if the change is not allowed
*/
public static function checkRecurringContributionChange(int $recurring_contribution_id, array $change): void {
public static function checkRecurringContributionChange($recurring_contribution_id, $change) {
// check if a change to the status is planned
if (empty($change['contribution_status_id'])) {
return;
}
if (empty($change['contribution_status_id'])) return;
// check if the target status is not closed
if (in_array($change['contribution_status_id'], [2, 5])) {
return;
}
if (in_array($change['contribution_status_id'], [2,5])) return;
// check if we're suspended
if (self::$protection_suspended) {
return;
}
if (self::$protection_suspended) return;
// check if protection is turned on
$protection_on = Civi::settings()->get('twingle_protect_recurring');
if (empty($protection_on)) {
return;
}
if (empty($protection_on)) return;
// load the recurring contribution
$recurring_contribution = civicrm_api3('ContributionRecur', 'getsingle', [
'return' => 'trxn_id,contribution_status_id,payment_instrument_id,contact_id',
'id' => $recurring_contribution_id,
]);
'return' => 'trxn_id,contribution_status_id,payment_instrument_id,contact_id',
'id' => $recurring_contribution_id]);
// check if this is a SEPA transaction (doesn't concern us)
if (self::isSDD($recurring_contribution['payment_instrument_id'])) {
return;
}
if (self::isSDD($recurring_contribution['payment_instrument_id'])) return;
// see if this recurring contribution is from Twingle
if (!self::isTwingleRecurringContribution($recurring_contribution_id, $recurring_contribution)) {
@ -73,29 +59,22 @@ class CRM_Twingle_Tools {
}
// check if it's really a termination (i.e. current status is 2 or 5)
if (!in_array($recurring_contribution['contribution_status_id'], [2, 5])) {
return;
}
if (!in_array($recurring_contribution['contribution_status_id'], [2,5])) return;
// this _IS_ on of the cases where we should step in:
CRM_Twingle_Tools::processRecurringContributionTermination(
$recurring_contribution_id,
$recurring_contribution
);
CRM_Twingle_Tools::processRecurringContributionTermination($recurring_contribution_id, $recurring_contribution);
}
/**
* @param $recurring_contribution_id int recurring contribution ID to check
* @param $recurring_contribution array recurring contribution data, optional
* @return bool|null true, false or null if can't be determined
* @throws \CRM_Core_Exception
* @throws CiviCRM_API3_Exception
*/
public static function isTwingleRecurringContribution($recurring_contribution_id, $recurring_contribution = NULL) {
// this currently only works with prefixes
$prefix = Civi::settings()->get('twingle_prefix');
if (empty($prefix)) {
return NULL;
}
if (empty($prefix)) return null;
// load recurring contribution if necessary
if (empty($recurring_contribution['trxn_id'])) {
@ -110,20 +89,13 @@ class CRM_Twingle_Tools {
/**
* Execute the recurring contribution protection
*
* @param int $recurring_contribution_id
* Recurring contribution ID.
* @param array<mixed> $recurring_contribution
* Recurring contribution fields.
* @param $recurring_contribution_id int recurring contribution ID
* @param $recurring_contribution array recurring contribution fields
* @throws Exception could be one of the measures
*/
public static function processRecurringContributionTermination(
int $recurring_contribution_id,
array $recurring_contribution
) {
public static function processRecurringContributionTermination($recurring_contribution_id, $recurring_contribution) {
// check if we're suspended
if (self::$protection_suspended) {
return;
}
if (self::$protection_suspended) return;
$protection_mode = Civi::settings()->get('twingle_protect_recurring');
switch ($protection_mode) {
@ -132,53 +104,39 @@ class CRM_Twingle_Tools {
break;
case CRM_Twingle_Config::RCUR_PROTECTION_EXCEPTION:
// phpcs:disable Generic.Files.LineLength.TooLong
throw new BaseException(E::ts(
'This is a Twingle recurring contribution. It should be terminated through the Twingle interface, otherwise it will still be collected.'
));
// phpcs:enable
throw new Exception(E::ts("This is a Twingle recurring contribution. It should be terminated through the Twingle interface, otherwise it will still be collected."));
case CRM_Twingle_Config::RCUR_PROTECTION_ACTIVITY:
// create contact source activity
// first: get the contact ID
if (!empty($recurring_contribution['contact_id'])) {
$target_id = (int) $recurring_contribution['contact_id'];
}
else {
} else {
$target_id = (int) civicrm_api3('ContributionRecur', 'getvalue', [
'id' => $recurring_contribution_id,
'return' => 'contact_id',
]);
'id' => $recurring_contribution_id,
'return' => 'contact_id']);
}
if (!empty($recurring_contribution['trxn_id'])) {
$trxn_id = $recurring_contribution['trxn_id'];
}
else {
} else {
$trxn_id = civicrm_api3('ContributionRecur', 'getvalue', [
'id' => $recurring_contribution_id,
'return' => 'trxn_id',
]);
'id' => $recurring_contribution_id,
'return' => 'trxn_id']);
}
try {
civicrm_api3('Activity', 'create', [
'activity_type_id' => Civi::settings()->get('twingle_protect_recurring_activity_type'),
'subject' => Civi::settings()->get('twingle_protect_recurring_activity_subject'),
'activity_date_time' => date('YmdHis'),
'target_id' => $target_id,
'assignee_id' => Civi::settings()->get('twingle_protect_recurring_activity_assignee'),
'status_id' => Civi::settings()->get('twingle_protect_recurring_activity_status'),
// phpcs:disable Generic.Files.LineLength.TooLong
'details' => E::ts(
"Recurring contribution [%1] (Transaction ID '%2') was terminated by a user. You need to end the corresponding record in Twingle as well, or it will still be collected.",
[1 => $recurring_contribution_id, 2 => $trxn_id]
),
// phpcs:enable
'source_contact_id' => CRM_Core_Session::getLoggedInContactID(),
'activity_type_id' => Civi::settings()->get('twingle_protect_recurring_activity_type'),
'subject' => Civi::settings()->get('twingle_protect_recurring_activity_subject'),
'activity_date_time' => date('YmdHis'),
'target_id' => $target_id,
'assignee_id' => Civi::settings()->get('twingle_protect_recurring_activity_assignee'),
'status_id' => Civi::settings()->get('twingle_protect_recurring_activity_status'),
'details' => E::ts("Recurring contribution [%1] (Transaction ID '%2') was terminated by a user. You need to end the corresponding record in Twingle as well, or it will still be collected.",
[1 => $recurring_contribution_id, 2 => $trxn_id]),
'source_contact_id' => CRM_Core_Session::getLoggedInContactID(),
]);
}
catch (Exception $ex) {
} catch (Exception $ex) {
Civi::log()->warning("TwingleAPI: Couldn't create recurring protection activity: " . $ex->getMessage());
}
break;
@ -192,10 +150,10 @@ class CRM_Twingle_Tools {
/**
* Check if the given payment instrument is SEPA
*
* @param string $payment_instrument_id
* @param $payment_instrument_id string payment instrument
* @return boolean
*/
public static function isSDD(string $payment_instrument_id) {
public static function isSDD($payment_instrument_id) {
static $sepa_payment_instruments = NULL;
if ($sepa_payment_instruments === NULL) {
// init with instrument names
@ -203,9 +161,9 @@ class CRM_Twingle_Tools {
// lookup and add instrument IDs
$lookup = civicrm_api3('OptionValue', 'get', [
'option_group_id' => 'payment_instrument',
'name' => ['IN' => $sepa_payment_instruments],
'return' => 'value',
'option_group_id' => 'payment_instrument',
'name' => ['IN' => $sepa_payment_instruments],
'return' => 'value'
]);
foreach ($lookup['values'] as $payment_instrument) {
$sepa_payment_instruments[] = $payment_instrument['value'];
@ -217,17 +175,18 @@ class CRM_Twingle_Tools {
/**
* Get a CiviSEPA mandate for the given contribution ID
*
* @param int $contribution_id contribution ID *or* recurring contribution ID
* @return array<string, mixed>|null mandate or null
* @param $contribution_id integer contribution ID *or* recurring contribution ID
* @return integer mandate ID or null
*/
public static function getMandateFor(int $contribution_id): ?array {
public static function getMandateFor($contribution_id) {
$contribution_id = (int) $contribution_id;
if ($contribution_id) {
try {
// try recurring mandate
$rcur_mandate = civicrm_api3('SepaMandate', 'get', [
'entity_id' => $contribution_id,
'entity_table' => 'civicrm_contribution_recur',
'type' => 'RCUR',
'entity_id' => $contribution_id,
'entity_table' => 'civicrm_contribution_recur',
'type' => 'RCUR',
]);
if ($rcur_mandate['count'] == 1) {
return reset($rcur_mandate['values']);
@ -236,19 +195,17 @@ class CRM_Twingle_Tools {
// try OOFF mandate
// try recurring mandate
$ooff_mandate = civicrm_api3('SepaMandate', 'get', [
'entity_id' => $contribution_id,
'entity_table' => 'civicrm_contribution',
'type' => 'OOFF',
'entity_id' => $contribution_id,
'entity_table' => 'civicrm_contribution',
'type' => 'OOFF',
]);
if ($ooff_mandate['count'] == 1) {
return reset($ooff_mandate['values']);
}
}
catch (Exception $ex) {
} catch (Exception $ex) {
Civi::log()->warning("CRM_Twingle_Tools::getMandate failed for [{$contribution_id}]: " . $ex->getMessage());
}
}
return NULL;
}
}

View file

@ -1,20 +1,4 @@
<?php
/*------------------------------------------------------------+
| SYSTOPIA Twingle Integration |
| Copyright (C) 2019 SYSTOPIA |
| Author: J. Schuppe (schuppe@systopia.de) |
+-------------------------------------------------------------+
| This program is released as free software under the |
| Affero GPL license. You can redistribute it and/or |
| modify it under the terms of this license which you |
| can read by viewing the included agpl.txt or online |
| at www.gnu.org/licenses/agpl.html. Removal of this |
| copyright header is strictly prohibited without |
| written permission from the original author(s). |
+-------------------------------------------------------------*/
declare(strict_types = 1);
use CRM_Twingle_ExtensionUtil as E;
/**
@ -25,7 +9,7 @@ class CRM_Twingle_Upgrader extends CRM_Extension_Upgrader_Base {
/**
* Installer script
*/
public function install(): void {
public function install() {
// create a DB table for the twingle profiles
$this->executeSqlFile('sql/civicrm_twingle_profile.sql');
@ -36,14 +20,14 @@ class CRM_Twingle_Upgrader extends CRM_Extension_Upgrader_Base {
/**
* Example: Run an external SQL script when the module is uninstalled.
*
* public function uninstall() {
* $this->executeSqlFile('sql/myuninstall.sql');
* }
*
* /**
public function uninstall() {
$this->executeSqlFile('sql/myuninstall.sql');
}
/**
* Copy financial_type_id setting to new setting financial_type_id_recur.
*/
public function upgrade_4000(): bool {
public function upgrade_4000() {
$this->ctx->log->info('Applying update 4000: Copying Financial type to new setting Financial type (recurring).');
foreach (CRM_Twingle_Profile::getProfiles() as $profile) {
$profile->setAttribute('financial_type_id_recur', $profile->getAttribute('financial_type_id'));
@ -57,7 +41,7 @@ class CRM_Twingle_Upgrader extends CRM_Extension_Upgrader_Base {
*
* @link https://civicrm.org/advisory/civi-sa-2019-21-poi-saved-search-and-report-instance-apis
*/
public function upgrade_5011(): bool {
public function upgrade_5011() {
// Do not use CRM_Core_BAO::getItem() or Civi::settings()->get().
// Extract and unserialize directly from the database.
$twingle_profiles_query = CRM_Core_DAO::executeQuery("
@ -78,74 +62,26 @@ class CRM_Twingle_Upgrader extends CRM_Extension_Upgrader_Base {
* @return TRUE on success
* @throws Exception
*/
public function upgrade_5140(): bool {
public function upgrade_5140() {
$this->ctx->log->info('Converting twingle profiles.');
// create a DB table for the twingle profiles
$this->executeSqlFile('sql/civicrm_twingle_profile.sql');
// migrate the current profiles
if (is_array($profiles_data = Civi::settings()->get('twingle_profiles'))) {
if ($profiles_data = Civi::settings()->get('twingle_profiles')) {
foreach ($profiles_data as $profile_name => $profile_data) {
$profile = new CRM_Twingle_Profile($profile_name, $profile_data);
$data = json_encode($profile->getData());
CRM_Core_DAO::executeQuery(<<<SQL
INSERT IGNORE INTO civicrm_twingle_profile(name,config,last_access,access_counter) VALUES (%1, %2, NOW(), 0)
SQL,
CRM_Core_DAO::executeQuery(
"INSERT IGNORE INTO civicrm_twingle_profile(name,config,last_access,access_counter) VALUES (%1, %2, NOW(), 0)",
[
1 => [$profile_name, 'String'],
2 => [$data, 'String'],
2 => [$data, 'String']
]);
}
}
return TRUE;
}
/**
* Upgrade to 1.5.0
*
* - Activate mapping of `purpose` and `user_extra_field` to notes in each existing profile to
* maintain default behavior after making the fields optional.
*
* @return bool
* @throws \Civi\Core\Exception\DBQueryException
* @throws \Civi\Twingle\Exceptions\ProfileException
*/
public function upgrade_5150(): bool {
$this->ctx->log->info('Activate mapping of `purpose` and `user_extra_field` to notes in each existing profile.');
foreach (CRM_Twingle_Profile::getProfiles() as $profile) {
$profile_changed = FALSE;
/** @phpstan-var array<string> $contribution_notes */
$contribution_notes = $profile->getAttribute('map_as_contribution_notes', []);
/** @phpstan-var array<string> $contact_notes */
$contact_notes = $profile->getAttribute('map_as_contact_notes', []);
if (!in_array('purpose', $contribution_notes, TRUE)) {
$profile->setAttribute('map_as_contribution_notes', array_merge($contribution_notes, ['purpose']));
$profile_changed = TRUE;
}
if (!in_array('user_extrafield', $contact_notes, TRUE)) {
$profile->setAttribute('map_as_contact_notes', array_merge($contact_notes, ['user_extrafield']));
$profile_changed = TRUE;
}
if ($profile_changed) {
$profile->saveProfile();
}
}
return TRUE;
}
/**
* The Upgrade to 1.5.1 creates the tables civicrm_twingle_product and
* civicrm_twingle_shop.
*
* @return TRUE on success
*/
public function upgrade_5151() {
$this->ctx->log->info('Creating tables for Twingle Shop.');
$this->executeSqlFile('sql/civicrm_twingle_shop.sql');
return TRUE;
}
}

View file

@ -1,12 +0,0 @@
<?php
namespace Civi\Api4;
/**
* TwingleShop entity.
*
* @package Civi\Api4
*/
class TwingleProduct extends Generic\DAOEntity {
}

View file

@ -1,12 +0,0 @@
<?php
namespace Civi\Api4;
/**
* TwingleShop entity.
*
* @package Civi\Api4
*/
class TwingleShop extends Generic\DAOEntity {
}

View file

@ -1,65 +0,0 @@
<?php
/*
* Copyright (C) 2024 SYSTOPIA GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation in version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types = 1);
namespace Civi\Twingle\Exceptions;
use CRM_Twingle_ExtensionUtil as E;
/**
* A simple custom exception class that indicates a problem within a class
* of the Twingle API extension.
*/
class BaseException extends \Exception {
/**
* @var int|string
*/
protected $code;
protected string $log_message;
/**
* BaseException Constructor
* @param string $message
* Error message
* @param string $error_code
* A meaningful error code
*/
public function __construct(string $message = '', string $error_code = '') {
parent::__construct($message, 1);
$this->log_message = !empty($message) ? E::LONG_NAME . ': ' . $message : '';
$this->code = $error_code;
}
/**
* Returns the error message, but with the extension name prefixed.
* @return string
*/
public function getLogMessage() {
return $this->log_message;
}
/**
* Returns the error code.
* @return string
*/
public function getErrorCode() {
return $this->code;
}
}

View file

@ -1,35 +0,0 @@
<?php
/*
* Copyright (C) 2024 SYSTOPIA GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation in version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types = 1);
namespace Civi\Twingle\Exceptions;
/**
* A simple custom exception that indicates a problem within the
* CRM_Twingle_Profile class
*/
class ProfileException extends BaseException {
public const ERROR_CODE_PROFILE_NOT_FOUND = 'profile_not_found';
public const ERROR_CODE_DEFAULT_PROFILE_NOT_FOUND = 'default_profile_not_found';
public const ERROR_CODE_COULD_NOT_SAVE_PROFILE = 'could_not_save_profile';
public const ERROR_CODE_COULD_NOT_RESET_PROFILE = 'could_not_reset_profile';
public const ERROR_CODE_COULD_NOT_DELETE_PROFILE = 'could_not_delete_profile';
public const ERROR_CODE_UNKNOWN_PROFILE_ATTRIBUTE = 'unknown_profile_attribute';
}

View file

@ -1,54 +0,0 @@
<?php
/*
* Copyright (C) 2024 SYSTOPIA GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation in version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types = 1);
namespace Civi\Twingle\Exceptions;
/**
* A simple custom error indicating a problem with the validation of the
* CRM_Twingle_Profile
*/
class ProfileValidationError extends BaseException {
private string $affected_field_name;
public const ERROR_CODE_PROFILE_VALIDATION_FAILED = 'profile_validation_failed';
public const ERROR_CODE_PROFILE_VALIDATION_WARNING = 'profile_validation_warning';
/**
* ProfileValidationError Constructor
* @param string $affected_field_name
* The name of the profile field which caused the exception
* @param string $message
* Error message
* @param string $error_code
* A meaningful error code
*/
public function __construct(string $affected_field_name, string $message = '', string $error_code = '') {
parent::__construct($message, $error_code);
$this->affected_field_name = $affected_field_name;
}
/**
* Returns the name of the profile field that caused the exception.
* @return string
*/
public function getAffectedFieldName() {
return $this->affected_field_name;
}
}

View file

@ -1,269 +0,0 @@
<?php /** @noinspection ALL */
namespace Civi\Twingle\Shop;
use CRM_TwingleCampaign_ExtensionUtil as E;
use Civi\Twingle\Shop\Exceptions\ApiCallError;
/**
* This class communicates with the Twingle API via cURL.
* To keep the overhead of initialization low, this class is implemented as
* a singleton. Please use CRM_Twingle_TwingleApiCall::singleton() to retrieve
* an instance.
*/
class ApiCall {
/**
* Twingle API url
*/
const BASE_URL = '.twingle.de/api';
/**
* The transfer protocol
*/
const PROTOCOL = 'https://';
/**
* The singleton object
* @var \Civi\Twingle\Shop\ApiCall $singleton
*/
public static ApiCall $singleton;
/**
* Your Twingle API token.
* You can request an API token from Twingle support: <hilfe@twingle.de>
* @var string $apiToken
*/
private string $apiToken;
/**
* The ID of your organization in the Twingle database.
* Automatically retrieved by sending a request with the associated API token.
* @var int $organisationId
*/
public int $organisationId;
/**
* This boolean indicates whether the connection was successful.
*
* @var bool $isConnected
*/
public bool $isConnected;
/**
* Limit the number of items requested per API call.
* @var int $limit
*/
public int $limit = 40;
/**
* Header for cURL request.
* @var string[] $header
*/
private array $header;
/**
* The cURL wrapper
* @var \Civi\Twingle\Shop\CurlWrapper $curlWrapper
*/
private CurlWrapper $curlWrapper;
/**
* Protected TwingleApiCall constructor.
* Use \Civi\Twingle\ApiCall::singleton() instead.
* @param \Civi\Twingle\Shop\CurlWrapper $curlWrapper
*/
protected function __construct(CurlWrapper $curlWrapper) {
$this->curlWrapper = $curlWrapper;
$this->isConnected = FALSE;
}
/**
* Returns \Civi\Twingle\Shop\ApiCall singleton
*
* @param \Civi\Twingle\Shop\CurlWrapper|null $curlWrapper
* Optional cURL wrapper for testing purposes
* @return \Civi\Twingle\Shop\ApiCall
*/
public static function singleton(CurlWrapper $curlWrapper = null): ApiCall {
if (empty(self::$singleton)) {
$curlWrapper = $curlWrapper ?? new CurlWrapper();
self::$singleton = new ApiCall($curlWrapper);
return self::$singleton;
}
else {
return self::$singleton;
}
}
/**
* Try to connect to the Twingle API and retrieve the organisation ID.
*
* @return bool
* returns TRUE if the connection was successfully established
*
* @throws \Civi\Twingle\Shop\Exceptions\ApiCallError
*/
public function connect(): bool {
$this->isConnected = FALSE;
try {
// Get api token from settings
$apiToken = \Civi::settings()->get("twingle_access_key");
if (empty($apiToken)) {
throw new \TypeError();
}
$this->apiToken = $apiToken;
} catch (\TypeError $e) {
throw new ApiCallError(
E::ts("Could not find Twingle API token"),
ApiCallError::ERROR_CODE_API_TOKEN_MISSING,
);
}
$this->header = [
"x-access-code: $this->apiToken",
'Content-Type: application/json',
];
$url = self::PROTOCOL . 'organisation' . self::BASE_URL . "/";
$curl = curl_init($url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($curl, CURLOPT_HTTPHEADER, $this->header);
$response = json_decode(curl_exec($curl), TRUE);
if (empty($response)) {
curl_close($curl);
throw new ApiCallError(
E::ts("Call to Twingle API failed. Please check your api token."),
ApiCallError::ERROR_CODE_CONNECTION_FAILED,
);
}
self::check_response_and_close($response, $curl);
$this->organisationId = array_column($response, 'id')[0];
$this->isConnected = TRUE;
return $this->isConnected;
}
/**
* Check response on cURL
*
* @param $response
* the cURL response to check
* @param $curl
* the cURL resource
*
* @return bool
* returns true if the response is fine
*
* @throws \Civi\Twingle\Shop\Exceptions\ApiCallError
*/
protected static function check_response_and_close($response, $curl) {
$curl_status_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
if ($response == FALSE) {
throw new ApiCallError(
E::ts('GET curl failed'),
ApiCallError::ERROR_CODE_GET_REQUEST_FAILED,
);
}
if ($curl_status_code == 404) {
throw new ApiCallError(
E::ts('http status code 404 (not found)'),
ApiCallError::ERROR_CODE_404,
);
}
elseif ($curl_status_code == 500) {
throw new ApiCallError(
E::ts('https status code 500 (internal error)'),
ApiCallError::ERROR_CODE_500,
);
}
return TRUE;
}
/**
* Sends a GET cURL and returns the result array.
*
* @param $entity
* Twingle entity
*
* @param null $params
* Optional GET parameters
*
* @return array
* Returns the result array of the or FALSE, if the cURL failed
* @throws \Civi\Twingle\Shop\Exceptions\ApiCallError
*/
public function get(
string $entity,
string $entityId = NULL,
string $endpoint = NULL,
string $endpointId = NULL,
array $params = NULL
): array {
// Throw an error, if connection is not yet established
if ($this->isConnected == FALSE) {
throw new ApiCallError(
E::ts("Connection not yet established. Use connect() method."),
ApiCallError::ERROR_CODE_NOT_CONNECTED,
);
}
// Build URL and initialize cURL
$url = self::PROTOCOL . $entity . self::BASE_URL;
if (!empty($entityId)) {
$url .= "/$entityId";
}
if (!empty($endpoint)) {
$url .= "/$endpoint";
}
if (!empty($endpointId)) {
$url .= "/$endpointId";
}
if (!empty($params)) {
$url .= '?' . http_build_query($params);
}
$curl = curl_init($url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($curl, CURLOPT_HTTPHEADER, $this->header);
// Execute cURL
$response = json_decode(curl_exec($curl), TRUE);
self::check_response_and_close($response, $curl);
return $response;
}
}
/**
* A simple wrapper for the cURL functions to allow for easier testing.
*/
class CurlWrapper {
public function init($url) {
return curl_init($url);
}
public function setopt($ch, $option, $value) {
return curl_setopt($ch, $option, $value);
}
public function exec($ch) {
return curl_exec($ch);
}
public function getinfo($ch, $option) {
return curl_getinfo($ch, $option);
}
public function close($ch) {
curl_close($ch);
}
}

View file

@ -1,19 +0,0 @@
<?php
namespace Civi\Twingle\Shop\Exceptions;
use Civi\Twingle\Exceptions\BaseException as BaseException;
/**
* A simple custom exception that indicates a problem within the
* Civi\Twingle\Shop\ApiCall class
*/
class ApiCallError extends BaseException {
public const ERROR_CODE_API_TOKEN_MISSING = "api_token_missing";
public const ERROR_CODE_CONNECTION_FAILED = "connection_failed";
public const ERROR_CODE_NOT_CONNECTED = "not_connected";
public const ERROR_CODE_GET_REQUEST_FAILED = "get_request_failed";
public const ERROR_CODE_404 = "404";
public const ERROR_CODE_500 = "500";
}

View file

@ -1,14 +0,0 @@
<?php
namespace Civi\Twingle\Shop\Exceptions;
use Civi\Twingle\Exceptions\BaseException as BaseException;
/**
* A simple custom exception that indicates a problem within the Line Items
*/
class LineItemException extends BaseException {
public const ERROR_CODE_CONTRIBUTION_NOT_FOUND = "contribution_not_found";
}

View file

@ -1,26 +0,0 @@
<?php
namespace Civi\Twingle\Shop\Exceptions;
use Civi\Twingle\Exceptions\BaseException as BaseException;
/**
* A simple custom exception that indicates a problem within the
* CRM_Twingle_Product class
*/
class ProductException extends BaseException {
public const ERROR_CODE_ATTRIBUTE_WRONG_DATA_TYPE = 'attribute_wrong_data_type';
public const ERROR_CODE_PRICE_FIELD_ALREADY_EXISTS = 'price_field_already_exists';
public const ERROR_CODE_PRICE_FIELD_VALUE_NOT_FOUND = 'price_field_value_not_found';
public const ERROR_CODE_PRICE_FIELD_NOT_FOUND = 'price_field_not_found';
public const ERROR_CODE_PRICE_SET_NOT_FOUND = 'price_set_not_found';
public const ERROR_CODE_COULD_NOT_CREATE_PRICE_FIELD = 'price_field_creation_failed';
public const ERROR_CODE_COULD_NOT_CREATE_PRICE_FIELD_VALUE = 'price_field_value_creation_failed';
public const ERROR_CODE_COULD_NOT_DELETE_PRICE_FIELD = 'price_field_deletion_failed';
public const ERROR_CODE_COULD_NOT_DELETE_PRICE_FIELD_VALUE = 'price_field_value_deletion_failed';
public const ERROR_CODE_PRICE_FIELD_STILL_EXISTS = 'price_field_still_exists';
public const ERROR_CODE_COULD_NOT_CREATE_PRODUCT = 'product_creation_failed';
public const ERROR_CODE_COULD_NOT_GET_PRODUCTS = 'product_retrieval_failed';
public const ERROR_CODE_COULD_NOT_DELETE_PRICE_SET = 'price_set_deletion_failed';
}

View file

@ -1,22 +0,0 @@
<?php
namespace Civi\Twingle\Shop\Exceptions;
use Civi\Twingle\Exceptions\BaseException as BaseException;
/**
* A simple custom exception that indicates a problem within the
* CRM_Twingle_Shop class
*/
class ShopException extends BaseException {
public const ERROR_CODE_NOT_A_SHOP = "not_a_shop";
public const ERROR_CODE_COULD_NOT_GET_PROJECTS = "could_not_get_projects";
public const ERROR_CODE_COULD_NOT_FIND_SHOP_IN_DB = "could_not_find_shop_in_db";
public const ERROR_CODE_PRICE_SET_ALREADY_EXISTS = "price_set_already_exists";
public const ERROR_CODE_COULD_NOT_CREATE_PRICE_SET = "price_set_creation_failed";
public const ERROR_CODE_PRICE_SET_NOT_FOUND = "price_set_not_found";
public const ERROR_CODE_COULD_NOT_DELETE_PRICE_SET = "price_set_deletion_failed";
public const ERROR_CODE_ATTRIBUTE_WRONG_DATA_TYPE = "attribute_wrong_data_type";
}

View file

@ -1,154 +0,0 @@
<?php
namespace Civi\Twingle\Shop\Utils;
/**
* Filter for allowed attributes.
*
* @param array $data
* Data to filter.
* @param array $allowed_attributes
* Allowed attributes.
* @param array $can_be_zero
* Attributes that can be zero.
*
* @return void
*/
function filter_attributes(array &$data, array $allowed_attributes, array $can_be_zero = Null): void {
$can_be_zero = $can_be_zero ?? [];
// Remove empty values if not of type int
$data = array_filter(
$data,
function($value, $key) use ($can_be_zero) {
return !empty($value) || in_array($key, $can_be_zero);
},
ARRAY_FILTER_USE_BOTH
);
// Filter for allowed attributes
$data = array_intersect_key(
$data,
$allowed_attributes
);
}
/**
* Convert string values to int.
*
* @param array $data
* @param array $str_to_int_conversion
*
* @return void
* @throws \Exception
*/
function convert_str_to_int(array &$data, array $str_to_int_conversion): void {
// Convert string to int
foreach ($str_to_int_conversion as $attribute) {
if (isset($data[$attribute]) && $data[$attribute] !== '') {
try {
$data[$attribute] = (int) $data[$attribute];
} catch (\Exception $e) {
throw new \Exception(
"Could not convert attribute '$attribute' to int."
);
}
}
}
}
/**
* Convert int values to bool.
*
* @param array $data
* @param array $int_to_bool_conversion
*
* @return void
* @throws \Exception
*/
function convert_int_to_bool(array &$data, array $int_to_bool_conversion): void {
// Convert int to bool
foreach ($int_to_bool_conversion as $attribute) {
if (isset($data[$attribute])) {
try {
$data[$attribute] = (bool) $data[$attribute];
}
catch (\Exception $e) {
throw new \Exception(
"Could not convert attribute '$attribute' to bool."
);
}
}
}
}
/**
* Convert string values to date.
*
* @param array $data
* @param array $str_to_date_conversion
*
* @return void
* @throws \Exception
*/
function convert_str_to_date(array &$data, array $str_to_date_conversion): void {
// Convert string to date
foreach ($str_to_date_conversion as $attribute) {
if (isset($data[$attribute]) && is_string($data[$attribute])) {
try {
$data[$attribute] = strtotime($data[$attribute]);
}
catch (\Exception $e) {
throw new \Exception(
"Could not convert attribute '$attribute' to date."
);
}
}
}
}
/**
* Convert an empty string to null.
*
* @param array $data
* @param array $empty_string_to_null
*
* @return void
*/
function convert_empty_string_to_null(array &$data, array $empty_string_to_null): void {
foreach ($empty_string_to_null as $attribute) {
if (isset($data[$attribute]) && $data[$attribute] === '') {
$data[$attribute] = NULL;
}
}
}
/**
* Validate data types. Throws an exception if data type is not valid.
*
* @param array $data
* @param array $allowed_attributes
*
* @return void
* @throws \Exception
*/
function validate_data_types(array &$data, array $allowed_attributes): void {
foreach ($data as $key => $value) {
// Skip empty values
if (empty($value)) {
continue;
}
// Find expected data type
$expected_data_type = strtolower(\CRM_Utils_Type::typeToString($allowed_attributes[$key])); // It could be so easy...
// Validate data type
if (!\CRM_Utils_Type::validatePhpType($value, $expected_data_type)) {
$given_type = gettype($value);
throw new \Exception(
"Data type of attribute '$key' is $given_type, but $expected_data_type was expected."
);
}
}
}

View file

@ -13,56 +13,54 @@
| written permission from the original author(s). |
+-------------------------------------------------------------*/
declare(strict_types = 1);
use CRM_Twingle_ExtensionUtil as E;
/**
* TwingleDonation.Cancel API specification (optional)
* This is used for documentation and validation.
*
* @param array<string, array<string, mixed>> $params description of fields supported by this API call
* @param array $params description of fields supported by this API call
*
* @return void
*
* @see http://wiki.civicrm.org/confluence/display/CRMDOC/API+Architecture+Standards
*/
function _civicrm_api3_twingle_donation_Cancel_spec(&$params) {
$params['project_id'] = [
$params['project_id'] = array(
'name' => 'project_id',
'title' => E::ts('Project ID'),
'type' => CRM_Utils_Type::T_STRING,
'api.required' => 1,
'description' => E::ts('The Twingle project ID.'),
];
$params['trx_id'] = [
);
$params['trx_id'] = array(
'name' => 'trx_id',
'title' => E::ts('Transaction ID'),
'type' => CRM_Utils_Type::T_STRING,
'api.required' => 1,
'description' => E::ts('The unique transaction ID of the donation'),
];
$params['cancelled_at'] = [
);
$params['cancelled_at'] = array(
'name' => 'cancelled_at',
'title' => E::ts('Cancelled at'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 1,
'description' => E::ts('The date when the donation was cancelled, format: YmdHis.'),
];
$params['cancel_reason'] = [
);
$params['cancel_reason'] = array(
'name' => 'cancel_reason',
'title' => E::ts('Cancel reason'),
'type' => CRM_Utils_Type::T_STRING,
'api.required' => 1,
'description' => E::ts('The reason for the donation being cancelled.'),
];
);
}
/**
* TwingleDonation.Cancel API
*
* @param array<string, mixed> $params
* @return array<string, mixed> API result descriptor
* @param array $params
* @return array API result descriptor
* @see civicrm_api3_create_success
* @see civicrm_api3_create_error
*/
@ -75,7 +73,7 @@ function civicrm_api3_twingle_donation_Cancel($params) {
try {
// Validate date for parameter "cancelled_at".
if (!DateTime::createFromFormat('YmdHis', $params['cancelled_at'])) {
throw new CRM_Core_Exception(
throw new CiviCRM_API3_Exception(
E::ts('Invalid date for parameter "cancelled_at".'),
'invalid_format'
);
@ -84,15 +82,15 @@ function civicrm_api3_twingle_donation_Cancel($params) {
// Retrieve (recurring) contribution.
$default_profile = CRM_Twingle_Profile::getProfile('default');
try {
$contribution = civicrm_api3('Contribution', 'getsingle', [
$contribution = civicrm_api3('Contribution', 'getsingle', array(
'trxn_id' => $default_profile->getTransactionID($params['trx_id']),
]);
));
$contribution_type = 'Contribution';
}
catch (CRM_Core_Exception $exception) {
$contribution = civicrm_api3('ContributionRecur', 'getsingle', [
'trxn_id' => $default_profile->getTransactionID($params['trx_id']),
]);
catch (CiviCRM_API3_Exception $exception) {
$contribution = civicrm_api3('ContributionRecur', 'getsingle', array(
'trxn_id' => $default_profile->getTransactionID($params['trx_id']),
));
$contribution_type = 'ContributionRecur';
}
@ -101,10 +99,10 @@ function civicrm_api3_twingle_donation_Cancel($params) {
&& CRM_Twingle_Tools::isSDD($contribution['payment_instrument_id'])
) {
// End SEPA mandate if applicable.
$mandate = CRM_Twingle_Tools::getMandateFor((int) $contribution['id']);
$mandate = CRM_Twingle_Tools::getMandateFor($contribution['id']);
if (!$mandate) {
throw new CRM_Core_Exception(
E::ts('SEPA Mandate for contribution [%1 not found.', [1 => $contribution['id']]),
throw new CiviCRM_API3_Exception(
E::ts("SEPA Mandate for contribution [%1 not found.", [1 => $contribution['id']]),
'api_error'
);
}
@ -112,13 +110,12 @@ function civicrm_api3_twingle_donation_Cancel($params) {
// Mandates can not be terminated in the past.
$end_date = date_create_from_format('YmdHis', $params['cancelled_at']);
if (FALSE !== $end_date) {
if ($end_date) {
// Mandates can not be terminated in the past:
$end_date = date('Ymd', max(
time(),
$end_date->getTimestamp()));
}
else {
} else {
// end date couldn't be parsed, use 'now'
$end_date = date('Ymd');
}
@ -128,26 +125,26 @@ function civicrm_api3_twingle_donation_Cancel($params) {
$end_date,
$params['cancel_reason']
)) {
throw new CRM_Core_Exception(
throw new CiviCRM_API3_Exception(
E::ts('Could not terminate SEPA mandate'),
'api_error'
);
}
// Retrieve updated contribution for return value.
$contribution = civicrm_api3($contribution_type, 'getsingle', [
$contribution = civicrm_api3($contribution_type, 'getsingle', array(
'id' => $contribution['id'],
]);
));
}
else {
// regular contribution
CRM_Twingle_Tools::$protection_suspended = TRUE;
$contribution = civicrm_api3($contribution_type, 'create', [
$contribution = civicrm_api3($contribution_type, 'create', array(
'id' => $contribution['id'],
'cancel_date' => $params['cancelled_at'],
'contribution_status_id' => 'Cancelled',
'cancel_reason' => $params['cancel_reason'],
]);
));
CRM_Twingle_Tools::$protection_suspended = FALSE;
}

View file

@ -13,49 +13,47 @@
| written permission from the original author(s). |
+-------------------------------------------------------------*/
declare(strict_types = 1);
use CRM_Twingle_ExtensionUtil as E;
/**
* TwingleDonation.Endrecurring API specification (optional)
* This is used for documentation and validation.
*
* @param array<string, array<string, mixed>> $params description of fields supported by this API call
* @param array $params description of fields supported by this API call
*
* @return void
*
* @see http://wiki.civicrm.org/confluence/display/CRMDOC/API+Architecture+Standards
*/
function _civicrm_api3_twingle_donation_endrecurring_spec(&$params) {
$params['project_id'] = [
$params['project_id'] = array(
'name' => 'project_id',
'title' => E::ts('Project ID'),
'type' => CRM_Utils_Type::T_STRING,
'api.required' => 1,
'description' => E::ts('The Twingle project ID.'),
];
$params['trx_id'] = [
);
$params['trx_id'] = array(
'name' => 'trx_id',
'title' => E::ts('Transaction ID'),
'type' => CRM_Utils_Type::T_STRING,
'api.required' => 1,
'description' => E::ts('The unique transaction ID of the donation'),
];
$params['ended_at'] = [
);
$params['ended_at'] = array(
'name' => 'ended_at',
'title' => E::ts('Ended at'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 1,
'description' => E::ts('The date when the recurring donation was ended, format: YmdHis.'),
];
);
}
/**
* TwingleDonation.Endrecurring API
*
* @param array<string, mixed> $params
* @return array<string, mixed> API result descriptor
* @param array $params
* @return array API result descriptor
* @see civicrm_api3_create_success
* @see civicrm_api3_create_error
*/
@ -67,17 +65,17 @@ function civicrm_api3_twingle_donation_endrecurring($params) {
try {
// Validate date for parameter "ended_at".
if (FALSE === DateTime::createFromFormat('YmdHis', $params['ended_at'])) {
throw new CRM_Core_Exception(
if (!DateTime::createFromFormat('YmdHis', $params['ended_at'])) {
throw new CiviCRM_API3_Exception(
E::ts('Invalid date for parameter "ended_at".'),
'invalid_format'
);
}
$default_profile = CRM_Twingle_Profile::getProfile('default');
$contribution = civicrm_api3('ContributionRecur', 'getsingle', [
$contribution = civicrm_api3('ContributionRecur', 'getsingle', array(
'trxn_id' => $default_profile->getTransactionID($params['trx_id']),
]);
));
// End SEPA mandate (which ends the associated recurring contribution) or
// recurring contributions.
@ -86,31 +84,30 @@ function civicrm_api3_twingle_donation_endrecurring($params) {
&& CRM_Twingle_Tools::isSDD($contribution['payment_instrument_id'])
) {
// END SEPA MANDATE
$mandate = CRM_Twingle_Tools::getMandateFor((int) $contribution['id']);
if (!isset($mandate)) {
throw new CRM_Core_Exception(
E::ts('SEPA Mandate for recurring contribution [%1 not found.', [1 => $contribution['id']]),
$mandate = CRM_Twingle_Tools::getMandateFor($contribution['id']);
if (!$mandate) {
throw new CiviCRM_API3_Exception(
E::ts("SEPA Mandate for recurring contribution [%1 not found.", [1 => $contribution['id']]),
'api_error'
);
}
$mandate_id = $mandate['id'];
$end_date = date_create_from_format('YmdHis', $params['ended_at']);
if (FALSE !== $end_date) {
if ($end_date) {
// Mandates can not be terminated in the past:
$end_date = date('Ymd', max(
time(),
$end_date->getTimestamp()));
}
else {
} else {
// end date couldn't be parsed, use 'now'
$end_date = date('Ymd');
}
// verify that the mandate has not been terminated in the past
if ($mandate['status'] != 'FRST' && $mandate['status'] != 'RCUR') {
throw new CRM_Core_Exception(
E::ts('SEPA Mandate [%1] already terminated.', [1 => $mandate_id]),
throw new CiviCRM_API3_Exception(
E::ts("SEPA Mandate [%1] already terminated.", [1 => $mandate_id]),
'api_error'
);
}
@ -120,23 +117,23 @@ function civicrm_api3_twingle_donation_endrecurring($params) {
$end_date,
E::ts('Mandate closed by TwingleDonation.Endrecurring API call')
)) {
throw new CRM_Core_Exception(
throw new CiviCRM_API3_Exception(
E::ts('Could not terminate SEPA mandate'),
'api_error'
);
}
$contribution = civicrm_api3('ContributionRecur', 'getsingle', [
$contribution = civicrm_api3('ContributionRecur', 'getsingle', array(
'id' => $contribution['id'],
]);
));
}
else {
// END RECURRING CONTRIBUTION
CRM_Twingle_Tools::$protection_suspended = TRUE;
$contribution = civicrm_api3('ContributionRecur', 'create', [
'id' => $contribution['id'],
'end_date' => $params['ended_at'],
'contribution_status_id' => CRM_Twingle_Submission::CONTRIBUTION_STATUS_COMPLETED,
]);
$contribution = civicrm_api3('ContributionRecur', 'create', array(
'id' => $contribution['id'],
'end_date' => $params['ended_at'],
'contribution_status_id' => CRM_Twingle_Submission::CONTRIBUTION_STATUS_COMPLETED,
));
CRM_Twingle_Tools::$protection_suspended = FALSE;
}

File diff suppressed because it is too large Load diff

View file

@ -1,137 +0,0 @@
<?php
use Civi\Twingle\Shop\Exceptions\ProductException;
use CRM_Twingle_ExtensionUtil as E;
/**
* TwingleProduct.Create API specification (optional)
* This is used for documentation and validation.
*
* @param array $spec description of fields supported by this API call
*
* @see https://docs.civicrm.org/dev/en/latest/framework/api-architecture/
*/
function _civicrm_api3_twingle_product_Create_spec(&$spec) {
$spec['id'] = [
'name' => 'id',
'title' => E::ts('TwingleProduct ID'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 0,
'description' => E::ts('The TwingleProduct ID in the database'),
];
$spec['external_id'] = [
'name' => 'external_id',
'title' => E::ts('Twingle ID'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 1,
'description' => E::ts('External product ID in Twingle database'),
];
$spec['project_id'] = [
'name' => 'project_id',
'title' => E::ts('Twingle Shop ID'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 1,
'description' => E::ts('ID of the corresponding Twingle Shop'),
];
$spec['name'] = [
'name' => 'name',
'title' => E::ts('Product Name'),
'type' => CRM_Utils_Type::T_STRING,
'api.required' => 1,
'description' => E::ts('Name of the product'),
];
$spec['is_active'] = [
'name' => 'is_active',
'title' => E::ts('Is active?'),
'type' => CRM_Utils_Type::T_BOOLEAN,
'api.required' => 0,
'api.default' => 1,
'description' => E::ts('Is the product active?'),
];
$spec['description'] = [
'name' => 'description',
'title' => E::ts('Product Description'),
'type' => CRM_Utils_Type::T_TEXT,
'api.required' => 0,
'description' => E::ts('Short description of the product'),
];
$spec['price'] = [
'name' => 'price',
'title' => E::ts('Product Price'),
'type' => CRM_Utils_Type::T_FLOAT,
'api.required' => 0,
'description' => E::ts('Price of the product'),
];
$spec['sort'] = [
'name' => 'sort',
'title' => E::ts('Sort'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 0,
'description' => E::ts('Sort order of the product'),
];
$spec['financial_type_id'] = [
'name' => 'financial_type_id',
'title' => E::ts('Financial Type ID'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 1,
'description' => E::ts('ID of the financial type of the product'),
];
$spec['twingle_shop_id'] = [
'name' => 'twingle_shop_id',
'title' => E::ts('FK to TwingleShop'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 1,
'description' => E::ts('FK to TwingleShop'),
];
$spec['tw_updated_at'] = [
'name' => 'tw_updated_at',
'title' => E::ts('Twingle timestamp'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 1,
'description' => E::ts('Timestamp of last update in Twingle db'),
];
$spec['price_field_id'] = [
'name' => 'price_field_id',
'title' => E::ts('FK to PriceField'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 0,
'description' => E::ts('FK to PriceField'),
];
}
/**
* TwingleProduct.Create API
*
* @param array $params
*
* @return array
* API result descriptor
*
* @see civicrm_api3_create_success
*
* @throws API_Exception
* @throws \Exception
*/
function civicrm_api3_twingle_product_Create($params): array {
// Filter for allowed params
$allowed_params = [];
_civicrm_api3_twingle_product_Create_spec($allowed_params);
$params = array_intersect_key($params, $allowed_params);
try {
// Create TwingleProduct and load params
$product = new CRM_Twingle_BAO_TwingleProduct();
$product->load($params);
// Save TwingleProduct
$product->add();
$result = $product->getAttributes();
return civicrm_api3_create_success($result, $params, 'TwingleProduct', 'Create');
}
catch (ProductException $e) {
return civicrm_api3_create_error($e->getMessage(), [
'error_code' => $e->getCode(),
'params' => $params,
]);
}
}

View file

@ -1,71 +0,0 @@
<?php
use CRM_Twingle_ExtensionUtil as E;
/**
* TwingleProduct.Delete API specification (optional)
* This is used for documentation and validation.
*
* @param array $spec description of fields supported by this API call
*
* @see https://docs.civicrm.org/dev/en/latest/framework/api-architecture/
*/
function _civicrm_api3_twingle_product_Delete_spec(&$spec) {
$spec['id'] = [
'name' => 'id',
'title' => E::ts('TwingleProduct ID'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 0,
'description' => E::ts('The TwingleProduct ID in CiviCRM'),
];
$spec['external_id'] = [
'name' => 'external_id',
'title' => E::ts('External TwingleProduct ID'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 0,
'description' => E::ts('Twingle\'s ID of the product'),
];
}
/**
* TwingleProduct.Delete API
*
* @param array $params
*
* @return array
* API result descriptor
*
* @throws API_Exception*@throws \Exception
* @throws \Exception
* @see civicrm_api3_create_success
*
*/
function civicrm_api3_twingle_product_Delete($params) {
// Filter for allowed params
$allowed_params = [];
_civicrm_api3_twingle_product_Delete_spec($allowed_params);
$params = array_intersect_key($params, $allowed_params);
// Find TwingleProduct via getsingle API
$product_data = civicrm_api3('TwingleProduct', 'getsingle', $params);
if ($product_data['is_error']) {
return civicrm_api3_create_error($product_data['error_message'],
['error_code' => $product_data['error_code'], 'params' => $params]
);
}
// Get TwingleProduct object
$product = CRM_Twingle_BAO_TwingleProduct::findById($product_data['id']);
// Delete TwingleProduct and associated PriceField and PriceFieldValue
$result = $product->delete();
if ($result) {
return civicrm_api3_create_success(1, $params, 'TwingleProduct', 'Delete');
}
else {
return civicrm_api3_create_error(
E::ts('TwingleProduct could not be deleted.'),
['error_code' => 'delete_failed', 'params' => $params]
);
}
}

View file

@ -1,136 +0,0 @@
<?php
use CRM_Twingle_ExtensionUtil as E;
/**
* TwingleProduct.Get API specification (optional)
* This is used for documentation and validation.
*
* @param array $spec description of fields supported by this API call
*
* @see https://docs.civicrm.org/dev/en/latest/framework/api-architecture/
*/
function _civicrm_api3_twingle_product_Get_spec(&$spec) {
$spec['id'] = [
'name' => 'id',
'title' => E::ts('TwingleProduct ID'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 0,
'description' => E::ts('The TwingleProduct ID in CiviCRM'),
];
$spec['external_id'] = [
'name' => 'external_id',
'title' => E::ts('External TwingleProduct ID'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 0,
'description' => E::ts('Twingle\'s ID of the product'),
];
$spec['price_field_id'] = [
'name' => 'Price Field ID',
'title' => E::ts('Price Field ID'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 0,
'description' => E::ts('FK to civicrm_price_field'),
];
$spec['twingle_shop_id'] = [
'name' => 'twingle_shop_id',
'title' => E::ts('TwingleShop ID'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 0,
'description' => E::ts('The TwingleShop ID in CiviCRM'),
];
$spec['project_identifier'] = [
'name' => 'project_identifier',
'title' => E::ts('Project Identifier'),
'type' => CRM_Utils_Type::T_STRING,
'api.required' => 0,
'description' => E::ts('Twingle project identifier'),
];
$spec['numerical_project_id'] = [
'name' => 'numerical_project_id',
'title' => E::ts('Numerical Project Identifier'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 0,
'description' => E::ts('Twingle numerical project identifier'),
];
}
/**
* TwingleProduct.Get API
*
* @param array $params
*
* @return array
* API result descriptor
*
* @throws API_Exception
* @see civicrm_api3_create_success
*
*/
function civicrm_api3_twingle_product_Get($params) {
// Filter for allowed params
$allowed_params = [];
_civicrm_api3_twingle_product_Get_spec($allowed_params);
$params = array_intersect_key($params, $allowed_params);
// Build query
$query = 'SELECT ctp.* FROM civicrm_twingle_product ctp
INNER JOIN civicrm_twingle_shop cts ON ctp.twingle_shop_id = cts.id';
$query_params = [];
if (!empty($params)) {
$query = $query . ' WHERE';
$possible_params = [];
_civicrm_api3_twingle_product_Get_spec($possible_params);
$param_count = 1;
$altered_params = [];
// Specify product fields to define table prefix
$productFields = array_keys(CRM_Twingle_BAO_TwingleProduct::fields());
// Alter params (prefix with table name)
foreach ($possible_params as $param) {
if (!empty($params[$param['name']])) {
// Prefix with table name
$table_prefix = in_array($param['name'], $productFields) ? 'ctp.' : 'cts.';
$altered_params[] = [
'name' => $table_prefix . $param['name'],
'value' => $params[$param['name']],
'type' => $param['type'],
];
}
}
// Add altered params to query
foreach ($altered_params as $param) {
$query = $query . ' ' . $param['name'] . " = %$param_count AND";
$query_params[$param_count] = [
$param['value'],
$param['type'] == CRM_Utils_Type::T_INT ? 'Integer' : 'String',
];
$param_count++;
}
}
// Cut away last 'AND'
$query = substr($query, 0, -4);
// Execute query
try {
$dao = CRM_Twingle_BAO_TwingleProduct::executeQuery($query, $query_params);
}
catch (Exception $e) {
return civicrm_api3_create_error($e->getMessage(), [
'error_code' => $e->getCode(),
'params' => $params,
]);
}
// Prepare return values
$returnValues = [];
while ($dao->fetch()) {
$returnValues[] = $dao->toArray();
}
return civicrm_api3_create_success($returnValues, $params, 'TwingleProduct', 'Get');
}

View file

@ -1,54 +0,0 @@
<?php
use CRM_Twingle_ExtensionUtil as E;
/**
* TwingleProduct.Getsingle API specification (optional)
* This is used for documentation and validation.
*
* @param array $spec description of fields supported by this API call
*
* @see https://docs.civicrm.org/dev/en/latest/framework/api-architecture/
*/
function _civicrm_api3_twingle_product_Getsingle_spec(&$spec) {
_civicrm_api3_twingle_product_Get_spec($spec);
}
/**
* TwingleProduct.Getsingle API
*
* @param array $params
*
* @return array
* API result descriptor
*
* @see civicrm_api3_create_success
*
* @throws API_Exception
*/
function civicrm_api3_twingle_product_Getsingle($params) {
// Filter for allowed params
$allowed_params = [];
_civicrm_api3_twingle_product_Getsingle_spec($allowed_params);
$params = array_intersect_key($params, $allowed_params);
// Check whether any parameters are set
if (empty($params)) {
return civicrm_api3_create_error(
"At least one parameter must be set",
['error_code' => 'missing_parameter', 'params' => $params]
);
}
// Find TwingleProduct via get API
$returnValues = civicrm_api3('TwingleProduct', 'get', $params);
$count = $returnValues['count'];
// Check whether only a single TwingleProduct is found
if ($count != 1) {
return civicrm_api3_create_error(
"Expected one TwingleProduct but found $count",
['error_code' => 'not_found', 'params' => $params]
);
}
return $returnValues['values'][$returnValues['id']];
}

View file

@ -1,79 +0,0 @@
<?php
use CRM_Twingle_ExtensionUtil as E;
use Civi\Twingle\Shop\Exceptions\ShopException;
/**
* TwingleShop.Create API specification (optional)
* This is used for documentation and validation.
*
* @param array $spec description of fields supported by this API call
*
* @see https://docs.civicrm.org/dev/en/latest/framework/api-architecture/
*/
function _civicrm_api3_twingle_shop_Create_spec(&$spec) {
$spec['project_identifier'] = [
'name' => 'project_identifier',
'title' => E::ts('Project Identifier'),
'type' => CRM_Utils_Type::T_STRING,
'api.required' => 1,
'description' => E::ts('Twingle project identifier'),
];
$spec['numerical_project_id'] = [
'name' => 'numerical_project_id',
'title' => E::ts('Numerical Project Identifier'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 1,
'description' => E::ts('Numerical Twingle project identifier'),
];
$spec['name'] = [
'name' => 'name',
'title' => E::ts('Shop Name'),
'type' => CRM_Utils_Type::T_STRING,
'api.required' => 1,
'description' => E::ts('Name of the shop'),
];
$spec['financial_type_id'] = [
'name' => 'financial_type_id',
'title' => E::ts('Financial Type ID'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 1,
'description' => E::ts('FK to civicrm_financial_type'),
];
}
/**
* TwingleShop.Create API
*
* @param array $params
*
* @return array
* API result descriptor
*
* @see civicrm_api3_create_success
*
* @throws API_Exception
*/
function civicrm_api3_twingle_shop_Create($params) {
// Filter for allowed params
$allowed_params = [];
_civicrm_api3_twingle_shop_Create_spec($allowed_params);
$params = array_intersect_key($params, $allowed_params);
try {
// Create TwingleShop and load params
$shop = new CRM_Twingle_BAO_TwingleShop();
$shop->load($params);
// Save TwingleShop
$result = $shop->add();
// Return success
return civicrm_api3_create_success($result, $params, 'TwingleShop', 'Create');
}
catch (ShopException $e) {
return civicrm_api3_create_error($e->getMessage(), [
'error_code' => $e->getErrorCode(),
'params' => $params,
]);
}
}

View file

@ -1,79 +0,0 @@
<?php
use CRM_Twingle_ExtensionUtil as E;
/**
* TwingleShop.Delete API specification (optional)
* This is used for documentation and validation.
*
* @param array $spec description of fields supported by this API call
*
* @see https://docs.civicrm.org/dev/en/latest/framework/api-architecture/
*/
function _civicrm_api3_twingle_shop_Delete_spec(&$spec) {
$spec['id'] = [
'name' => 'id',
'title' => E::ts('TwingleShop ID'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 0,
'description' => E::ts('The TwingleShop ID in CiviCRM'),
];
$spec['project_identifier'] = [
'name' => 'project_identifier',
'title' => E::ts('Project Identifier'),
'type' => CRM_Utils_Type::T_STRING,
'api.required' => 0,
'description' => E::ts('Twingle project identifier'),
];
}
/**
* TwingleShop.Delete API
*
* @param array $params
*
* @return array
* API result descriptor
*
* @throws \API_Exception
* @throws \Civi\Twingle\Shop\Exceptions\ShopException
* @throws \Exception
* @see civicrm_api3_create_success
*
*/
function civicrm_api3_twingle_shop_Delete($params) {
// Filter for allowed params
$allowed_params = [];
_civicrm_api3_twingle_shop_Delete_spec($allowed_params);
$params = array_intersect_key($params, $allowed_params);
// Find TwingleShop via getsingle API
$shop_data = civicrm_api3('TwingleShop', 'getsingle', $params);
if ($shop_data['is_error']) {
return civicrm_api3_create_error($shop_data['error_message'],
['error_code' => $shop_data['error_code'], 'params' => $params]
);
}
// Get TwingleShop object
$shop = CRM_Twingle_BAO_TwingleShop::findById($shop_data['id']);
// Delete TwingleShop
/** @var \CRM_Twingle_BAO_TwingleShop $shop */
$result = $shop->deleteByConstraint();
if ($result) {
return civicrm_api3_create_success(1, $params, 'TwingleShop', 'Delete');
}
elseif ($result === 0) {
return civicrm_api3_create_error(
E::ts('TwingleShop could not be found.'),
['error_code' => 'not_found', 'params' => $params]
);
}
else {
return civicrm_api3_create_error(
E::ts('TwingleShop could not be deleted.'),
['error_code' => 'delete_failed', 'params' => $params]
);
}
}

View file

@ -1,96 +0,0 @@
<?php
use CRM_Twingle_ExtensionUtil as E;
use Civi\Twingle\Shop\Exceptions\ApiCallError;
use Civi\Twingle\Shop\Exceptions\ProductException;
use Civi\Twingle\Shop\Exceptions\ShopException;
/**
* TwingleShop.Fetch API specification (optional)
* This is used for documentation and validation.
*
* @param array $spec description of fields supported by this API call
*
* @see https://docs.civicrm.org/dev/en/latest/framework/api-architecture/
*/
function _civicrm_api3_twingle_shop_Fetch_spec(&$spec) {
$spec['project_identifiers'] = [
'name' => 'project_identifiers',
'title' => E::ts('Project Identifiers'),
'type' => CRM_Utils_Type::T_STRING,
'api.required' => 1,
'description' => E::ts('Comma separated list of Twingle project identifiers.'),
];
}
/**
* TwingleShop.Fetch API
*
* @param array $params
*
* @return array
* API result descriptor
*
* @see civicrm_api3_create_success
*
* @throws API_Exception
*/
function civicrm_api3_twingle_shop_Fetch($params) {
// Filter for allowed params
$allowed_params = [];
_civicrm_api3_twingle_shop_Fetch_spec($allowed_params);
$params = array_intersect_key($params, $allowed_params);
$returnValues = [];
// Explode string with project IDs and trim
$projectIds = array_map(
function ($projectId) {
return trim($projectId);
},
explode(',', $params['project_identifiers'])
);
// Get products for all projects of type 'shop'
foreach ($projectIds as $projectId) {
try {
$shop = CRM_Twingle_BAO_TwingleShop::findByProjectIdentifier($projectId);
$products = $shop->fetchProducts();
$returnValues[$projectId] = [];
$returnValues[$projectId] += $shop->getAttributes();
$returnValues[$projectId]['products'] = array_map(function ($product) {
return $product->getAttributes();
}, $products);
}
catch (ShopException | ApiCallError | ProductException $e) {
// If this project identifier doesn't belong to a project of type
// 'shop', just skip it
if ($e->getErrorCode() == ShopException::ERROR_CODE_NOT_A_SHOP) {
$returnValues[$projectId] = "project is not of type 'shop'";
continue;
}
// Else, log error and throw exception
else {
Civi::log()->error(
$e->getMessage(),
[
'project_identifier' => $projectId,
'params' => $params,
]
);
return civicrm_api3_create_error($e->getMessage(), [
'error_code' => $e->getErrorCode(),
'project_identifier' => $projectId,
'params' => $params,
]);
}
}
}
return civicrm_api3_create_success(
$returnValues,
$params,
'TwingleShop',
'Fetch'
);
}

View file

@ -1,111 +0,0 @@
<?php
use CRM_Twingle_ExtensionUtil as E;
/**
* TwingleShop.Get API specification (optional)
* This is used for documentation and validation.
*
* @param array $spec description of fields supported by this API call
*
* @see https://docs.civicrm.org/dev/en/latest/framework/api-architecture/
*/
function _civicrm_api3_twingle_shop_Get_spec(&$spec) {
$spec['id'] = [
'name' => 'id',
'title' => E::ts('TwingleShop ID'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 0,
'description' => E::ts('The TwingleShop ID in CiviCRM'),
];
$spec['project_identifier'] = [
'name' => 'project_identifier',
'title' => E::ts('Project Identifier'),
'type' => CRM_Utils_Type::T_STRING,
'api.required' => 0,
'description' => E::ts('Twingle project identifier'),
];
$spec['numerical_project_id'] = [
'name' => 'numerical_project_id',
'title' => E::ts('Numerical Project Identifier'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 0,
'description' => E::ts('Twingle numerical project identifier'),
];
$spec['name'] = [
'name' => 'name',
'title' => E::ts('Name'),
'type' => CRM_Utils_Type::T_STRING,
'api.required' => 0,
'description' => E::ts('Name of the TwingleShop'),
];
$spec['price_set_id'] = [
'name' => 'price_set_id',
'title' => E::ts('Price Set ID'),
'type' => CRM_Utils_Type::T_INT,
'api.required' => 0,
'description' => E::ts('FK to civicrm_price_set'),
];
}
/**
* TwingleShop.Get API
*
* @param array $params
*
* @return array
* API result descriptor
*
* @see civicrm_api3_create_success
*
* @throws API_Exception
*/
function civicrm_api3_twingle_shop_Get($params) {
// Filter for allowed params
$allowed_params = [];
_civicrm_api3_twingle_shop_Get_spec($allowed_params);
$params = array_intersect_key($params, $allowed_params);
// Build query
$query = 'SELECT * FROM civicrm_twingle_shop';
$query_params = [];
if (!empty($params)) {
$query = $query . ' WHERE';
$possible_params = [];
_civicrm_api3_twingle_shop_Get_spec($possible_params);
$param_count = 1;
foreach ($possible_params as $param) {
if (!empty($params[$param['name']])) {
$query = $query . ' ' . $param['name'] . " = %$param_count AND";
$query_params[$param_count] = [
$params[$param['name']],
$param['type'] == CRM_Utils_Type::T_INT ? 'Integer' : 'String'
];
$param_count++;
}
}
// Cut away last 'AND'
$query = substr($query, 0, -4);
}
// Execute query
try {
$dao = CRM_Twingle_BAO_TwingleShop::executeQuery($query, $query_params);
}
catch (\Exception $e) {
return civicrm_api3_create_error($e->getMessage(), [
'error_code' => $e->getCode(),
'params' => $params,
]);
}
// Prepare return values
$returnValues = [];
while ($dao->fetch()) {
$returnValues[] = $dao->toArray();
}
return civicrm_api3_create_success($returnValues, $params, 'TwingleShop', 'Get');
}

View file

@ -1,54 +0,0 @@
<?php
use CRM_Twingle_ExtensionUtil as E;
/**
* TwingleShop.Getsingle API specification (optional)
* This is used for documentation and validation.
*
* @param array $spec description of fields supported by this API call
*
* @see https://docs.civicrm.org/dev/en/latest/framework/api-architecture/
*/
function _civicrm_api3_twingle_shop_Getsingle_spec(&$spec) {
_civicrm_api3_twingle_shop_Get_spec($spec);
}
/**
* TwingleShop.Getsingle API
*
* @param array $params
*
* @return array
* API result descriptor
*
* @see civicrm_api3_create_success
*
* @throws API_Exception
*/
function civicrm_api3_twingle_shop_Getsingle($params) {
// Filter for allowed params
$allowed_params = [];
_civicrm_api3_twingle_shop_Getsingle_spec($allowed_params);
$params = array_intersect_key($params, $allowed_params);
// Check whether any parameters are set
if (empty($params)) {
return civicrm_api3_create_error(
"At least one parameter must be set",
['error_code' => 'missing_parameter', 'params' => $params]
);
}
// Find TwingleShop via get API
$returnValues = civicrm_api3('TwingleShop', 'get', $params);
$count = $returnValues['count'];
// Check whether only a single TwingleShop is found
if ($count != 1) {
return civicrm_api3_create_error(
"Expected one TwingleShop but found $count",
['error_code' => 'not_found', 'params' => $params]
);
}
return $returnValues['values'][$returnValues['id']];
}

View file

@ -1,2 +0,0 @@
The dependencies specified in composer.json of this directory are required to
run phpstan in CI.

View file

@ -1,33 +0,0 @@
{
"minimum-stability": "dev",
"prefer-stable": true,
"config": {
"allow-plugins": {
"civicrm/composer-compile-plugin": false,
"civicrm/composer-downloads-plugin": true,
"cweagans/composer-patches": true
}
},
"require": {
"civicrm/civicrm-core": "^5"
},
"scripts": {
"post-install-or-update": [
"# The following statements are only necessary when the extension is inside a CiviCRM installation, actually not required in CI.",
"# Avoid redeclaration of function \\GuzzleHttp\\http_build_query()",
"if [ -e vendor/civicrm/civicrm-core/guzzle_php81_shim.php ]; then echo '' >vendor/civicrm/civicrm-core/guzzle_php81_shim.php; fi",
"# Avoid redeclaration of function \\GuzzleHttp\\Promise\\queue()",
"if [ -e vendor/guzzlehttp/promises/src/functions.php ]; then echo '' >vendor/guzzlehttp/promises/src/functions.php; fi",
"# Avoid CiviCRM load extensions in vendor",
"if [ -e vendor/civicrm ]; then find vendor/civicrm -name 'info.xml' -delete; fi",
"# Avoid Class 'CRM_AfformAdmin_ExtensionUtil' not found",
"find vendor -name '*.mgd.php' -delete"
],
"post-install-cmd": [
"@post-install-or-update"
],
"post-update-cmd": [
"@post-install-or-update"
]
}
}

View file

@ -1,45 +0,0 @@
{
"name": "systopia/de.systopia.twingle",
"type": "civicrm-ext",
"license": "AGPL-3.0-or-later",
"minimum-stability": "dev",
"prefer-stable": true,
"config": {
"sort-packages": true
},
"require": {
},
"scripts": {
"composer-phpcs": [
"@composer --working-dir=tools/phpcs"
],
"composer-phpstan": [
"@composer --working-dir=tools/phpstan"
],
"composer-phpunit": [
"@composer --working-dir=tools/phpunit"
],
"composer-tools": [
"@composer-phpcs",
"@composer-phpstan",
"@composer-phpunit"
],
"phpcs": [
"@php tools/phpcs/vendor/bin/phpcs"
],
"phpcbf": [
"@php tools/phpcs/vendor/bin/phpcbf"
],
"phpstan": [
"@php tools/phpstan/vendor/bin/phpstan"
],
"phpunit": [
"@php tools/phpunit/vendor/bin/simple-phpunit --coverage-text"
],
"test": [
"@phpcs",
"@phpstan",
"@phpunit"
]
}
}

View file

@ -1,3 +0,0 @@
.twingle-profile-list {
border-bottom: 1px solid #cfcec3;
}

View file

@ -1,37 +0,0 @@
.twingle-shop-table-caption {
font-weight: bold;
font-size: 13px;
padding: 6px;
text-align: left;
}
.twingle-shop-table-divider {
border-top: 1px solid #ddd;
margin-top: 20px;
margin-bottom: 4px;
}
#twingle-shop-spinner {
animation: spin 2s linear infinite;
}
.twingle-shop-table tbody tr {
height: 32px;
}
.twingle-shop-table-button {
margin: 10px 15px 0 0 !important;
}
.twingle-shop-cell-button {
margin: 3px 5px 3px 5px;
}
.strikethrough {
text-decoration: line-through;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View file

@ -14,17 +14,13 @@
<url desc="Support">https://github.com/systopia/de.systopia.twingle/issues</url>
<url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
</urls>
<releaseDate></releaseDate>
<version>1.6-dev</version>
<develStage>dev</develStage>
<releaseDate>2024-11-28</releaseDate>
<version>1.4.1</version>
<develStage>stable</develStage>
<compatibility>
<ver>5.58</ver>
<ver>5.19</ver>
</compatibility>
<comments></comments>
<classloader>
<psr4 prefix="Civi\" path="Civi"/>
<psr0 prefix="CRM_" path="."/>
</classloader>
<comments/>
<requires>
<ext>de.systopia.xcm</ext>
</requires>
@ -36,7 +32,10 @@
<mixin>menu-xml@1.0.0</mixin>
<mixin>mgd-php@1.0.0</mixin>
<mixin>smarty-v2@1.0.1</mixin>
<mixin>entity-types-php@1.0.0</mixin>
</mixins>
<classloader>
<psr0 prefix="CRM_" path="."/>
<psr4 prefix="Civi\" path="Civi"/>
</classloader>
<upgrader>CRM_Twingle_Upgrader</upgrader>
</extension>

View file

@ -1,744 +0,0 @@
/**
* This file contains the JavaScript code for the Twingle Shop integration.
*/
/**
* This function initializes the Twingle Shop integration.
*/
function twingleShopInit() {
cj('#twingle-shop-spinner').hide();
// Run once on page load
load_financial_types();
twingle_shop_active_changed();
twingle_map_products_changed();
twingle_fetch_products();
// Add event listeners
cj('#enable_shop_integration:checkbox').change(twingle_shop_active_changed);
cj('#shop_map_products:checkbox').change(twingle_map_products_changed);
cj('#btn_fetch_products').click(function (event) {
event.preventDefault(); // Prevent the default form submission behavior
twingle_fetch_products();
});
}
// Define financial types as global variable
let financialTypes = {};
/**
* Load financial types from CiviCRM
*/
function load_financial_types() {
CRM.api3('FinancialType', 'get', {
'sequential': 1,
'options': { 'limit': 0 },
}).then(function (result) {
financialTypes = result.values.reduce((obj, item) => {
obj[item.id] = item.name;
return obj;
}, {});
});
}
/**
* Fetches the Twingle products for the given project identifiers.
*/
function twingle_fetch_products() {
let active = cj('#shop_map_products:checkbox:checked').length;
if (active) {
cj('#twingle-shop-spinner').show();
CRM.api3('TwingleShop', 'fetch', {
'project_identifiers': cj('#selectors :input').val(),
}).then(function (result) {
if (result.is_error === 1) {
cj('#btn_fetch_products').crmError(result.error_message, ts('Could not fetch products', []));
cj('#twingle-shop-spinner').hide();
return;
}
buildShopTables(result);
cj('#twingle-shop-spinner').hide();
}, function () {
cj('#btn_fetch_products').crmError(ts('Could not fetch products. Please check your Twingle API key.', []));
cj('#twingle-shop-spinner').hide();
});
}
}
/**
* Update the form fields based on whether shop integration is currently active
*/
function twingle_shop_active_changed() {
let active = cj('#enable_shop_integration:checkbox:checked').length;
if (active) {
cj('.twingle-shop-element').show();
} else {
cj('.twingle-shop-element').hide();
}
}
/**
* Display fetch button and product mapping when the corresponding option is active
*/
function twingle_map_products_changed() {
let active = cj('#shop_map_products:checkbox:checked').length;
if (active) {
cj('.twingle-product-mapping').show();
} else {
cj('.twingle-product-mapping').hide();
}
}
/**
* This function builds the shop tables.
* @param shopData
*/
function buildShopTables(shopData) {
let productTables = [];
// Create table for each project (shop)
for (const key in shopData.values) {
productTables.push(new ProductsTable(shopData.values[key]));
}
// Add table container to DOM
const tableContainer = document.getElementById('tableContainer');
// Add tables to table container
for (const productTable of productTables) {
tableContainer.appendChild(productTable.table);
}
}
/**
* Get the value of the default financial type for the shops defined in this profile.
* @returns {string|string}
*/
function getShopDefaultFinancialType() {
const default_selection = document.getElementById('s2id_shop_financial_type');
const selected = default_selection.getElementsByClassName('select2-chosen')[0];
return selected ? selected.textContent : '';
}
/**
* Get the value of the default financial type.
* @returns {string}
*/
function getShopDefaultFinancialTypeValue() {
const shopDefaultFinancialType = getShopDefaultFinancialType();
return Object.keys(financialTypes).find(key => financialTypes[key] === shopDefaultFinancialType);
}
/**
* This class represents a Twingle Product.
*/
class Product {
/**
* Creates a new Product object.
* @param productData
* @param parentTable
*/
constructor(productData, parentTable) {
this.parentTable = parentTable;
this.setProps(productData);
}
/**
* Sets the properties of this product.
* @param productData
* @private
*/
setProps(productData) {
this.id = productData.id;
this.name = productData.name;
this.isActive = productData.is_active;
this.price = productData.price;
this.sort = productData.sort;
this.description = productData.description;
this.projectId = productData.project_id;
this.externalId = productData.external_id;
this.isOutdated = productData.is_outdated;
this.isOrphaned = productData.is_orphaned;
// this.updatedAt = productData.updated_at;
this.createdAt = productData.created_at;
this.twUpdatedAt = productData.tw_updated_at;
this.financialTypeId = productData.financial_type_id;
this.priceFieldId = productData.price_field_id;
}
/**
* Dumps the product data.
* @returns {{id, name, is_active, price, sort, description, project_id, external_id, financial_type_id, tw_updated_at, twingle_shop_id: *}}
*/
dumpData() {
return {
'id': this.id,
'name': this.name,
'is_active': this.isActive,
'price': this.price,
'sort': this.sort,
'description': this.description,
'project_id': this.projectId,
'external_id': this.externalId,
'financial_type_id': this.financialTypeId,
'price_field_id': this.priceFieldId,
'tw_updated_at': this.twUpdatedAt,
'twingle_shop_id': this.parentTable.id,
};
}
/**
* Creates a button for creating, updating or deleting the price field for
* this product.
* @param action
* @param handler
* @returns {HTMLButtonElement}
* @private
*/
createProductButton(action, handler) {
// Create button
const button = document.createElement('button');
button.id = action + '_twingle_product_tw_' + this.externalId;
button.classList.add('twingle-shop-cell-button');
// Add button text
let text = action === 'create' ? ts('Create', []) : action === 'update' ? ts('Update', []) : ts('Delete', []);
button.textContent = ' ' + ts(text, []);
// Add button handler
if (handler) {
button.onclick = handler;
} else {
button.disabled = true;
}
// Deactivate 'create' button if product hast no financial type
if (action === 'create' && this.financialTypeId === null) {
button.disabled = true;
}
// Deactivate 'update' button if product is not outdated
if (action === 'update' && !this.isOutdated) {
button.disabled = true;
}
// Add icon
const icon = document.createElement('i');
const iconClass = action === 'create' ? 'fa-plus-circle' : action === 'update' ? 'fa-refresh' : 'fa-trash';
icon.classList.add('crm-i', iconClass);
button.insertBefore(icon, button.firstChild);
return button;
}
/**
* Creates a handler for creating a price field for this product.
* @returns {(function(*): void)|*}
* @private
*/
createPriceFieldHandler() {
const self = this;
return function (event) {
event.preventDefault();
const action = event.target.innerText.includes('Update') ? 'updated' : 'created';
CRM.api3('TwingleProduct', 'create', self.dumpData())
.then(function (result) {
if (result.is_error === 1) {
cj('#create_twingle_product_tw_' + self.id).crmError(result.error_message, ts('Could not create Price Field for this product', []));
} else {
self.update(result.values);
CRM.alert(ts(`The Price Field was ${action} successfully.`, []), ts(`Price Field ${action}`, []), 'success', {'expires': 5000});
}
}, function (error) {
cj('#create_twingle_product_tw_' + self.id).crmError(error.message, ts('Could not create Price Field for this product', []));
});
};
}
/**
* Creates a handler for creating a price field for this product.
* @returns {(function(*): void)|*}
* @private
*/
deletePriceFieldHandler() {
let self = this;
return function (event) {
event.preventDefault();
const options = {
'title': ts('Delete Price Field', []),
'message': ts('Are you sure you want to delete the price field associated with this product?', []),
};
CRM.confirm(options)
.on('crmConfirm:yes', function () {
CRM.api3('TwingleProduct', 'delete', { 'id': self.id })
.then(function (result) {
if (result.is_error === 1) {
cj('#create_twingle_product_tw_' + self.id).crmError(result.error_message, ts('Could not delete Price Field', []));
} else {
self.update();
}
CRM.alert(ts('The Price Field was deleted successfully.', []), ts('Price Field deleted', []), 'success', {'expires': 5000});
}, function (error) {
cj('#create_twingle_product_tw_' + self.id).crmError(error.message, ts('Could not delete Price Field', []));
});
});
};
}
/**
* Creates a new row with the product name and buttons for creating, updating
* or deleting the price field for this product.
* @returns {*}
*/
createRow() {
let row;
// Clear row
if (this.row) {
for (let i = this.row.cells.length - 1; i >= 0; i--) {
// Delete everything from row
this.row.deleteCell(i);
}
row = this.row;
} else {
// Create new row element
row = document.createElement('tr');
// Add id to row
row.id = 'twingle_product_tw_' + this.externalId;
}
// Add cell with product name
const nameCell = document.createElement('td');
if (this.isOrphaned) {
nameCell.classList.add('strikethrough');
}
nameCell.textContent = this.name;
row.appendChild(nameCell);
// Add cell for buttons
let buttonCell = row.insertCell(1);
// Add product buttons which allow to create, update or delete the price
// field for this product
if (this.parentTable.id) {
let buttons = this.createProductButtons();
for (const button of buttons) {
buttonCell.appendChild(button);
}
}
// Add financial type dropdown for each product if price set exists
if (this.parentTable.id) {
let dropdown = this.createFinancialTypeDropdown();
const cell = document.createElement('td');
cell.classList.add('twingle-shop-financial-type-select');
cell.appendChild(dropdown);
row.insertCell(2).appendChild(cell);
}
// else add default financial type
else {
const cell = document.createElement('td');
cell.classList.add('twingle-shop-financial-type-default');
cell.innerHTML = '<i>' + getShopDefaultFinancialType() + '</i>';
row.insertCell(1).appendChild(cell);
}
this.row = row;
return this.row;
}
/**
* Determining which actions are available for this product and creating a
* button for each of them.
* @returns {Array} Array of buttons
*/
createProductButtons() {
let actionsAndHandlers = [];
let buttons = [];
// Determine actions; if product has price field id, it can be updated or
// deleted, otherwise it can be created
if (this.priceFieldId) {
if (!this.isOrphaned) {
actionsAndHandlers.push(['update', this.createPriceFieldHandler()]);
}
actionsAndHandlers.push(['delete', this.deletePriceFieldHandler()]);
} else if (!this.isOrphaned) {
actionsAndHandlers.push(['create', this.createPriceFieldHandler()]);
}
// Create button for each action
for (const [action, handler] of actionsAndHandlers) {
buttons.push(this.createProductButton(action, handler));
}
return buttons;
}
/**
* Creates a dropdown for selecting the financial type for this product.
* @returns {HTMLSelectElement}
* @private
*/
createFinancialTypeDropdown() {
// Create new dropdown element
const dropdown = document.createElement('select');
dropdown.id = 'twingle_product_tw_' + this.externalId + '_financial_type';
// Add empty option if no price field exists
if (!this.priceFieldId) {
let option = document.createElement('option');
option.value = '';
option.innerHTML = '&lt;' + ts('select financial type', []) + '&gt;';
option.selected = true;
option.disabled = true;
dropdown.appendChild(option);
}
// Add options for each financial type available in CiviCRM
for (const key in financialTypes) {
let option = document.createElement('option');
option.value = key;
option.text = financialTypes[key]; // financialTypes is defined in twingle_shop.tpl as smarty variable
if (this.financialTypeId !== null && this.financialTypeId.toString() === key) {
option.selected = true;
}
dropdown.appendChild(option);
}
// Add handlers
let self = this;
dropdown.onchange = function () {
// Enable 'create' or 'update' button if financial type is selected
const button = document.getElementById('twingle_product_tw_' + self.externalId).getElementsByClassName('twingle-shop-cell-button')[0];
if (button.textContent.includes('Create') || button.textContent.includes('Update')) {
button.disabled = dropdown.value === '0';
}
// Update financial type
self.financialTypeId = dropdown.value;
};
return dropdown;
}
/**
* Updates the product properties and rebuilds the row.
* @param productData
*/
update(productData = null) {
if (productData) {
this.setProps(productData);
} else {
this.reset();
}
this.createRow();
}
/**
* Resets the product properties.
*/
reset() {
this.financialTypeId = null;
this.priceFieldId = null;
this.isOutdated = null;
this.isOutdated = null;
// this.updatedAt = null;
this.createdAt = null;
this.id = null;
}
}
/**
* This class represents a Twingle Shop.
*/
class ProductsTable {
/**
* Creates a new ProductsTable object.
* @param projectData
*/
constructor(projectData) {
this.setProps(projectData);
}
/**
* Sets the properties of this project.
* @param projectData
* @private
*/
setProps(projectData) {
this.id = projectData.id;
this.name = projectData.name;
this.numericalProjectId = projectData.numerical_project_id;
this.projectIdentifier = projectData.project_identifier;
this.products = projectData.products.map(productData => new Product(productData, this));
this.priceSetId = projectData.price_set_id;
this.table = this.buildTable();
}
/**
* Dumps the projects data.
* @returns {{price_set_id, financial_type_id, numerical_project_id, name, id, project_identifier, products: *}}
*/
dumpData() {
return {
'id': this.id,
'name': this.name,
'numerical_project_id': this.numericalProjectId,
'project_identifier': this.projectIdentifier,
'price_set_id': this.priceSetId,
'products': this.products.map(product => product.dumpData()),
'financial_type_id': getShopDefaultFinancialTypeValue()
};
}
/**
* Builds the table for this project (shop).
* @returns {HTMLTableElement}
* @private
*/
buildTable() {
let table;
// Clear table body
if (this.table) {
this.clearTableHeader();
this.clearTableBody();
this.updateTableButtons();
table = this.table;
} else {
// Create new table element
table = document.createElement('table');
table.classList.add('twingle-shop-table');
table.id = this.projectIdentifier;
// Add caption
const caption = table.createCaption();
caption.textContent = this.name + ' (' + this.projectIdentifier + ')';
caption.classList.add('twingle-shop-table-caption');
// Add table body
const tbody = document.createElement('tbody');
table.appendChild(tbody);
// Add table buttons
this.addTableButtons(table);
}
// Add header row
const thead = table.createTHead();
const headerRow = thead.insertRow();
const headers = [ts('Product', []), ts('Financial Type', [])];
// Add price field column if price set exists
if (this.priceSetId) {
headers.splice(1, 0, ts('Price Field', []));
}
for (const headerText of headers) {
const headerCell = document.createElement('th');
headerCell.textContent = headerText;
headerRow.appendChild(headerCell);
}
// Add products to table
this.addProductsToTable(table);
return table;
}
/**
* Adds buttons for creating, updating or deleting the price set for the
* given project (shop).
* @private
*/
addTableButtons(table) {
table.appendChild(this.createTableButton('update', this.updatePriceSetHandler()));
if (this.priceSetId === null) {
table.appendChild(this.createTableButton('create', this.createPriceSetHandler()));
} else {
table.appendChild(this.createTableButton('delete', this.deletePriceSetHandler()));
}
}
/**
* Creates a button for creating, updating or deleting the price set for the
* given project (shop).
* @param action
* @param handler
* @returns {HTMLButtonElement}
* @private
*/
createTableButton(action, handler) {
// Create button
const button = document.createElement('button');
button.id = 'btn_' + action + '_twingle_shop_' + this.projectIdentifier;
button.classList.add('crm-button', 'twingle-shop-table-button');
// Add button text
const text = action === 'create' ? ts('Create Price Set', []) : action === 'update' ? ts('Update Price Set', []) : ts('Delete Price Set', []);
button.textContent = ' ' + ts(text, []);
// Add button handler
button.onclick = handler;
// Add icon
const icon = document.createElement('i');
const iconClass = action === 'create' ? 'fa-plus-circle' : action === 'update' ? 'fa-refresh' : 'fa-trash';
icon.classList.add('crm-i', iconClass);
button.insertBefore(icon, button.firstChild);
return button;
}
/**
* Adds products to table body.
* @param table
* @private
*/
addProductsToTable(table) {
// Get table body
const tbody = table.getElementsByTagName('tbody')[0];
// Add products to table body
for (const product of this.products) {
// Add row for product
const row = product.createRow();
// Add row to table
tbody.appendChild(row);
}
}
/**
* Updates the table buttons.
*/
updateTableButtons() {
const table_buttons = this.table.getElementsByClassName('twingle-shop-table-button');
// Remove all price set buttons from table
while (table_buttons.length > 0) {
table_buttons[0].remove();
}
this.addTableButtons(this.table);
}
/**
* Clears the table header.
* @private
*/
clearTableHeader() {
const thead = this.table.getElementsByTagName('thead')[0];
while (thead.firstChild) {
thead.removeChild(thead.firstChild);
}
}
/**
* Clears the table body.
*/
clearTableBody() {
const tbody = this.table.getElementsByTagName('tbody')[0];
while (tbody.firstChild) {
tbody.removeChild(tbody.firstChild);
}
}
/**
* Creates a handler for creating the price set for the given project (shop).
* @returns {(function(*): void)|*}
*/
createPriceSetHandler() {
let self = this;
return function (event) {
event.preventDefault();
CRM.api3('TwingleShop', 'create', self.dumpData())
.then(function (result) {
if (result.is_error === 1) {
cj('#btn_create_price_set_' + self.projectIdentifier).crmError(result.error_message, ts('Could not create Twingle Shop', []));
} else {
self.update();
CRM.alert(ts('The Price Set was created successfully.', []), ts('Price Field created', []), 'success', {'expires': 5000});
}
}, function (error) {
cj('#btn_create_price_set_' + self.projectIdentifier).crmError(error.message, ts('Could not create TwingleShop', []));
});
};
}
/**
* Creates a handler for deleting the price set for the given project (shop).
* @returns {(function(*): void)|*}
*/
deletePriceSetHandler() {
let self = this;
return function (event) {
event.preventDefault();
const options = {
'title': ts('Delete Price Set', []),
'message': ts('Are you sure you want to delete the price set associated with this Twingle Shop?', []),
};
CRM.confirm(options)
.on('crmConfirm:yes', function () {
CRM.api3('TwingleShop', 'delete', {
'project_identifier': self.projectIdentifier,
}).then(function (result) {
if (result.is_error === 1) {
cj('#btn_create_price_set_' + self.projectIdentifier).crmError(result.error_message, ts('Could not delete Twingle Shop', []));
} else {
self.update();
CRM.alert(ts('The Price Set was deleted successfully.', []), ts('Price Set deleted', []), 'success', {'expires': 5000});
}
}, function (error) {
cj('#btn_delete_price_set_' + self.projectIdentifier).crmError(error.message, ts('Could not delete Twingle Shop', []));
});
});
};
}
/**
* Creates a handler for updating the price set for the given project (shop).
* @returns {(function(*): void)|*}
*/
updatePriceSetHandler() {
let self = this;
return function (event) {
cj('#twingle-shop-spinner').show();
if (event) {
event.preventDefault();
}
CRM.api3('TwingleShop', 'fetch', {
'project_identifiers': self.projectIdentifier,
}).then(function (result) {
if (result.is_error === 1) {
cj('#btn_create_price_set_' + self.projectIdentifier).crmError(result.error_message, ts('Could not delete Twingle Shop', []));
cj('#twingle-shop-spinner').hide();
} else {
self.update(result.values[self.projectIdentifier]);
cj('#twingle-shop-spinner').hide();
}
}, function (error) {
cj('#btn_update_price_set_' + self.projectIdentifier).crmError(error.message, ts('Could not update Twingle Shop', []));
cj('#twingle-shop-spinner').hide();
});
};
}
/**
* Updates the project properties and rebuilds the table.
* @param projectData
*/
update(projectData) {
if (!projectData) {
const updatePriceSet = this.updatePriceSetHandler();
updatePriceSet();
} else {
this.setProps(projectData);
this.buildTable();
}
}
}

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

276
l10n/pot/twingle.pot Normal file
View file

@ -0,0 +1,276 @@
#: ./CRM/Twingle/Form/Profile.php
msgid "Delete Twingle API profile <em>%1</em>"
msgstr ""
#: ./CRM/Twingle/Form/Profile.php ./templates/CRM/Twingle/Page/Profiles.tpl
msgid "Reset"
msgstr ""
#: ./CRM/Twingle/Form/Profile.php ./templates/CRM/Twingle/Page/Profiles.tpl
msgid "Delete"
msgstr ""
#: ./CRM/Twingle/Form/Profile.php
msgid "Edit Twingle API profile <em>%1</em>"
msgstr ""
#: ./CRM/Twingle/Form/Profile.php
msgid "New Twingle API profile"
msgstr ""
#: ./CRM/Twingle/Form/Profile.php ./templates/CRM/Twingle/Page/Profiles.tpl
msgid "Profile name"
msgstr ""
#: ./CRM/Twingle/Form/Profile.php
msgid "Project IDs"
msgstr ""
#: ./CRM/Twingle/Form/Profile.php
msgid "Location type"
msgstr ""
#: ./CRM/Twingle/Form/Profile.php
msgid "Financial Type"
msgstr ""
#: ./CRM/Twingle/Form/Profile.php
msgid "Gender option for submitted value \"male\""
msgstr ""
#: ./CRM/Twingle/Form/Profile.php
msgid "Gender option for submitted value \"female\""
msgstr ""
#: ./CRM/Twingle/Form/Profile.php
msgid "Gender option for submitted value \"other\""
msgstr ""
#: ./CRM/Twingle/Form/Profile.php
msgid "Record %1 as"
msgstr ""
#: ./CRM/Twingle/Form/Profile.php
msgid "CiviSEPA creditor"
msgstr ""
#: ./CRM/Twingle/Form/Profile.php
msgid "Sign up for newsletter groups"
msgstr ""
#: ./CRM/Twingle/Form/Profile.php
msgid "Sign up for postal mail groups"
msgstr ""
#: ./CRM/Twingle/Form/Profile.php
msgid "Sign up for Donation receipt groups"
msgstr ""
#: ./CRM/Twingle/Form/Profile.php ./CRM/Twingle/Form/Settings.php
msgid "Save"
msgstr ""
#: ./CRM/Twingle/Form/Profile.php
msgid "Only alphanumeric characters and the underscore (_) are allowed for profile names."
msgstr ""
#: ./CRM/Twingle/Form/Profile.php
msgid "CiviSEPA"
msgstr ""
#: ./CRM/Twingle/Form/Profile.php
msgid "No mailing lists available"
msgstr ""
#: ./CRM/Twingle/Profile.php
msgid "Unknown attribute %1."
msgstr ""
#: ./CRM/Twingle/Profile.php
msgid "Bank transfer"
msgstr ""
#: ./CRM/Twingle/Profile.php
msgid "Debit manual"
msgstr ""
#: ./CRM/Twingle/Profile.php
msgid "Debit automatic"
msgstr ""
#: ./CRM/Twingle/Profile.php
msgid "Credit card"
msgstr ""
#: ./CRM/Twingle/Profile.php
msgid "Mobile phone Germany"
msgstr ""
#: ./CRM/Twingle/Profile.php
msgid "PayPal"
msgstr ""
#: ./CRM/Twingle/Profile.php
msgid "SOFORT Überweisung"
msgstr ""
#: ./CRM/Twingle/Profile.php
msgid "Amazon Pay"
msgstr ""
#: ./CRM/Twingle/Profile.php
msgid "paydirekt"
msgstr ""
#: ./CRM/Twingle/Profile.php
msgid "Apple Pay"
msgstr ""
#: ./CRM/Twingle/Profile.php
msgid "Google Pay"
msgstr ""
#: ./CRM/Twingle/Submission.php
msgid "Invalid donation rhythm."
msgstr ""
#: ./CRM/Twingle/Submission.php
msgid "Payment method could not be matched to existing payment instrument."
msgstr ""
#: ./CRM/Twingle/Submission.php
msgid "Invalid date for parameter \"confirmed_at\"."
msgstr ""
#: ./CRM/Twingle/Submission.php
msgid "Invalid date for parameter \"user_birthdate\"."
msgstr ""
#: ./CRM/Twingle/Submission.php
msgid "Gender could not be matched to existing gender."
msgstr ""
#: ./CRM/Twingle/Submission.php
msgid "Unknown country %1."
msgstr ""
#: ./api/v3/TwingleDonation/Cancel.php
msgid "Invalid date for parameter \"cancelled_at\"."
msgstr ""
#: ./api/v3/TwingleDonation/Cancel.php ./api/v3/TwingleDonation/Endrecurring.php
msgid "Could not terminate SEPA mandate"
msgstr ""
#: ./api/v3/TwingleDonation/Endrecurring.php
msgid "Invalid date for parameter \"ended_at\"."
msgstr ""
#: ./api/v3/TwingleDonation/Endrecurring.php
msgid "Mandate closed by TwingleDonation.Endrecurring API call"
msgstr ""
#: ./api/v3/TwingleDonation/Submit.php
msgid "Contribution with the given transaction ID already exists."
msgstr ""
#: ./api/v3/TwingleDonation/Submit.php
msgid "Individual contact could not be found or created."
msgstr ""
#: ./api/v3/TwingleDonation/Submit.php
msgid "Organisation contact could not be found or created."
msgstr ""
#: ./api/v3/TwingleDonation/Submit.php
msgid "Missing attribute %1 for SEPA mandate"
msgstr ""
#: ./api/v3/TwingleDonation/Submit.php
msgid "Could not create recurring contribution."
msgstr ""
#: ./api/v3/TwingleDonation/Submit.php
msgid "Could not create contribution"
msgstr ""
#: ./templates/CRM/Twingle/Form/Profile.tpl
msgid "General settings"
msgstr ""
#: ./templates/CRM/Twingle/Form/Profile.tpl
msgid "Payment methods"
msgstr ""
#: ./templates/CRM/Twingle/Form/Profile.tpl
msgid "Groups"
msgstr ""
#: ./templates/CRM/Twingle/Form/Profile.tpl
msgid "Are you sure you want to reset the default profile?"
msgstr ""
#: ./templates/CRM/Twingle/Form/Profile.tpl
msgid "Are you sure you want to delete the profile <em>%1</em>?"
msgstr ""
#: ./templates/CRM/Twingle/Form/Profile.tpl
msgid "Profile name not given or invalid."
msgstr ""
#: ./templates/CRM/Twingle/Form/Settings.hlp
msgid "When the %1 is enabled and one of its payment instruments is assigned to a Twingle payment method (practically the <em>debit_manual</em> payment method), submitting a Twingle donation through the API will create a SEPA mandate with the given data."
msgstr ""
#: ./templates/CRM/Twingle/Form/Settings.tpl
msgid "Help"
msgstr ""
#: ./templates/CRM/Twingle/Page/Configuration.tpl
msgid "Profiles"
msgstr ""
#: ./templates/CRM/Twingle/Page/Configuration.tpl
msgid "Configure profiles"
msgstr ""
#: ./templates/CRM/Twingle/Page/Configuration.tpl
msgid "Settings"
msgstr ""
#: ./templates/CRM/Twingle/Page/Configuration.tpl
msgid "Configure extension settings"
msgstr ""
#: ./templates/CRM/Twingle/Page/Profiles.tpl
msgid "New profile"
msgstr ""
#: ./templates/CRM/Twingle/Page/Profiles.tpl
msgid "Properties"
msgstr ""
#: ./templates/CRM/Twingle/Page/Profiles.tpl
msgid "Operations"
msgstr ""
#: ./templates/CRM/Twingle/Page/Profiles.tpl
msgid "Selector"
msgstr ""
#: ./templates/CRM/Twingle/Page/Profiles.tpl
msgid "Edit profile %1"
msgstr ""
#: ./templates/CRM/Twingle/Page/Profiles.tpl
msgid "Edit"
msgstr ""
#: ./templates/CRM/Twingle/Page/Profiles.tpl
msgid "Reset profile %1"
msgstr ""
#: ./templates/CRM/Twingle/Page/Profiles.tpl
msgid "Delete profile %1"
msgstr ""

View file

@ -1,83 +0,0 @@
<?xml version="1.0"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="CiviCRM - Modified"
xsi:noNamespaceSchemaLocation="tools/phpcs/vendor/squizlabs/php_codesniffer/phpcs.xsd">
<description>CiviCRM coding standard with some additional changes</description>
<file>api</file>
<file>Civi</file>
<file>CRM</file>
<file>tests</file>
<exclude-pattern>/CRM/[^/]+/DAO/.*\.php$</exclude-pattern>
<arg name="extensions" value="php"/>
<arg name="cache" value=".phpcs.cache"/>
<arg name="colors"/>
<arg value="sp"/>
<!-- Exit with code 0 if warnings, but no error occurred -->
<config name="ignore_warnings_on_exit" value="true"/>
<rule ref="tools/phpcs/vendor/drupal/coder/coder_sniffer/Drupal">
<!-- Conflicts with PHPStan type hints -->
<exclude name="Drupal.Commenting.VariableComment.IncorrectVarType"/>
<exclude name="Drupal.Commenting.FunctionComment.ParamTypeSpaces"/>
<exclude name="Drupal.Commenting.FunctionComment.ReturnTypeSpaces"/>
<exclude name="Drupal.Commenting.VariableComment.MissingVar"/>
<!-- Don't enforce phpdoc type hint because it (might) only duplicate a PHP type hint -->
<exclude name="Drupal.Commenting.VariableComment.MissingVar"/>
<!-- Don't enforce phpdoc type hint because it (might) only duplicate a PHP type hint -->
<exclude name="Drupal.Commenting.FunctionComment.ParamMissingDefinition"/>
<!-- False positive when license header is set and variable has no comment -->
<exclude name="Drupal.Commenting.VariableComment.WrongStyle"/>
</rule>
<rule ref="Generic.Arrays.DisallowLongArraySyntax"/>
<rule ref="Generic.CodeAnalysis.EmptyStatement">
<exclude name="Generic.CodeAnalysis.EmptyStatement.DetectedCatch"/>
</rule>
<rule ref="Generic.CodeAnalysis.ForLoopWithTestFunctionCall"/>
<rule ref="Generic.Files.OneClassPerFile"/>
<rule ref="Generic.Files.OneInterfacePerFile"/>
<rule ref="Generic.Files.OneObjectStructurePerFile"/>
<rule ref="Generic.Files.OneTraitPerFile"/>
<rule ref="Generic.Formatting.DisallowMultipleStatements"/>
<rule ref="Generic.Metrics.CyclomaticComplexity"/>
<rule ref="Generic.Metrics.NestingLevel"/>
<rule ref="Generic.NamingConventions.AbstractClassNamePrefix"/>
<rule ref="Generic.NamingConventions.InterfaceNameSuffix"/>
<rule ref="Generic.NamingConventions.TraitNameSuffix"/>
<rule ref="Generic.PHP.RequireStrictTypes"/>
<rule ref="PSR1.Files.SideEffects"/>
<rule ref="PSR12.Classes.ClassInstantiation"/>
<rule ref="PSR12.Properties.ConstantVisibility"/>
<rule ref="Squiz.PHP.CommentedOutCode"/>
<rule ref="Squiz.PHP.GlobalKeyword"/>
<rule ref="Squiz.Strings.DoubleQuoteUsage">
<exclude name="Squiz.Strings.DoubleQuoteUsage.ContainsVar"/>
</rule>
<!-- Lines can be 120 chars long, but never show errors -->
<rule ref="Generic.Files.LineLength">
<properties>
<property name="lineLimit" value="120"/>
<property name="absoluteLineLimit" value="0"/>
</properties>
</rule>
<!-- Ban some functions -->
<rule ref="Generic.PHP.ForbiddenFunctions">
<properties>
<property name="forbiddenFunctions" type="array">
<element key="sizeof" value="count"/>
<element key="delete" value="unset"/>
<element key="print" value="echo"/>
<element key="is_null" value="null"/>
<element key="create_function" value="null"/>
</property>
</properties>
</rule>
</ruleset>

View file

@ -1,13 +0,0 @@
includes:
- phpstan.neon.dist
parameters:
scanDirectories:
- ci/vendor/civicrm/civicrm-core/CRM/
bootstrapFiles:
- ci/vendor/autoload.php
# Because we test with different versions in CI we have unmatched errors
reportUnmatchedIgnoredErrors: false
ignoreErrors:
# Errors we get when using "prefer-lowest"
- '#::getSubscribedEvents\(\) return type has no value type specified in iterable type array.$#'

View file

@ -1,42 +0,0 @@
parameters:
paths:
- api
- Civi
- CRM
- tests
excludePaths:
analyse:
- CRM/*/DAO/*
- tests/phpunit/bootstrap.php
scanFiles:
- twingle.civix.php
- tools/phpunit/vendor/bin/.phpunit/phpunit/src/Framework/TestCase.php
scanDirectories:
- tools/phpunit/vendor/bin/.phpunit/phpunit/src/Framework
bootstrapFiles:
- tools/phpunit/vendor/bin/.phpunit/phpunit/vendor/autoload.php
- vendor/autoload.php
- phpstanBootstrap.php
level: 9
universalObjectCratesClasses:
- Civi\Core\Event\GenericHookEvent
checkTooWideReturnTypesInProtectedAndPublicMethods: true
checkUninitializedProperties: true
checkMissingCallableSignature: true
treatPhpDocTypesAsCertain: false
exceptions:
check:
missingCheckedExceptionInThrows: true
tooWideThrowType: true
checkedExceptionClasses:
- \Webmozart\Assert\InvalidArgumentException
implicitThrows: false
ignoreErrors:
# Note paths are prefixed with "*/" to work with inspections in PHPStorm because of:
# https://youtrack.jetbrains.com/issue/WI-63891/PHPStan-ignoreErrors-configuration-isnt-working-with-inspections
# Example
#- # Accessing results of API requests
#message: "#^Offset '[^']+' does not exist on array[^\\|]+\\|null.$#"
#path: */tests/phpunit/**/*Test.php
tmpDir: .phpstan

View file

@ -1,14 +0,0 @@
# Copy this file to phpstan.neon and replace {VENDOR_DIR} with the appropriate
# path.
includes:
- phpstan.neon.dist
parameters:
scanDirectories:
- {VENDOR_DIR}/civicrm/civicrm-core/Civi/
- {VENDOR_DIR}/civicrm/civicrm-core/CRM/
- {VENDOR_DIR}/civicrm/civicrm-core/api/
- {VENDOR_DIR}/civicrm/civicrm-packages/
bootstrapFiles:
- {VENDOR_DIR}/autoload.php

View file

@ -1,43 +0,0 @@
<?php
/*
* Copyright (C) 2022 SYSTOPIA GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation in version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types = 1);
// phpcs:disable Drupal.Commenting.DocComment.ContentAfterOpen
/** @var \PHPStan\DependencyInjection\Container $container */
/** @phpstan-var array<string> $bootstrapFiles */
$bootstrapFiles = $container->getParameter('bootstrapFiles');
foreach ($bootstrapFiles as $bootstrapFile) {
if (str_ends_with($bootstrapFile, 'vendor/autoload.php')) {
$vendorDir = dirname($bootstrapFile);
$civiCrmVendorDir = $vendorDir . '/civicrm';
$civiCrmCoreDir = $civiCrmVendorDir . '/civicrm-core';
if (file_exists($civiCrmCoreDir)) {
set_include_path(get_include_path()
. PATH_SEPARATOR . $civiCrmCoreDir
. PATH_SEPARATOR . $civiCrmVendorDir . '/civicrm-packages'
);
// $bootstrapFile might not be included, yet. It is required for the
// following require_once, though.
require_once $bootstrapFile;
// Prevent error "Class 'CRM_Core_Exception' not found in file".
require_once $civiCrmCoreDir . '/CRM/Core/Exception.php';
break;
}
}
}

View file

@ -1,35 +0,0 @@
<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.5/phpunit.xsd"
colors="true"
failOnRisky="true"
failOnWarning="true"
forceCoversAnnotation="true"
bootstrap="tests/phpunit/bootstrap.php">
<php>
<ini name="error_reporting" value="-1" />
<env name="SYMFONY_DEPRECATIONS_HELPER" value="max[direct]=0&amp;baselineFile=./tests/ignored-deprecations.json"/>
</php>
<testsuites>
<testsuite name="Extension Test Suite">
<directory>./tests/phpunit</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">api</directory>
<directory suffix=".php">CRM</directory>
<directory suffix=".php">Civi</directory>
<exclude>
<directory>CRM/*/DAO</directory>
</exclude>
</whitelist>
</filter>
<listeners>
<listener class="Civi\Test\CiviTestListener">
<arguments/>
</listener>
</listeners>
</phpunit>

View file

@ -1,21 +0,0 @@
-- +--------------------------------------------------------------------+
-- | Copyright CiviCRM LLC. All rights reserved. |
-- | |
-- | This work is published under the GNU AGPLv3 license with some |
-- | permitted exceptions and without any warranty. For full license |
-- | and copyright information, see https://civicrm.org/licensing |
-- +--------------------------------------------------------------------+
--
-- Generated from drop.tpl
-- DO NOT EDIT. Generated by CRM_Core_CodeGen
---- /*******************************************************
-- *
-- * Clean up the existing tables-- *
-- *******************************************************/
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS `civicrm_twingle_product`;
DROP TABLE IF EXISTS `civicrm_twingle_shop`;
SET FOREIGN_KEY_CHECKS=1;

View file

@ -1,66 +0,0 @@
-- +--------------------------------------------------------------------+
-- | Copyright CiviCRM LLC. All rights reserved. |
-- | |
-- | This work is published under the GNU AGPLv3 license with some |
-- | permitted exceptions and without any warranty. For full license |
-- | and copyright information, see https://civicrm.org/licensing |
-- +--------------------------------------------------------------------+
--
-- Generated from schema.tpl
-- DO NOT EDIT. Generated by CRM_Core_CodeGen
--
-- /*******************************************************
-- *
-- * Clean up the existing tables - this section generated from drop.tpl
-- *
-- *******************************************************/
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS `civicrm_twingle_product`;
DROP TABLE IF EXISTS `civicrm_twingle_shop`;
SET FOREIGN_KEY_CHECKS=1;
-- /*******************************************************
-- *
-- * Create new tables
-- *
-- *******************************************************/
-- /*******************************************************
-- *
-- * civicrm_twingle_shop
-- *
-- * This table contains the Twingle Shop data. Each Twingle Shop is linked to a corresponding Price Set.
-- *
-- *******************************************************/
CREATE TABLE `civicrm_twingle_shop` (
`id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Unique TwingleShop ID',
`project_identifier` varchar(32) NOT NULL COMMENT 'Twingle Project Identifier',
`numerical_project_id` int unsigned NOT NULL COMMENT 'Numerical Twingle Project Identifier',
`price_set_id` int unsigned COMMENT 'FK to Price Set',
`name` varchar(64) NOT NULL COMMENT 'name of the shop',
PRIMARY KEY (`id`),
CONSTRAINT FK_civicrm_twingle_shop_price_set_id FOREIGN KEY (`price_set_id`) REFERENCES `civicrm_price_set`(`id`) ON DELETE CASCADE
)
ENGINE=InnoDB;
-- /*******************************************************
-- *
-- * civicrm_twingle_product
-- *
-- * This table contains the Twingle Product data.
-- *
-- *******************************************************/
CREATE TABLE `civicrm_twingle_product` (
`id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Unique TwingleProduct ID',
`external_id` int unsigned NOT NULL COMMENT 'The ID of this product in the Twingle database',
`price_field_id` int unsigned NOT NULL COMMENT 'FK to Price Field',
`twingle_shop_id` int unsigned COMMENT 'FK to Twingle Shop',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Timestamp of when the product was created in the database',
`updated_at` datetime NOT NULL COMMENT 'Timestamp of when the product was last updated in the Twingle database',
PRIMARY KEY (`id`),
CONSTRAINT FK_civicrm_twingle_product_price_field_id FOREIGN KEY (`price_field_id`) REFERENCES `civicrm_price_field`(`id`) ON DELETE CASCADE,
CONSTRAINT FK_civicrm_twingle_product_twingle_shop_id FOREIGN KEY (`twingle_shop_id`) REFERENCES `civicrm_twingle_shop`(`id`) ON DELETE CASCADE
)
ENGINE=InnoDB;

View file

@ -62,10 +62,10 @@
{/htxt}
{htxt id='id-custom_field_mapping'}
{ts domain="de.systopia.twingle"}<p>Map Twingle custom fields to CiviCRM fields using the following format (each assignment in a separate line):</p>
{ts domain="de.systopia.twingle"}<p>Map Twingle custom fields to CiviCRM custom fields using the following format (each assignment in a separate line):</p>
<pre>twingle_field_1=custom_123<br />twingle_field_2=custom_789</pre>
<p>Always use the <code>custom_[id]</code> notation for CiviCRM custom fields.</p>
<p>This works for fields that Twingle themselves provide in the <code>custom_fields</code> parameter, and for any other parameter (e.g. <code>user_extrafield</code>)</p>
<p>This only works for fields that Twingle themselves provide in the <code>custom_fields</code> parameter, not for any parameters; e.g. <code>user_extrafield</code> always ends up in a note.</p>
<p>Only custom fields extending one of the following CiviCRM entities are allowed:</p>
<ul>
<li><strong>Contact</strong> &ndash; Will be set on the Individual contact</li>
@ -75,23 +75,4 @@
<li><strong>ContributionRecur</strong> &ndash; Will be set on the recurring contribution and deriving single contributions</li>
</ul>{/ts}
{/htxt}
{htxt id='id-map_as_contribution_notes'}
{ts domain="de.systopia.twingle"}<p>Create a contribution note for each field specified in this selection.</p>
<p><i>Tip: You can enable or disable this fields in the TwingleMANAGER.</i></p>{/ts}
{/htxt}
{htxt id='id-map_as_contact_notes'}
{ts domain="de.systopia.twingle"}<p>Create a contact note for each field specified in this selection.</p>
<p><i>Tip: You can enable or disable this fields in the TwingleMANAGER.</i></p>{/ts}
{/htxt}
{htxt id='id-enable_shop_integration'}
<p>{ts domain="de.systopia.twingle"}Enable the processing of orders via Twingle Shop for this profile. The ordered products will then appear as line items in the contribution.{/ts}</p>
{/htxt}
{htxt id='id-shop_map_products'}
<p>{ts domain="de.systopia.twingle"}If this option is enabled, all Twingle Shop products corresponding to the specified project IDs will be retrieved from Twingle and mapped as price sets and price fields. Each Twingle Shop is mapped as a price set with its products as price fields.{/ts}</p>
<p>{ts domain="de.systopia.twingle"}This allows you to manually create contributions with the same line items for phone orders, for example, as would be the case for orders placed through the Twingle Shop.{/ts}</p>
{/htxt}
{/crmScope}
{/crmScope}

View file

@ -28,7 +28,6 @@
</tr>
<tr class="crm-section">
{if not $is_default}
<td class="label">{$form.selector.label}
<a
onclick='
@ -46,8 +45,7 @@
class="helpicon"
></a>
</td>
<td id="selectors" class="content">{$form.selector.html}</td>
{/if}
<td class="content">{$form.selector.html}</td>
</tr>
<tr class="crm-section">
@ -313,22 +311,6 @@
<td class="content">{$form.contribution_source.html}</td>
</tr>
<tr class="crm-section">
<td class="label">
{$form.map_as_contribution_notes.label}
{help id="id-map_as_contribution_notes" title=$form.map_as_contribution_notes.label}
</td>
<td class="content">{$form.map_as_contribution_notes.html}</td>
</tr>
<tr class="crm-section">
<td class="label">
{$form.map_as_contact_notes.label}
{help id="id-map_as_contact_notes" title=$form.map_as_contact_notes.label}
</td>
<td class="content">{$form.map_as_contact_notes.html}</td>
</tr>
<tr class="crm-section">
<td class="label">
{$form.custom_field_mapping.label}
@ -353,71 +335,6 @@
</table>
{if $twingle_use_shop eq 1}
<legend>{ts domain="de.systopia.twingle"}Shop Integration{/ts}</legend>
<table class="form-layout-compressed">
<tr class="crm-section">
<td class="label">
{$form.enable_shop_integration.label}
<a
onclick='
CRM.help(
"{ts domain="de.systopia.twingle"}Enable Shop Integration{/ts}",
{literal}{
"id": "id-enable_shop_integration",
"file": "CRM\/Twingle\/Form\/Profile"
}{/literal}
);
return false;
'
href="#"
title="{ts domain="de.systopia.twingle"}Help{/ts}"
class="helpicon"
></a>
</td>
<td class="content">{$form.enable_shop_integration.html}</td>
</tr>
<tr class="crm-section twingle-shop-element">
<td class="label">{$form.shop_financial_type.label}</td>
<td class="content">{$form.shop_financial_type.html}</td>
</tr>
<tr class="crm-section twingle-shop-element">
<td class="label">{$form.shop_donation_financial_type.label}</td>
<td class="content">{$form.shop_donation_financial_type.html}</td>
</tr>
<tr class="crm-section twingle-shop-element">
<td class="label">{$form.shop_map_products.label}
<a
onclick='
CRM.help(
"{ts domain="de.systopia.twingle"}Map Products as Price Fields{/ts}",
{literal}{
"id": "id-shop_map_products",
"file": "CRM\/Twingle\/Form\/Profile"
}{/literal}
);
return false;
'
href="#"
title="{ts domain="de.systopia.twingle"}Help{/ts}"
class="helpicon"
></a></td>
<td class="content">{$form.shop_map_products.html}
<i id="twingle-shop-spinner" class="crm-i fa-spinner fa-spin"></i>
<div class="twingle-product-mapping">
<div id="tableContainer"></div>
</div>
</td>
</tr>
</table>
{/if}
</fieldset>
{elseif $op == 'delete'}
@ -454,18 +371,11 @@
}
}
// register events
// register events and run once
cj(document).ready(function (){
cj('#membership_type_id').change(twingle_membership_active_changed);
cj('#membership_type_id_recur').change(twingle_membership_active_changed);
// init Twingle Shop integration
if ({/literal}{if $twingle_use_shop eq 1}true{else}false{/if}{literal}) {
twingleShopInit();
}
});
// run once
twingle_membership_active_changed();
</script>
{/literal}
{/literal}

View file

@ -27,11 +27,3 @@
{htxt id='id-twingle_prefix'}
{ts domain="de.systopia.twingle"}You can use this setting to add a prefix to the Twingle transaction ID, in order to avoid collisions with other transaction ids.{/ts}
{/htxt}
{htxt id='id-twingle_use_shop'}
{ts domain="de.systopia.twingle"}If you enable Twingle Shop integration, you can configure Twingle API profiles to include products ordered through Twingle Shop as line items in the created contribution.{/ts}
{/htxt}
{htxt id='id-twingle_access_key'}
{ts domain="de.systopia.twingle"}Enter your twingle API access key.{/ts}
{/htxt}

View file

@ -18,40 +18,47 @@
<table class="form-layout-compressed">
<tr class="crm-twingle-form-block-use-sepa">
<td class="label">
{$form.twingle_use_sepa.label}
{help id="id-twingle_use_sepa" title=$form.twingle_use_sepa.label}
</td>
<td class="label">{$form.twingle_use_sepa.label} &nbsp;<a onclick='CRM.help("{$form.twingle_use_sepa.label}", {literal}{"id":"id-{/literal}{$form.twingle_use_sepa.name}{literal}","file":"CRM\/Twingle\/Form\/Settings"}{/literal}); return false;' href="#" title="{ts domain="de.systopia.twingle"}Help{/ts}" class="helpicon"></a></td>
<td>
{$form.twingle_use_sepa.html}
<br />
<span class="description">
{$formElements.twingle_use_sepa.description}
</span>
</td>
</tr>
<tr class="crm-twingle-form-block-use-sepa-reference">
<td class="label">
{$form.twingle_dont_use_reference.label}
{help id="id-twingle_dont_use_reference" title=$form.twingle_dont_use_reference.label}
</td>
<td class="label">{$form.twingle_dont_use_reference.label} &nbsp; <a onclick='CRM.help("{$form.twingle_dont_use_reference.label}", {literal}{"id":"id-{/literal}{$form.twingle_dont_use_reference.name}{literal}","file":"CRM\/Twingle\/Form\/Settings"}{/literal}); return false;' href="#" title="{ts domain="de.systopia.twingle"}Help{/ts}" class="helpicon"></a></td>
<td>
{$form.twingle_dont_use_reference.html}
<br />
<span class="description">
{$formElements.twingle_dont_use_reference.description}
</span>
</td>
</tr>
<tr class="crm-twingle-form-block-prefix">
<td class="label">{$form.twingle_prefix.label}
{help id="id-twingle_prefix" title=$form.twingle_prefix.label}
</td>
<td class="label">{$form.twingle_prefix.label} &nbsp;<a onclick='CRM.help("{$form.twingle_prefix.label}", {literal}{"id":"id-{/literal}{$form.twingle_prefix.name}{literal}","file":"CRM\/Twingle\/Form\/Settings"}{/literal}); return false;' href="#" title="{ts domain="de.systopia.twingle"}Help{/ts}" class="helpicon"></a></td>
<td>
{$form.twingle_prefix.html}
<br />
<span class="description">
{$formElements.twingle_prefix.description}
</span>
</td>
</tr>
<tr class="crm-twingle-form-block-recurring-protection">
<td class="label">{$form.twingle_protect_recurring.label}
{help id="id-twingle_protect_recurring" title=$form.twingle_protect_recurring.label}
</td>
<td class="label">{$form.twingle_protect_recurring.label} &nbsp;<a onclick='CRM.help("{$form.twingle_protect_recurring.label}", {literal}{"id":"id-{/literal}{$form.twingle_protect_recurring.name}{literal}","file":"CRM\/Twingle\/Form\/Settings"}{/literal}); return false;' href="#" title="{ts domain="de.systopia.twingle"}Help{/ts}" class="helpicon"></a></td>
<td>
{$form.twingle_protect_recurring.html}
<br />
<span class="description">
{$formElements.protect_recurring.description}
</span>
</td>
</tr>
@ -59,6 +66,10 @@
<td class="label">{$form.twingle_protect_recurring_activity_type.label}</td>
<td>
{$form.twingle_protect_recurring_activity_type.html}
<br />
<span class="description">
{$formElements.twingle_protect_recurring_activity_type.description}
</span>
</td>
</tr>
@ -66,6 +77,10 @@
<td class="label">{$form.twingle_protect_recurring_activity_subject.label}</td>
<td>
{$form.twingle_protect_recurring_activity_subject.html}
<br />
<span class="description">
{$formElements.twingle_protect_recurring_activity_subject.description}
</span>
</td>
</tr>
@ -73,6 +88,10 @@
<td class="label">{$form.twingle_protect_recurring_activity_status.label}</td>
<td>
{$form.twingle_protect_recurring_activity_status.html}
<br />
<span class="description">
{$formElements.twingle_protect_recurring_activity_status.description}
</span>
</td>
</tr>
@ -80,27 +99,15 @@
<td class="label">{$form.twingle_protect_recurring_activity_assignee.label}</td>
<td>
{$form.twingle_protect_recurring_activity_assignee.html}
<br />
<span class="description">
{$formElements.twingle_protect_recurring_activity_assignee.description}
</span>
</td>
</tr>
</table>
<h3>Twingle Shop Integration</h3>
<table class="form-layout-compressed">
<tr class="crm-twingle-form-block-use-shop">
<td class="label">{$form.twingle_use_shop.label}
{help id="id-twingle_use_shop" title=$form.twingle_use_shop.label}
</td>
<td>{$form.twingle_use_shop.html}</td>
</tr>
<tr class="crm-twingle-form-block-access-key twingle-shop-element">
<td class="label">{$form.twingle_access_key.label}
{help id="id-twingle_access_key" title=$form.twingle_access_key.label}
</td>
<td>{$form.twingle_access_key.html}</td>
</tr>
</table>
<div class="crm-submit-buttons">
{include file="CRM/common/formButtons.tpl" location="bottom"}
@ -109,23 +116,22 @@
</div>
{literal}
<script>
/**
* Will show/hide the twingle_protect_recurring_activity_* fields based on
* whether activity creation is selected
*/
function twingle_protect_recurring_change() {
if (cj('#twingle_protect_recurring').val() == '2') {
cj('tr.crm-twingle-form-block-recurring-protection-activity').show();
}
else {
cj('tr.crm-twingle-form-block-recurring-protection-activity').hide();
}
<script>
/**
* Will show/hide the twingle_protect_recurring_activity_* fields based on
* whether activity creation is selected
*/
function twingle_protect_recurring_change() {
if (cj("#twingle_protect_recurring").val() == '2') {
cj("tr.crm-twingle-form-block-recurring-protection-activity").show();
} else {
cj("tr.crm-twingle-form-block-recurring-protection-activity").hide();
}
}
cj(document).ready(function () {
cj('#twingle_protect_recurring').change(twingle_protect_recurring_change);
twingle_protect_recurring_change();
});
</script>
{/literal}
cj(document).ready(function () {
cj("#twingle_protect_recurring").change(twingle_protect_recurring_change);
twingle_protect_recurring_change();
});
</script>
{/literal}

View file

@ -25,10 +25,7 @@
<thead>
<tr>
<th>{ts domain="de.systopia.twingle"}Profile name{/ts}</th>
<th>{ts domain="de.systopia.twingle"}Selectors{/ts}</th>
{if $twingle_use_shop eq 1}
<th>{ts domain="de.systopia.twingle"}Shop Integration{/ts}</th>
{/if}
<th>{ts domain="de.systopia.twingle"}Properties{/ts}</th>
<th>{ts domain="de.systopia.twingle"}Used{/ts}</th>
<th>{ts domain="de.systopia.twingle"}Last Used{/ts}</th>
<th>{ts domain="de.systopia.twingle"}Operations{/ts}</th>
@ -36,32 +33,23 @@
</thead>
<tbody>
{foreach from=$profiles item=profile}
{assign var="profile_id" value=$profile.id}
{assign var="profile_name" value=$profile.name}
<tr class="twingle-profile-list">
<tr>
<td>{$profile.name}</td>
<td>
{if not $profile.is_default}
<ul>
{foreach from=$profile.selectors item=selector}
<li><strong></strong> {$selector}</li>
{/foreach}
</ul>
{/if}
<div><strong>{ts domain="de.systopia.twingle"}Selector{/ts}:</strong> {$profile.selector}</div>
</td>
{if $twingle_use_shop eq 1}
<td>{if $profile.enable_shop_integration}<span style="color:green">{ts domain="de.systopia.twingle"}enabled{/ts}</span>{else}<span>{ts domain="de.systopia.twingle"}disabled{/ts}</span>{/if}</td>
{/if}
<td>{ts domain="de.systopia.twingle"}{$profile_stats.$profile_name.access_counter_txt}{/ts}</td>
<td>{ts domain="de.systopia.twingle"}{$profile_stats.$profile_name.last_access_txt}{/ts}</td>
<td>
<a href="{crmURL p="civicrm/admin/settings/twingle/profile" q="op=edit&id=$profile_id"}" title="{ts domain="de.systopia.twingle" 1=$profile.name}Edit profile %1{/ts}" class="action-item crm-hover-button">{ts domain="de.systopia.twingle"}Edit{/ts}</a>
<a href="{crmURL p="civicrm/admin/settings/twingle/profile" q="op=copy&source_id=$profile_id"}" title="{ts domain="de.systopia.twingle" 1=$profile.name}Copy profile %1{/ts}" class="action-item crm-hover-button">{ts domain="de.systopia.twingle"}Copy{/ts}</a>
<a href="{crmURL p="civicrm/admin/settings/twingle/profile" q="op=edit&name=$profile_name"}" title="{ts domain="de.systopia.twingle" 1=$profile.name}Edit profile %1{/ts}" class="action-item crm-hover-button">{ts domain="de.systopia.twingle"}Edit{/ts}</a>
<a href="{crmURL p="civicrm/admin/settings/twingle/profile" q="op=copy&source_name=$profile_name"}" title="{ts domain="de.systopia.twingle" 1=$profile.name}Copy profile %1{/ts}" class="action-item crm-hover-button">{ts domain="de.systopia.twingle"}Copy{/ts}</a>
{if $profile_name == 'default'}
<a href="{crmURL p="civicrm/admin/settings/twingle/profile" q="op=delete&id=$profile_id"}" title="{ts domain="de.systopia.twingle" 1=$profile.name}Reset profile %1{/ts}" class="action-item crm-hover-button">{ts domain="de.systopia.twingle"}Reset{/ts}</a>
<a href="{crmURL p="civicrm/admin/settings/twingle/profile" q="op=delete&name=$profile_name"}" title="{ts domain="de.systopia.twingle" 1=$profile.name}Reset profile %1{/ts}" class="action-item crm-hover-button">{ts domain="de.systopia.twingle"}Reset{/ts}</a>
{else}
<a href="{crmURL p="civicrm/admin/settings/twingle/profile" q="op=delete&id=$profile_id"}" title="{ts domain="de.systopia.twingle" 1=$profile.name}Delete profile %1{/ts}" class="action-item crm-hover-button">{ts domain="de.systopia.twingle"}Delete{/ts}</a>
<a href="{crmURL p="civicrm/admin/settings/twingle/profile" q="op=delete&name=$profile_name"}" title="{ts domain="de.systopia.twingle" 1=$profile.name}Delete profile %1{/ts}" class="action-item crm-hover-button">{ts domain="de.systopia.twingle"}Delete{/ts}</a>
{/if}
</td>
</tr>
{/foreach}

View file

@ -1,33 +0,0 @@
version: "3"
services:
civicrm:
image: michaelmcandrew/civicrm:${CIVICRM_IMAGE_TAG:-5-drupal-php8.1}
environment:
- PROJECT_NAME=test
- BASE_URL=http://localhost
- CIVICRM_DB_NAME=test
- CIVICRM_DB_USER=root
- CIVICRM_DB_PASS=secret
- CIVICRM_DB_HOST=mysql
- CIVICRM_DB_PORT=3306
- DRUPAL_DB_NAME=test
- DRUPAL_DB_USER=root
- DRUPAL_DB_PASS=secret
- DRUPAL_DB_HOST=mysql
- DRUPAL_DB_PORT=3306
- PHP_DATE_TIMEZONE=UTC
- DEBUG=ON
- SMTP_HOST=localhost
- SMTP_MAILDOMAIN=example.org
volumes:
- ../:/var/www/html/sites/default/files/civicrm/ext/de.systopia.twingle:${BIND_VOLUME_PERMISSIONS:-ro}
- /var/www/html/sites/default/files/civicrm/ext/de.systopia.twingle/vendor
- /var/www/html/sites/default/files/civicrm/ext/de.systopia.twingle/tools/phpunit/vendor
# Don't start Apache HTTP Server, but keep container running
command: ["tail", "-f", "/dev/null"]
stop_signal: SIGKILL
mysql:
image: mariadb
environment:
MARIADB_ROOT_PASSWORD: secret
MARIADB_DATABASE: test

View file

@ -1,19 +0,0 @@
#!/bin/bash
set -eu -o pipefail
SCRIPT_DIR=$(realpath "$(dirname "$0")")
EXT_DIR=$(dirname "$SCRIPT_DIR")
cd "$EXT_DIR"
if [ ! -e tools/phpunit/vendor/bin ]; then
"$SCRIPT_DIR/docker-prepare.sh"
fi
export XDEBUG_MODE=coverage
# TODO: Remove when not needed, anymore.
# In Docker container with CiviCRM 5.5? all deprecations are reported as direct
# deprecations so "disabling" check of deprecation count is necessary for the
# tests to pass (if baselineFile does not contain all deprecations).
export SYMFONY_DEPRECATIONS_HELPER="max[total]=99999&baselineFile=./tests/ignored-deprecations.json"
composer phpunit -- --cache-result-file=/tmp/.phpunit.result.cache "$@"

View file

@ -1,45 +0,0 @@
#!/bin/bash
set -eu -o pipefail
EXT_DIR=$(dirname "$(dirname "$(realpath "$0")")")
EXT_NAME=$(basename "$EXT_DIR")
i=0
while ! mysql -h "$CIVICRM_DB_HOST" -P "$CIVICRM_DB_PORT" -u "$CIVICRM_DB_USER" --password="$CIVICRM_DB_PASS" -e 'SELECT 1;' >/dev/null 2>&1; do
i=$((i+1))
if [ $i -gt 10 ]; then
echo "Failed to connect to database" >&2
exit 1
fi
echo -n .
sleep 1
done
echo
export XDEBUG_MODE=off
if mysql -h "$CIVICRM_DB_HOST" -P "$CIVICRM_DB_PORT" -u "$CIVICRM_DB_USER" --password="$CIVICRM_DB_PASS" "$CIVICRM_DB_NAME" -e 'SELECT 1 FROM civicrm_setting LIMIT 1;' >/dev/null 2>&1; then
cv flush
else
# For headless tests it is required that CIVICRM_UF is defined using the corresponding env variable.
sed -E "s/define\('CIVICRM_UF', '([^']+)'\);/define('CIVICRM_UF', getenv('CIVICRM_UF') ?: '\1');/g" \
-i /var/www/html/sites/default/civicrm.settings.php
civicrm-docker-install
# Avoid this error:
# The autoloader expected class "Civi\ActionSchedule\Mapping" to be defined in
# file "[...]/Civi/ActionSchedule/Mapping.php". The file was found but the
# class was not in it, the class name or namespace probably has a typo.
rm -f /var/www/html/sites/all/modules/civicrm/Civi/ActionSchedule/Mapping.php
# For headless tests these files need to exist.
touch /var/www/html/sites/all/modules/civicrm/sql/test_data.mysql
touch /var/www/html/sites/all/modules/civicrm/sql/test_data_second_domain.mysql
cv ext:enable "$EXT_NAME"
fi
cd "$EXT_DIR"
composer update --no-progress --prefer-dist --optimize-autoloader --no-dev
composer composer-phpunit -- update --no-progress --prefer-dist

View file

@ -1 +0,0 @@
[]

View file

@ -1,54 +0,0 @@
<?php
use Civi\Test\CiviEnvBuilder;
use Civi\Test\HeadlessInterface;
use Civi\Test\HookInterface;
use Civi\Test\TransactionalInterface;
/**
* TwingleProduct.Create API Test Case
* This is a generic test class implemented with PHPUnit.
* @group headless
*/
class api_v3_TwingleProduct_CreateTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
use \Civi\Test\Api3TestTrait;
/**
* Set up for headless tests.
*
* Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
*
* See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
*/
public function setUpHeadless(): CiviEnvBuilder {
return \Civi\Test::headless()
->installMe(__DIR__)
->apply();
}
/**
* The setup() method is executed before the test is executed (optional).
*/
public function setUp() {
parent::setUp();
}
/**
* The tearDown() method is executed after the test was executed (optional)
* This can be used for cleanup.
*/
public function tearDown() {
parent::tearDown();
}
/**
* Simple example test case.
*
* Note how the function name begins with the word "test".
*/
public function testApiExample() {
$result = civicrm_api3('TwingleProduct', 'create', array('magicword' => 'sesame'));
$this->assertEquals('Twelve', $result['values'][12]['name']);
}
}

View file

@ -1,54 +0,0 @@
<?php
use Civi\Test\CiviEnvBuilder;
use Civi\Test\HeadlessInterface;
use Civi\Test\HookInterface;
use Civi\Test\TransactionalInterface;
/**
* TwingleProduct.Delete API Test Case
* This is a generic test class implemented with PHPUnit.
* @group headless
*/
class api_v3_TwingleProduct_DeleteTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
use \Civi\Test\Api3TestTrait;
/**
* Set up for headless tests.
*
* Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
*
* See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
*/
public function setUpHeadless(): CiviEnvBuilder {
return \Civi\Test::headless()
->installMe(__DIR__)
->apply();
}
/**
* The setup() method is executed before the test is executed (optional).
*/
public function setUp() {
parent::setUp();
}
/**
* The tearDown() method is executed after the test was executed (optional)
* This can be used for cleanup.
*/
public function tearDown() {
parent::tearDown();
}
/**
* Simple example test case.
*
* Note how the function name begins with the word "test".
*/
public function testApiExample() {
$result = civicrm_api3('TwingleProduct', 'delete', array('magicword' => 'sesame'));
$this->assertEquals('Twelve', $result['values'][12]['name']);
}
}

View file

@ -1,54 +0,0 @@
<?php
use Civi\Test\CiviEnvBuilder;
use Civi\Test\HeadlessInterface;
use Civi\Test\HookInterface;
use Civi\Test\TransactionalInterface;
/**
* TwingleProduct.Get API Test Case
* This is a generic test class implemented with PHPUnit.
* @group headless
*/
class api_v3_TwingleProduct_GetTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
use \Civi\Test\Api3TestTrait;
/**
* Set up for headless tests.
*
* Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
*
* See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
*/
public function setUpHeadless(): CiviEnvBuilder {
return \Civi\Test::headless()
->installMe(__DIR__)
->apply();
}
/**
* The setup() method is executed before the test is executed (optional).
*/
public function setUp() {
parent::setUp();
}
/**
* The tearDown() method is executed after the test was executed (optional)
* This can be used for cleanup.
*/
public function tearDown() {
parent::tearDown();
}
/**
* Simple example test case.
*
* Note how the function name begins with the word "test".
*/
public function testApiExample() {
$result = civicrm_api3('TwingleProduct', 'get', array('magicword' => 'sesame'));
$this->assertEquals('Twelve', $result['values'][12]['name']);
}
}

View file

@ -1,54 +0,0 @@
<?php
use Civi\Test\CiviEnvBuilder;
use Civi\Test\HeadlessInterface;
use Civi\Test\HookInterface;
use Civi\Test\TransactionalInterface;
/**
* TwingleProduct.Getsingle API Test Case
* This is a generic test class implemented with PHPUnit.
* @group headless
*/
class api_v3_TwingleProduct_GetsingleTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
use \Civi\Test\Api3TestTrait;
/**
* Set up for headless tests.
*
* Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
*
* See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
*/
public function setUpHeadless(): CiviEnvBuilder {
return \Civi\Test::headless()
->installMe(__DIR__)
->apply();
}
/**
* The setup() method is executed before the test is executed (optional).
*/
public function setUp() {
parent::setUp();
}
/**
* The tearDown() method is executed after the test was executed (optional)
* This can be used for cleanup.
*/
public function tearDown() {
parent::tearDown();
}
/**
* Simple example test case.
*
* Note how the function name begins with the word "test".
*/
public function testApiExample() {
$result = civicrm_api3('TwingleProduct', 'getsingle', array('magicword' => 'sesame'));
$this->assertEquals('Twelve', $result['values'][12]['name']);
}
}

View file

@ -1,54 +0,0 @@
<?php
use Civi\Test\CiviEnvBuilder;
use Civi\Test\HeadlessInterface;
use Civi\Test\HookInterface;
use Civi\Test\TransactionalInterface;
/**
* TwingleShop.Create API Test Case
* This is a generic test class implemented with PHPUnit.
* @group headless
*/
class api_v3_TwingleShop_CreateTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
use \Civi\Test\Api3TestTrait;
/**
* Set up for headless tests.
*
* Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
*
* See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
*/
public function setUpHeadless(): CiviEnvBuilder {
return \Civi\Test::headless()
->installMe(__DIR__)
->apply();
}
/**
* The setup() method is executed before the test is executed (optional).
*/
public function setUp() {
parent::setUp();
}
/**
* The tearDown() method is executed after the test was executed (optional)
* This can be used for cleanup.
*/
public function tearDown() {
parent::tearDown();
}
/**
* Simple example test case.
*
* Note how the function name begins with the word "test".
*/
public function testApiExample() {
$result = civicrm_api3('TwingleShop', 'create', array('magicword' => 'sesame'));
$this->assertEquals('Twelve', $result['values'][12]['name']);
}
}

View file

@ -1,54 +0,0 @@
<?php
use Civi\Test\CiviEnvBuilder;
use Civi\Test\HeadlessInterface;
use Civi\Test\HookInterface;
use Civi\Test\TransactionalInterface;
/**
* TwingleShop.Delete API Test Case
* This is a generic test class implemented with PHPUnit.
* @group headless
*/
class api_v3_TwingleShop_DeleteTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
use \Civi\Test\Api3TestTrait;
/**
* Set up for headless tests.
*
* Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
*
* See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
*/
public function setUpHeadless(): CiviEnvBuilder {
return \Civi\Test::headless()
->installMe(__DIR__)
->apply();
}
/**
* The setup() method is executed before the test is executed (optional).
*/
public function setUp() {
parent::setUp();
}
/**
* The tearDown() method is executed after the test was executed (optional)
* This can be used for cleanup.
*/
public function tearDown() {
parent::tearDown();
}
/**
* Simple example test case.
*
* Note how the function name begins with the word "test".
*/
public function testApiExample() {
$result = civicrm_api3('TwingleShop', 'delete', array('magicword' => 'sesame'));
$this->assertEquals('Twelve', $result['values'][12]['name']);
}
}

View file

@ -1,54 +0,0 @@
<?php
use Civi\Test\CiviEnvBuilder;
use Civi\Test\HeadlessInterface;
use Civi\Test\HookInterface;
use Civi\Test\TransactionalInterface;
/**
* TwingleShop.Get API Test Case
* This is a generic test class implemented with PHPUnit.
* @group headless
*/
class api_v3_TwingleShop_GetTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
use \Civi\Test\Api3TestTrait;
/**
* Set up for headless tests.
*
* Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
*
* See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
*/
public function setUpHeadless(): CiviEnvBuilder {
return \Civi\Test::headless()
->installMe(__DIR__)
->apply();
}
/**
* The setup() method is executed before the test is executed (optional).
*/
public function setUp() {
parent::setUp();
}
/**
* The tearDown() method is executed after the test was executed (optional)
* This can be used for cleanup.
*/
public function tearDown() {
parent::tearDown();
}
/**
* Simple example test case.
*
* Note how the function name begins with the word "test".
*/
public function testApiExample() {
$result = civicrm_api3('TwingleShop', 'get', array('magicword' => 'sesame'));
$this->assertEquals('Twelve', $result['values'][12]['name']);
}
}

View file

@ -1,54 +0,0 @@
<?php
use Civi\Test\CiviEnvBuilder;
use Civi\Test\HeadlessInterface;
use Civi\Test\HookInterface;
use Civi\Test\TransactionalInterface;
/**
* TwingleShop.Getsingle API Test Case
* This is a generic test class implemented with PHPUnit.
* @group headless
*/
class api_v3_TwingleShop_GetsingleTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
use \Civi\Test\Api3TestTrait;
/**
* Set up for headless tests.
*
* Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
*
* See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
*/
public function setUpHeadless(): CiviEnvBuilder {
return \Civi\Test::headless()
->installMe(__DIR__)
->apply();
}
/**
* The setup() method is executed before the test is executed (optional).
*/
public function setUp() {
parent::setUp();
}
/**
* The tearDown() method is executed after the test was executed (optional)
* This can be used for cleanup.
*/
public function tearDown() {
parent::tearDown();
}
/**
* Simple example test case.
*
* Note how the function name begins with the word "test".
*/
public function testApiExample() {
$result = civicrm_api3('TwingleShop', 'getsingle', array('magicword' => 'sesame'));
$this->assertEquals('Twelve', $result['values'][12]['name']);
}
}

View file

@ -1,65 +0,0 @@
<?php
ini_set('memory_limit', '2G');
// phpcs:disable
eval(cv('php:boot --level=classloader', 'phpcode'));
// phpcs:enable
// Allow autoloading of PHPUnit helper classes in this extension.
$loader = new \Composer\Autoload\ClassLoader();
$loader->add('CRM_', [__DIR__ . '/../..', __DIR__]);
$loader->addPsr4('Civi\\', [__DIR__ . '/../../Civi', __DIR__ . '/Civi']);
$loader->add('api_', [__DIR__ . '/../..', __DIR__]);
$loader->addPsr4('api\\', [__DIR__ . '/../../api', __DIR__ . '/api']);
$loader->register();
/**
* Call the "cv" command.
*
* @param string $cmd
* The rest of the command to send.
* @param string $decode
* Ex: 'json' or 'phpcode'.
* @return mixed
* Response output (if the command executed normally).
* For 'raw' or 'phpcode', this will be a string. For 'json', it could be any JSON value.
* @throws \RuntimeException
* If the command terminates abnormally.
*/
function cv(string $cmd, string $decode = 'json') {
$cmd = 'cv ' . $cmd;
$descriptorSpec = [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => STDERR];
$oldOutput = getenv('CV_OUTPUT');
putenv('CV_OUTPUT=json');
// Execute `cv` in the original folder. This is a work-around for
// phpunit/codeception, which seem to manipulate PWD.
$cmd = sprintf('cd %s; %s', escapeshellarg(getenv('PWD')), $cmd);
$process = proc_open($cmd, $descriptorSpec, $pipes, __DIR__);
putenv("CV_OUTPUT=$oldOutput");
fclose($pipes[0]);
$result = stream_get_contents($pipes[1]);
fclose($pipes[1]);
if (proc_close($process) !== 0) {
throw new RuntimeException("Command failed ($cmd):\n$result");
}
switch ($decode) {
case 'raw':
return $result;
case 'phpcode':
// If the last output is /*PHPCODE*/, then we managed to complete execution.
if (substr(trim($result), 0, 12) !== '/*BEGINPHP*/' || substr(trim($result), -10) !== '/*ENDPHP*/') {
throw new \RuntimeException("Command failed ($cmd):\n$result");
}
return $result;
case 'json':
return json_decode($result, 1);
default:
throw new RuntimeException("Bad decoder format ($decode)");
}
}

View file

@ -1,11 +0,0 @@
{
"repositories": [
{
"type": "git",
"url": "https://github.com/civicrm/coder.git"
}
],
"require": {
"drupal/coder": "dev-8.x-2.x-civi"
}
}

View file

@ -1,18 +0,0 @@
{
"require": {
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "^1.7",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.1",
"phpstan/phpstan-strict-rules": "^1.2",
"phpstan/phpstan-webmozart-assert": "^1.2",
"thecodingmachine/phpstan-strict-rules": "^1.0",
"voku/phpstan-rules": "^3.0"
},
"config": {
"allow-plugins": {
"phpstan/extension-installer": true
},
"sort-packages": true
}
}

View file

@ -1,13 +0,0 @@
{
"require": {
"symfony/phpunit-bridge": "^6.1"
},
"scripts": {
"post-install-cmd": [
"@php vendor/bin/simple-phpunit install"
],
"post-update-cmd": [
"@php vendor/bin/simple-phpunit install"
]
}
}

View file

@ -5,35 +5,10 @@ use CRM_Twingle_ExtensionUtil as E;
/**
* Implements hook_civicrm_pre().
*
* @throws \Civi\Twingle\Shop\Exceptions\ProductException
* @throws \CRM_Core_Exception
* @throws \Civi\Twingle\Shop\Exceptions\ShopException
*/
function twingle_civicrm_pre($op, $objectName, $id, &$params) {
if ($objectName == 'ContributionRecur' && $op == 'edit') {
CRM_Twingle_Tools::checkRecurringContributionChange((int) $id, $params);
}
// Create/delete PriceField and PriceFieldValue for TwingleProduct
elseif ($objectName == 'TwingleProduct') {
$twingle_product = new CRM_Twingle_BAO_TwingleProduct();
$twingle_product->load($params);
if ($op == 'create' || $op == 'edit') {
$twingle_product->createPriceField();
}
elseif ($op == 'delete') {
$twingle_product->deletePriceField();
}
$params = $twingle_product->getAttributes();
}
// Create PriceSet for TwingleShop
elseif ($objectName == 'TwingleShop' && ($op == 'create' || $op == 'edit')) {
$twingle_shop = new CRM_Twingle_BAO_TwingleShop();
$twingle_shop->load($params);
$twingle_shop->createPriceSet();
$params = $twingle_shop->getAttributes();
CRM_Twingle_Tools::checkRecurringContributionChange($id, $params);
}
}

View file

@ -1,10 +0,0 @@
<?php
// This file declares a new entity type. For more details, see "hook_civicrm_entityTypes" at:
// https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_entityTypes
return [
[
'name' => 'TwingleProduct',
'class' => 'CRM_Twingle_DAO_TwingleProduct',
'table' => 'civicrm_twingle_product',
],
];

View file

@ -1,75 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1" ?>
<table>
<base>CRM/Twingle</base>
<class>TwingleProduct</class>
<name>civicrm_twingle_product</name>
<comment>This table contains the Twingle Product data.</comment>
<log>false</log>
<!-- <add>1.5</add>-->
<field>
<name>id</name>
<type>int unsigned</type>
<required>true</required>
<unique>true</unique>
<comment>Unique TwingleProduct ID</comment>
<html>
<type>Number</type>
</html>
</field>
<primaryKey>
<name>id</name>
<autoincrement>true</autoincrement>
</primaryKey>
<field>
<name>external_id</name>
<type>int unsigned</type>
<required>true</required>
<comment>The ID of this product in the Twingle database</comment>
<html>
<type>Number</type>
</html>
</field>
<field>
<name>price_field_id</name>
<type>int unsigned</type>
<comment>FK to Price Field</comment>
<required>true</required>
</field>
<foreignKey>
<name>price_field_id</name>
<table>civicrm_contact</table>
<key>id</key>
<onDelete>CASCADE</onDelete>
</foreignKey>
<field>
<name>twingle_shop_id</name>
<type>int unsigned</type>
<unique>true</unique>
<comment>FK to Twingle Shop</comment>
</field>
<foreignKey>
<name>twingle_shop_id</name>
<table>civicrm_twingle_shop</table>
<key>id</key>
<onDelete>CASCADE</onDelete>
</foreignKey>
<field>
<name>created_at</name>
<type>datetime</type>
<required>true</required>
<comment>Timestamp of when the product was created in the database</comment>
</field>
<field>
<name>updated_at</name>
<type>datetime</type>
<required>true</required>
<comment>Timestamp of when the product was last updated in the database</comment>
</field>
</table>

View file

@ -1,10 +0,0 @@
<?php
// This file declares a new entity type. For more details, see "hook_civicrm_entityTypes" at:
// https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_entityTypes
return [
[
'name' => 'TwingleShop',
'class' => 'CRM_Twingle_DAO_TwingleShop',
'table' => 'civicrm_twingle_shop',
],
];

View file

@ -1,73 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1" ?>
<table>
<base>CRM/Twingle</base>
<class>TwingleShop</class>
<name>civicrm_twingle_shop</name>
<comment>This table contains the Twingle Shop data. Each Twingle Shop is linked to a corresponding Price Set.</comment>
<log>false</log>
<!-- <add>1.5</add>-->
<field>
<name>id</name>
<type>int unsigned</type>
<required>true</required>
<unique>true</unique>
<comment>Unique TwingleShop ID</comment>
<html>
<type>Number</type>
</html>
</field>
<primaryKey>
<name>id</name>
<autoincrement>true</autoincrement>
</primaryKey>
<field>
<name>project_identifier</name>
<type>varchar</type>
<length>32</length>
<required>true</required>
<unique>true</unique>
<comment>Twingle Project Identifier</comment>
<html>
<type>Text</type>
</html>
</field>
<field>
<name>numerical_project_id</name>
<type>int unsigned</type>
<required>true</required>
<unique>true</unique>
<comment>Numerical Twingle Project Identifier</comment>
<html>
<type>Number</type>
</html>
</field>
<field>
<name>price_set_id</name>
<type>int unsigned</type>
<unique>true</unique>
<comment>FK to Price Set</comment>
</field>
<foreignKey>
<name>price_set_id</name>
<table>civicrm_price_set</table>
<key>id</key>
<onDelete>CASCADE</onDelete>
</foreignKey>
<field>
<name>name</name>
<type>varchar</type>
<unique>false</unique>
<length>64</length>
<required>true</required>
<comment>name of the shop</comment>
<html>
<type>Text</type>
</html>
</field>
</table>