Compare commits

...
Sign in to create a new pull request.

85 commits

Author SHA1 Message Date
Jens Schuppe
b8f44d962d Back to dev (1.6-dev) 2025-02-21 13:14:04 +01:00
Jens Schuppe
21f29ce169 Version 1.5.0 2025-02-21 13:13:44 +01:00
Jens Schuppe
c8a577b651 Back to dev (1.5-dev) 2025-01-23 15:18:14 +01:00
Jens Schuppe
2ee06faf34 Version 1.5-beta4 2025-01-23 15:17:54 +01:00
Jens Schuppe
b9b26d9524 Merge branch 'fixClassNamespaces'
[#105] Fix BAO class namespace issues
2024-10-09 13:12:32 +02:00
Jens Schuppe
c7c766d926 Fix BAO class namespace issues 2024-10-09 12:39:57 +02:00
Jens Schuppe
82952a0162 Back to 1.5-dev 2024-10-01 14:28:06 +02:00
Jens Schuppe
eaf9d53169 Version 1.5-beta3 2024-10-01 14:27:53 +02:00
Jens Schuppe
e26b5c3933 Merge branch 'germanTranslation'
[#103] Update German translation
2024-10-01 14:25:49 +02:00
Jens Schuppe
82456d2ae4 Update German translation 2024-10-01 14:24:23 +02:00
Jens Schuppe
9c9fed20d7 Back to 1.5-dev 2024-09-24 14:29:38 +02:00
Jens Schuppe
30c34f72be Version 1.5-beta2 2024-09-24 14:29:26 +02:00
Jens Schuppe
355a377c4f Merge branch 'translationTemplate'
[#101] Update translation template and fix incorrect use of ts()
2024-09-24 14:28:58 +02:00
Jens Schuppe
612224901a Update translation template 2024-09-24 14:28:34 +02:00
Jens Schuppe
fa301676e3 Update translation template and fix incorrect use of ts() 2024-09-24 14:26:37 +02:00
Jens Schuppe
2834f8028d Merge branch 'formTemplates'
[#100] Fix form template issues (help texts, undefined template variables, etc.)
2024-09-24 14:26:16 +02:00
Jens Schuppe
f6cd3614c3 Merge branch 'twingleShopFixes'
[#99] Fix DAO/BAO namespaces and definitions
2024-09-24 14:25:52 +02:00
Jens Schuppe
4ae20a1b04 Fix form template issues (help texts, undefined template variables, etc.) 2024-09-11 13:19:54 +02:00
Jens Schuppe
64d6b48813 Fix DAO/BAO namespaces and definitions 2024-09-11 12:59:22 +02:00
Jens Schuppe
7cca29b458 Back to 1.5-dev 2024-09-03 13:19:48 +02:00
Jens Schuppe
8de34f7b2a Version 1.5-beta1 2024-09-03 13:19:27 +02:00
Jens Schuppe
26132e785e Merge branch 'integrate_twingle_shop'
[#69] Integrate Twingle Shop
2024-09-03 13:13:04 +02:00
Marc Michalsky
33cb076d42 activate update button on financial type change 2024-09-02 15:26:12 +02:00
Marc Michalsky
d3ccb3b092 allow products with self-selected price 2024-09-02 15:26:12 +02:00
Marc Michalsky
477c57ca53 fix another messed up merge 2024-09-02 15:26:12 +02:00
Marc Michalsky
ac892c9afc fix messed up merge conflicts 2024-09-02 15:26:12 +02:00
Marc Michalsky
1fc6529064 set price_field to is_required = false 2024-09-02 15:26:12 +02:00
Marc Michalsky
8cfa270dff implement TwingleShop integration 2024-09-02 15:26:12 +02:00
Marc Michalsky
ea46e6a747 cherry pick of "make sure that default values are present" 2024-09-02 15:25:51 +02:00
Marc Michalsky
1a5f77c090 refactoring 2024-09-02 15:25:51 +02:00
Marc Michalsky
eacc9cf496 pass $error_code to parent BaseException 2024-09-02 15:25:51 +02:00
Marc Michalsky
7c7c040b30 add error code for profile validation warning 2024-09-02 15:25:51 +02:00
Marc Michalsky
0f947e4277 override the $code property inherited from Exception in BaseException 2024-09-02 15:25:51 +02:00
Marc Michalsky
db94f26d6d use new namespace style 2024-09-02 15:25:51 +02:00
Marc Michalsky
c971b6f8eb let CRM_Twingle_Profile class handle its validation 2024-09-02 15:25:51 +02:00
Marc Michalsky
72bfa3fb2c create custom exceptions 2024-09-02 15:25:51 +02:00
Jens Schuppe
6606d09dce Back to 1.5-dev 2024-06-13 13:47:22 +02:00
Jens Schuppe
a363e1c888 Version 1.5-alpha2 2024-06-13 13:47:07 +02:00
Jens Schuppe
9ff9234644 Merge remote-tracking branch 'MarcMichalsky/improve_api_result_values' 2024-06-13 13:38:43 +02:00
Marc Michalsky
07435ad997
remove unnecessary reset() for $params[$target] 2024-06-13 13:35:09 +02:00
Jens Schuppe
8d30b2a52a Fix excess reset() in note creation 2024-06-13 13:34:49 +02:00
Jens Schuppe
96d0e5fbec Code style 2024-06-13 13:24:34 +02:00
Jens Schuppe
f7b15ac4f6 Update German translation 2024-06-13 13:03:57 +02:00
Jens Schuppe
a8c8401be3 Update translation template 2024-06-13 13:03:49 +02:00
Jens Schuppe
61f45034c6 Update help text for custom field mapping configuration option to reflect that all parameters are supported. 2024-06-13 13:03:17 +02:00
Marc Michalsky
ff24256bc1
improve contribution result values 2024-06-12 15:28:41 +02:00
Marc Michalsky
c0af2e16ab
improve SEPA mandate result values 2024-06-12 15:27:01 +02:00
Marc Michalsky
67283fa1a7
improve donation receipt result values 2024-06-12 15:27:00 +02:00
Marc Michalsky
90f27f70c7
improve newsletter subcription result values 2024-06-12 15:26:59 +02:00
Jens Schuppe
221f9c72f3 Back to 1.5-dev 2024-06-12 15:21:10 +02:00
Jens Schuppe
1dbc842253 Version 1.5-alpha1 2024-06-12 15:20:52 +02:00
Jens Schuppe
0b9c1709fd Merge branch 'fix_incorrect_fixes'
[#92] Some more fixed conditions
2024-06-12 15:19:49 +02:00
Jens Schuppe
0cf7ccb1cb Code style 2024-06-12 15:18:35 +02:00
Marc Michalsky
cd008d9545
fix: Check for $creditor_id always fails 2024-06-12 15:02:36 +02:00
Marc Michalsky
4432060d05
add a condition to check if the value is 1 2024-06-12 15:01:55 +02:00
Marc Michalsky
ab5d0906d7
fix accidentally flipped condition 2024-06-12 15:01:54 +02:00
Jens Schuppe
d9d68fa937 Merge branch 'donationReceiptOrganization'
[#94] Add the organisation instead of the individual to donation receipts group
2024-06-12 14:51:54 +02:00
Marc Michalsky
03a37aed31 add the organisation instead of the individual to donation_receipt group 2024-06-12 14:47:48 +02:00
Jens Schuppe
933c51c48e Merge branch 'issue/84'
[#88] Improve profile options for note creation
2024-06-12 14:44:26 +02:00
Jens Schuppe
ac1b08b775 Code style 2024-06-12 14:35:44 +02:00
Marc Michalsky
cf9483ca3e
fix: "in_array() expects parameter 2 to be array, null given" 2024-06-12 11:08:03 +02:00
Marc Michalsky
f5ab576ed9
remove html tags from transalation strings 2024-06-11 15:03:15 +02:00
Marc Michalsky
0caf9bf98e
Revert "fix obsolete use of CRM_Utils_Array::first()" 2024-06-11 14:57:15 +02:00
Marc Michalsky
bea59b4365
use strict comparisons 2024-06-11 14:57:14 +02:00
Marc Michalsky
089bbdf934
use api4 instead of api3 2024-06-11 14:57:13 +02:00
Marc Michalsky
1ddcc217e3
fix obsolete use of CRM_Utils_Array::first() 2024-06-11 14:57:13 +02:00
Marc Michalsky
87ca179791
add German translations 2024-06-11 14:57:12 +02:00
Marc Michalsky
547254158c
add Upgrader to maintain profile behaviour 2024-06-11 14:57:11 +02:00
Marc Michalsky
758b793c0d
add logic to create selected contact and contribution notes 2024-06-11 14:57:11 +02:00
Marc Michalsky
9836168122
add profile settings for note mapping 2024-06-11 14:57:10 +02:00
Jens Schuppe
aee56769b7 Merge branch 'settingsTemplate'
[#91] Repair help links in settings form
2024-06-06 12:03:56 +02:00
Jens Schuppe
8020491bf1 Repair help links in settings form (and reformat) 2024-06-06 12:01:05 +02:00
Marc Michalsky
f5723a4e7d
fix obsolete use of CRM_Utils_Array::first() 2024-06-06 11:57:32 +02:00
Jens Schuppe
5169e5a0ce Merge branch 'issue/86' 2024-06-06 11:07:15 +02:00
Jens Schuppe
d1f3dd871c Merge branch 'defaultProfileProjectId'
[#90] Do not require project IDs for default profile forms
2024-06-06 11:04:45 +02:00
Jens Schuppe
75676d42eb Fix condition in profile form template for not showing selector field for default profile 2024-06-06 11:01:56 +02:00
Jens Schuppe
a0b2879b69 Prevent undefined index warnings for unchecked checkbpxes in the settings form 2024-06-06 10:56:49 +02:00
Jens Schuppe
c7f1b7cb6e Do not require project IDs for default profile forms 2024-06-06 10:33:19 +02:00
Jens Schuppe
2b8ab813db Fix ambiguous conditions replacing empty() 2024-05-10 12:27:33 +02:00
Marc Michalsky
63b7e4d3ee
add German translations 2024-04-30 16:52:30 +02:00
Marc Michalsky
b3f82fbfba
add Upgrader to maintain profile behaviour 2024-04-30 16:52:30 +02:00
Marc Michalsky
df51d59cea
add logic to create selected contact and contribution notes 2024-04-30 15:44:45 +02:00
Marc Michalsky
10f6ca4e89
add profile settings for note mapping 2024-04-30 15:44:45 +02:00
Marc Michalsky
48c49c1814
avoid the use of empty() 2024-04-30 10:21:32 +02:00
Marc Michalsky
02bac833de
fix #86 2024-04-26 16:17:02 +02:00
59 changed files with 8582 additions and 444 deletions

View file

@ -0,0 +1,676 @@
<?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

@ -0,0 +1,475 @@
<?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

@ -0,0 +1,325 @@
<?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

@ -0,0 +1,305 @@
<?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;
}
}

View file

@ -251,6 +251,7 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
// Assign template variables. // Assign template variables.
$this->assign('op', $this->_op); $this->assign('op', $this->_op);
$this->assign('twingle_use_shop', (int) Civi::settings()->get('twingle_use_shop'));
$this->assign('profile_name', $profile_name); $this->assign('profile_name', $profile_name);
$this->assign('is_default', $is_default); $this->assign('is_default', $is_default);
@ -354,6 +355,10 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
static::getPrefixOptions() static::getPrefixOptions()
); );
// Add script and css for Twingle Shop integration
Civi::resources()->addScriptUrl(E::url('js/twingle_shop.js'));
Civi::resources()->addStyleFile(E::LONG_NAME, 'css/twingle_shop.css');
$payment_instruments = CRM_Twingle_Profile::paymentInstruments(); $payment_instruments = CRM_Twingle_Profile::paymentInstruments();
$this->assign('payment_instruments', $payment_instruments); $this->assign('payment_instruments', $payment_instruments);
foreach ($payment_instruments as $pi_name => $pi_label) { foreach ($payment_instruments as $pi_name => $pi_label) {
@ -498,6 +503,67 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
[] []
); );
$this->add(
'select',
'map_as_contribution_notes',
E::ts('Create contribution notes for'),
[
'purpose' => E::ts('Purpose'),
'remarks' => E::ts('Remarks'),
],
// is not required
FALSE,
['class' => 'crm-select2 huge', 'multiple' => 'multiple']
);
$this->add(
'select',
'map_as_contact_notes',
E::ts('Create contact notes for'),
[
'user_extrafield' => E::ts('User Extra Field'),
],
// is not required
FALSE,
['class' => 'crm-select2 huge', 'multiple' => 'multiple']
);
if (Civi::settings()->get('twingle_use_shop')) {
$this->add(
'checkbox', // field type
'enable_shop_integration', // field name
E::ts('Enable Shop Integration'), // field label
FALSE,
[]
);
$this->add(
'select', // field type
'shop_financial_type', // field name
E::ts('Default Financial Type'), // field label
static::getFinancialTypes(), // list of options
TRUE,
['class' => 'crm-select2 huge']
);
$this->add(
'select', // field type
'shop_donation_financial_type', // field name
E::ts('Financial Type for top up donations'), // field label
static::getFinancialTypes(), // list of options
TRUE,
['class' => 'crm-select2 huge']
);
$this->add(
'checkbox', // field type
'shop_map_products', // field name
E::ts('Map Products as Price Fields'), // field label
FALSE, // is not required
[]
);
}
$this->addButtons([ $this->addButtons([
[ [
'type' => 'submit', 'type' => 'submit',
@ -571,7 +637,15 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
// Show warning when there is configuration missing for required fields. // Show warning when there is configuration missing for required fields.
$requiredConfig = CRM_Twingle_Profile::allowedAttributes(TRUE); $requiredConfig = CRM_Twingle_Profile::allowedAttributes(TRUE);
foreach ($requiredConfig as $key => $metadata) { foreach ($requiredConfig as $key => $metadata) {
if (!isset($profile_data[$key]) && $metadata['required']) { $required = $metadata['required'] ?? FALSE;
// Do not require twingle project IDs/selector for the default profile.
if (
$this->profile->is_default()
&& 'selector' === $key
) {
$required = FALSE;
}
if (!isset($profile_data[$key]) && $required) {
CRM_Core_Session::setStatus( CRM_Core_Session::setStatus(
E::ts( E::ts(
'The required configuration option "%1" has no value. Saving the profile might set this option to a possibly unwanted default value.', 'The required configuration option "%1" has no value. Saving the profile might set this option to a possibly unwanted default value.',
@ -956,5 +1030,4 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
} }
return static::$_campaigns; return static::$_campaigns;
} }
} }

View file

@ -37,6 +37,8 @@ class CRM_Twingle_Form_Settings extends CRM_Core_Form {
'twingle_protect_recurring_activity_subject', 'twingle_protect_recurring_activity_subject',
'twingle_protect_recurring_activity_status', 'twingle_protect_recurring_activity_status',
'twingle_protect_recurring_activity_assignee', 'twingle_protect_recurring_activity_assignee',
'twingle_use_shop',
'twingle_access_key',
]; ];
/** /**
@ -105,10 +107,22 @@ 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([ $this->addButtons([
[ [
'type' => 'submit', 'type' => 'submit',
'name' => E::ts('Save'), 'name' => E::ts('Save'),
'isDefault' => TRUE, 'isDefault' => TRUE,
], ],
]); ]);
@ -124,8 +138,7 @@ class CRM_Twingle_Form_Settings extends CRM_Core_Form {
} }
/** /**
* Custom form validation, because the activity creation fields * Custom form validation, as some fields are mandatory only when others are active.
* are only mandatory if activity creation is active
* @return bool * @return bool
*/ */
public function validate() { public function validate() {
@ -146,6 +159,14 @@ class CRM_Twingle_Form_Settings extends CRM_Core_Form {
} }
} }
// 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)); return (0 == count($this->_errors));
} }
@ -157,7 +178,7 @@ class CRM_Twingle_Form_Settings extends CRM_Core_Form {
// store settings // store settings
foreach (self::$SETTINGS_LIST as $setting) { foreach (self::$SETTINGS_LIST as $setting) {
Civi::settings()->set($setting, $values[$setting]); Civi::settings()->set($setting, $values[$setting] ?? NULL);
} }
parent::postProcess(); parent::postProcess();

View file

@ -33,6 +33,7 @@ class CRM_Twingle_Page_Profiles extends CRM_Core_Page {
} }
$this->assign('profiles', $profiles); $this->assign('profiles', $profiles);
$this->assign('profile_stats', CRM_Twingle_Profile::getProfileStats()); $this->assign('profile_stats', CRM_Twingle_Profile::getProfileStats());
$this->assign('twingle_use_shop', (int) Civi::settings()->get('twingle_use_shop'));
// Add custom css // Add custom css
Civi::resources()->addStyleFile(E::LONG_NAME, 'css/twingle.css'); Civi::resources()->addStyleFile(E::LONG_NAME, 'css/twingle.css');

View file

@ -43,6 +43,16 @@ class CRM_Twingle_Profile {
*/ */
protected $data; 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',
];
/** /**
* CRM_Twingle_Profile constructor. * CRM_Twingle_Profile constructor.
* *
@ -111,7 +121,8 @@ class CRM_Twingle_Profile {
*/ */
public function getCustomFieldMapping() { public function getCustomFieldMapping() {
$custom_field_mapping = []; $custom_field_mapping = [];
if (is_string($custom_field_definition = $this->getAttribute('custom_field_mapping'))) { if ('' !== ($custom_field_definition = $this->getAttribute('custom_field_mapping', ''))) {
/** @var string $custom_field_definition */
$custom_field_maps = preg_split( $custom_field_maps = preg_split(
'/\r\n|\r|\n/', '/\r\n|\r|\n/',
$custom_field_definition, $custom_field_definition,
@ -196,6 +207,16 @@ class CRM_Twingle_Profile {
); );
} }
/**
* 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. * Retrieves an attribute of the profile.
* *
@ -205,7 +226,9 @@ class CRM_Twingle_Profile {
* @return mixed | NULL * @return mixed | NULL
*/ */
public function getAttribute($attribute_name, $default = NULL) { public function getAttribute($attribute_name, $default = NULL) {
return $this->data[$attribute_name] ?? $default; return (isset($this->data[$attribute_name]) && $this->data[$attribute_name] !== '')
? $this->data[$attribute_name]
: $default;
} }
/** /**
@ -527,6 +550,12 @@ class CRM_Twingle_Profile {
'membership_postprocess_call' => ['required' => FALSE], 'membership_postprocess_call' => ['required' => FALSE],
'newsletter_double_opt_in' => ['required' => FALSE], 'newsletter_double_opt_in' => ['required' => FALSE],
'required_address_components' => ['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],
], ],
// Add payment methods. // Add payment methods.
array_combine( array_combine(
@ -643,6 +672,12 @@ class CRM_Twingle_Profile {
'city', 'city',
'country', '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. // Add contribution status for all payment methods.
// phpcs:ignore Drupal.Formatting.SpaceUnaryOperator.PlusMinus // phpcs:ignore Drupal.Formatting.SpaceUnaryOperator.PlusMinus
@ -659,7 +694,7 @@ class CRM_Twingle_Profile {
* @param string $project_id * @param string $project_id
* *
* @return CRM_Twingle_Profile * @return CRM_Twingle_Profile
* @throws \CRM_Twingle_Exceptions_ProfileException * @throws \Civi\Twingle\Exceptions\ProfileException
* @throws \Civi\Core\Exception\DBQueryException * @throws \Civi\Core\Exception\DBQueryException
*/ */
public static function getProfileForProject($project_id) { public static function getProfileForProject($project_id) {
@ -680,7 +715,10 @@ class CRM_Twingle_Profile {
return $default_profile; return $default_profile;
} }
else { else {
throw new ProfileException('Could not find default profile', ProfileException::ERROR_CODE_DEFAULT_PROFILE_NOT_FOUND); throw new ProfileException(
'Could not find default profile',
ProfileException::ERROR_CODE_DEFAULT_PROFILE_NOT_FOUND
);
} }
} }
@ -769,5 +807,4 @@ class CRM_Twingle_Profile {
} }
return $stats; return $stats;
} }
} }

View file

@ -17,6 +17,7 @@ declare(strict_types = 1);
use CRM_Twingle_ExtensionUtil as E; use CRM_Twingle_ExtensionUtil as E;
use Civi\Twingle\Exceptions\BaseException; use Civi\Twingle\Exceptions\BaseException;
use Civi\Twingle\Shop\Exceptions\LineItemException;
class CRM_Twingle_Submission { class CRM_Twingle_Submission {
@ -41,7 +42,19 @@ class CRM_Twingle_Submission {
public const EMPLOYER_RELATIONSHIP_TYPE_ID = 5; public const EMPLOYER_RELATIONSHIP_TYPE_ID = 5;
/** /**
* @param array<string, mixed> &$params * List of allowed product attributes.
*/
const ALLOWED_PRODUCT_ATTRIBUTES = [
'id',
'name',
'internal_id',
'price',
'count',
'total_value',
];
/**
* @param array &$params
* A reference to the parameters array of the submission. * A reference to the parameters array of the submission.
* *
* @param \CRM_Twingle_Profile $profile * @param \CRM_Twingle_Profile $profile
@ -72,8 +85,8 @@ class CRM_Twingle_Submission {
// Get the payment instrument defined within the profile, or return an error // Get the payment instrument defined within the profile, or return an error
// if none matches (i.e. an unknown payment method was submitted). // if none matches (i.e. an unknown payment method was submitted).
$payment_instrument_id = $profile->getAttribute('pi_' . $params['payment_method']); $payment_instrument_id = $profile->getAttribute('pi_' . $params['payment_method'], '');
if (!isset($payment_instrument_id)) { if ('' === $payment_instrument_id) {
throw new CRM_Core_Exception( throw new CRM_Core_Exception(
E::ts('Payment method could not be matched to existing payment instrument.'), E::ts('Payment method could not be matched to existing payment instrument.'),
'invalid_format' 'invalid_format'
@ -101,7 +114,7 @@ class CRM_Twingle_Submission {
// matches (i.e. an unknown gender was submitted). // matches (i.e. an unknown gender was submitted).
if (is_string($params['user_gender'])) { if (is_string($params['user_gender'])) {
$gender_id = $profile->getAttribute('gender_' . $params['user_gender']); $gender_id = $profile->getAttribute('gender_' . $params['user_gender']);
if (!isset($gender_id)) { if (!is_numeric($gender_id)) {
throw new CRM_Core_Exception( throw new CRM_Core_Exception(
E::ts('Gender could not be matched to existing gender.'), E::ts('Gender could not be matched to existing gender.'),
'invalid_format' 'invalid_format'
@ -123,6 +136,22 @@ class CRM_Twingle_Submission {
} }
} }
// 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. // Validate campaign_id, if given.
if (isset($params['campaign_id'])) { if (isset($params['campaign_id'])) {
// Check whether campaign_id is a numeric string and cast it to an integer. // Check whether campaign_id is a numeric string and cast it to an integer.
@ -433,4 +462,131 @@ class CRM_Twingle_Submission {
} }
} }
/**
* @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

@ -116,7 +116,10 @@ class CRM_Twingle_Tools {
* Recurring contribution fields. * Recurring contribution fields.
* @throws Exception could be one of the measures * @throws Exception could be one of the measures
*/ */
public static function processRecurringContributionTermination(int $recurring_contribution_id, array $recurring_contribution) { public static function processRecurringContributionTermination(
int $recurring_contribution_id,
array $recurring_contribution
) {
// check if we're suspended // check if we're suspended
if (self::$protection_suspended) { if (self::$protection_suspended) {
return; return;

View file

@ -102,4 +102,50 @@ class CRM_Twingle_Upgrader extends CRM_Extension_Upgrader_Base {
return TRUE; 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

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

12
Civi/Api4/TwingleShop.php Normal file
View file

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

View file

@ -28,7 +28,7 @@ use CRM_Twingle_ExtensionUtil as E;
class BaseException extends \Exception { class BaseException extends \Exception {
/** /**
* @var string * @var int|string
*/ */
protected $code; protected $code;
protected string $log_message; protected string $log_message;
@ -36,15 +36,13 @@ class BaseException extends \Exception {
/** /**
* BaseException Constructor * BaseException Constructor
* @param string $message * @param string $message
* Error message * Error message
* @param string $error_code * @param string $error_code
* A meaningful error code * A meaningful error code
* @param \Throwable $previous
* A previously thrown exception to include.
*/ */
public function __construct(string $message = '', string $error_code = '', \Throwable $previous = NULL) { public function __construct(string $message = '', string $error_code = '') {
parent::__construct($message, 1, $previous); parent::__construct($message, 1);
$this->log_message = '' !== $message ? E::LONG_NAME . ': ' . $message : ''; $this->log_message = !empty($message) ? E::LONG_NAME . ': ' . $message : '';
$this->code = $error_code; $this->code = $error_code;
} }

View file

@ -38,13 +38,8 @@ class ProfileValidationError extends BaseException {
* @param string $error_code * @param string $error_code
* A meaningful error code * A meaningful error code
*/ */
public function __construct( public function __construct(string $affected_field_name, string $message = '', string $error_code = '') {
string $affected_field_name, parent::__construct($message, $error_code);
string $message = '',
string $error_code = '',
?\Throwable $previous = NULL
) {
parent::__construct($message, $error_code, $previous);
$this->affected_field_name = $affected_field_name; $this->affected_field_name = $affected_field_name;
} }

View file

@ -0,0 +1,269 @@
<?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

@ -0,0 +1,19 @@
<?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

@ -0,0 +1,14 @@
<?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

@ -0,0 +1,26 @@
<?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

@ -0,0 +1,22 @@
<?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

@ -0,0 +1,154 @@
<?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

@ -17,6 +17,7 @@ declare(strict_types = 1);
use CRM_Twingle_ExtensionUtil as E; use CRM_Twingle_ExtensionUtil as E;
use Civi\Twingle\Exceptions\BaseException; use Civi\Twingle\Exceptions\BaseException;
use Civi\Api4\Note;
/** /**
* TwingleDonation.Submit API specification * TwingleDonation.Submit API specification
@ -253,7 +254,21 @@ function _civicrm_api3_twingle_donation_Submit_spec(&$params) {
'title' => E::ts('Custom fields'), 'title' => E::ts('Custom fields'),
'type' => CRM_Utils_Type::T_STRING, 'type' => CRM_Utils_Type::T_STRING,
'api.required' => 0, 'api.required' => 0,
'description' => E::ts('Additional information for either the contact or the (recurring) contribution.'), 'description' => E::ts('Additional information for either the contact or the (recurring) contribution.'),
];
$params['products'] = [
'name' => 'products',
'title' => E::ts('Products'),
'type' => CRM_Utils_Type::T_STRING,
'api.required' => 0,
'description' => E::ts('Products ordered via TwingleShop'),
];
$params['remarks'] = [
'name' => 'remarks',
'title' => E::ts('Remarks'),
'type' => CRM_Utils_Type::T_STRING,
'api.required' => 0,
'description' => E::ts('Additional remarks for the donation.'),
]; ];
} }
@ -420,7 +435,7 @@ function civicrm_api3_twingle_donation_Submit($params) {
// Get the prefix ID defined within the profile // Get the prefix ID defined within the profile
if ( if (
isset($params['user_gender']) isset($params['user_gender'])
&& NULL !== ($prefix_id = $profile->getAttribute('prefix_' . $params['user_gender'])) && is_numeric($prefix_id = $profile->getAttribute('prefix_' . $params['user_gender']))
) { ) {
$contact_data['prefix_id'] = $prefix_id; $contact_data['prefix_id'] = $prefix_id;
} }
@ -477,13 +492,21 @@ function civicrm_api3_twingle_donation_Submit($params) {
); );
} }
// Save user_extrafield as contact note. // Create contact notes.
if (isset($params['user_extrafield']) && '' != $params['user_extrafield']) { /** @phpstan-var array<string> $contact_note_mappings */
civicrm_api3('Note', 'create', [ $contact_note_mappings = $profile->getAttribute('map_as_contact_notes', []);
'entity_table' => 'civicrm_contact', foreach (['user_extrafield'] as $target) {
'entity_id' => $contact_id, if (
'note' => $params['user_extrafield'], isset($params[$target])
]); && '' !== $params[$target]
&& in_array($target, $contact_note_mappings, TRUE)
) {
Note::create(FALSE)
->addValue('entity_table', 'civicrm_contact')
->addValue('entity_id', $contact_id)
->addValue('note', $params[$target])
->execute();
}
} }
// Share organisation address with individual contact, using configured // Share organisation address with individual contact, using configured
@ -510,15 +533,16 @@ function civicrm_api3_twingle_donation_Submit($params) {
// If usage of double opt-in is selected, use MailingEventSubscribe.create // If usage of double opt-in is selected, use MailingEventSubscribe.create
// to add contact to newsletter groups defined in the profile // to add contact to newsletter groups defined in the profile
$result_values['newsletter']['newsletter_double_opt_in'] $result_values['newsletter_double_opt_in']
= (bool) $profile->getAttribute('newsletter_double_opt_in') = (bool) $profile->getAttribute('newsletter_double_opt_in')
? 'true' ? 'true'
: 'false'; : 'false';
if ( if (
(bool) $profile->getAttribute('newsletter_double_opt_in') (bool) $profile->getAttribute('newsletter_double_opt_in')
&& isset($params['newsletter']) && (bool) ($params['newsletter'] ?? FALSE)
&& is_array($groups = $profile->getAttribute('newsletter_groups')) && is_array($groups = $profile->getAttribute('newsletter_groups'))
) { ) {
// TODO: Ensure the values being integers.
$group_memberships = array_column( $group_memberships = array_column(
civicrm_api3( civicrm_api3(
'GroupContact', 'GroupContact',
@ -539,7 +563,7 @@ function civicrm_api3_twingle_donation_Submit($params) {
] ]
)['visibility'] == 'Public Pages'; )['visibility'] == 'Public Pages';
if (!in_array($group_id, $group_memberships, FALSE) && $is_public_group) { if (!in_array($group_id, $group_memberships, FALSE) && $is_public_group) {
$result_values['newsletter'][][$group_id] = civicrm_api3( $result = civicrm_api3(
'MailingEventSubscribe', 'MailingEventSubscribe',
'create', 'create',
[ [
@ -548,15 +572,18 @@ function civicrm_api3_twingle_donation_Submit($params) {
'contact_id' => $contact_id, 'contact_id' => $contact_id,
] ]
); );
$subscription = reset($result['values']);
$subscription['group_id'] = $group_id;
$result_values['newsletter_subscriptions'][] = $subscription;
} }
elseif ($is_public_group) { elseif ($is_public_group) {
$result_values['newsletter'][] = $group_id; $result_values['newsletter_group_ids'][] = $group_id;
} }
} }
// If requested, add contact to newsletter groups defined in the profile. // If requested, add contact to newsletter groups defined in the profile.
} }
elseif ( elseif (
isset($params['newsletter']) (bool) ($params['newsletter'] ?? FALSE)
&& is_array($groups = $profile->getAttribute('newsletter_groups')) && is_array($groups = $profile->getAttribute('newsletter_groups'))
) { ) {
foreach ($groups as $group_id) { foreach ($groups as $group_id) {
@ -568,14 +595,13 @@ function civicrm_api3_twingle_donation_Submit($params) {
'contact_id' => $contact_id, 'contact_id' => $contact_id,
] ]
); );
$result_values['newsletter_group_ids'][] = $group_id;
$result_values['newsletter'][] = $group_id;
} }
} }
// If requested, add contact to postinfo groups defined in the profile. // If requested, add contact to postinfo groups defined in the profile.
if ( if (
isset($params['postinfo']) (bool) ($params['postinfo'] ?? FALSE)
&& is_array($groups = $profile->getAttribute('postinfo_groups')) && is_array($groups = $profile->getAttribute('postinfo_groups'))
) { ) {
foreach ($groups as $group_id) { foreach ($groups as $group_id) {
@ -589,18 +615,18 @@ function civicrm_api3_twingle_donation_Submit($params) {
} }
// If requested, add contact to donation_receipt groups defined in the // If requested, add contact to donation_receipt groups defined in the
// profile. // profile. If an organisation is provided, add it to the groups instead.
// (see issue #83)
if ( if (
isset($params['donation_receipt']) (bool) ($params['donation_receipt'] ?? FALSE)
&& is_array($groups = $profile->getAttribute('donation_receipt_groups')) && is_array($groups = $profile->getAttribute('donation_receipt_groups'))
) { ) {
foreach ($groups as $group_id) { foreach ($groups as $group_id) {
civicrm_api3('GroupContact', 'create', [ civicrm_api3('GroupContact', 'create', [
'group_id' => $group_id, 'group_id' => $group_id,
'contact_id' => $contact_id, 'contact_id' => $organisation_id ?? $contact_id,
]); ]);
$result_values['donation_receipt_group_ids'][] = $group_id;
$result_values['donation_receipt'][] = $group_id;
} }
} }
@ -615,15 +641,16 @@ function civicrm_api3_twingle_donation_Submit($params) {
'total_amount' => $params['amount'] / 100, 'total_amount' => $params['amount'] / 100,
]; ];
// If the submission contains products, do not auto-create a line item
if (!empty($params['products']) && $profile->isShopEnabled()) {
$contribution_data['skipLineItem'] = 1;
}
// Add custom field values. // Add custom field values.
if (isset($custom_fields['Contribution'])) { if (isset($custom_fields['Contribution'])) {
$contribution_data += $custom_fields['Contribution']; $contribution_data += $custom_fields['Contribution'];
} }
if (isset($params['purpose'])) {
$contribution_data['note'] = $params['purpose'];
}
// set campaign, subject to configuration // set campaign, subject to configuration
CRM_Twingle_Submission::setCampaign($contribution_data, 'contribution', $params, $profile); CRM_Twingle_Submission::setCampaign($contribution_data, 'contribution', $params, $profile);
@ -653,7 +680,7 @@ function civicrm_api3_twingle_donation_Submit($params) {
} }
$creditor_id = $profile->getAttribute('sepa_creditor_id'); $creditor_id = $profile->getAttribute('sepa_creditor_id');
if (!is_int($creditor_id)) { if (!isset($creditor_id) || '' === $creditor_id) {
throw new BaseException( throw new BaseException(
E::ts('SEPA creditor is not configured for profile "%1".', [1 => $profile->getName()]) E::ts('SEPA creditor is not configured for profile "%1".', [1 => $profile->getName()])
); );
@ -711,7 +738,28 @@ function civicrm_api3_twingle_donation_Submit($params) {
// Create the mandate. // Create the mandate.
$mandate = civicrm_api3('SepaMandate', 'createfull', $mandate_data); $mandate = civicrm_api3('SepaMandate', 'createfull', $mandate_data);
$result_values['sepa_mandate'] = $mandate['values']; $result_values['sepa_mandate'] = reset($mandate['values']);
// Add contribution data to result_values for later use
$contribution_id = $result_values['sepa_mandate']['entity_id'];
if ($contribution_id) {
$contribution = civicrm_api3(
'Contribution',
'getsingle',
['id' => $contribution_id]
);
$result_values['contribution'] = $contribution;
} else {
$mandate_id = $result_values['sepa_mandate']['id'];
$message = E::LONG_NAME . ": could not find contribution for sepa mandate $mandate_id";
throw new CiviCRM_API3_Exception($message, 'api_error');
}
// Add products as line items to the contribution
if (!empty($params['products']) && $profile->isShopEnabled()) {
$line_items = CRM_Twingle_Submission::createLineItems($result_values, $params, $profile);
$result_values['contribution']['line_items'] = $line_items;
}
} }
else { else {
// Set financial type depending on donation rhythm. This applies for // Set financial type depending on donation rhythm. This applies for
@ -784,14 +832,40 @@ function civicrm_api3_twingle_donation_Submit($params) {
} }
$contribution = civicrm_api3('Contribution', 'create', $contribution_data); $contribution = civicrm_api3('Contribution', 'create', $contribution_data);
if ($contribution['is_error']) { /** @phpstan-var array{'values': array<int, array<mixed>>, 'is_error'?: string} $contribution */
if ((bool) ($contribution['is_error'] ?? FALSE)) {
throw new CRM_Core_Exception( throw new CRM_Core_Exception(
E::ts('Could not create contribution'), E::ts('Could not create contribution'),
'api_error' 'api_error'
); );
} }
$contribution = reset($contribution['values']);
/** @phpstan-var array{'id': int} $contribution */
$result_values['contribution'] = $contribution['values']; // Add notes to the contribution.
/** @phpstan-var array<string> $contribution_note_mappings */
$contribution_note_mappings = $profile->getAttribute('map_as_contribution_notes', []);
foreach (['purpose', 'remarks'] as $target) {
if (
in_array($target, $contribution_note_mappings, TRUE)
&& isset($params[$target])
&& '' !== $params[$target]
) {
Note::create(FALSE)
->addValue('entity_table', 'civicrm_contribution')
->addValue('entity_id', $contribution['id'])
->addValue('note', $params[$target])
->execute();
}
}
$result_values['contribution'] = $contribution;
// Add products as line items to the contribution
if (!empty($params['products']) && $profile->isShopEnabled()) {
$line_items = CRM_Twingle_Submission::createLineItems($result_values, $params, $profile);
$result_values['contribution']['line_items'] = $line_items;
}
} }
// MEMBERSHIP CREATION // MEMBERSHIP CREATION
@ -830,8 +904,8 @@ function civicrm_api3_twingle_donation_Submit($params) {
$result_values['membership'] = $membership; $result_values['membership'] = $membership;
// call the postprocess API // call the postprocess API
$postprocess_call = $profile->getAttribute('membership_postprocess_call'); if ('' !== ($postprocess_call = $profile->getAttribute('membership_postprocess_call', ''))) {
if (is_string($postprocess_call)) { /** @var string $postprocess_call */
[$pp_entity, $pp_action] = explode('.', $postprocess_call, 2); [$pp_entity, $pp_action] = explode('.', $postprocess_call, 2);
try { try {
// gather the contribution IDs // gather the contribution IDs

View file

@ -0,0 +1,137 @@
<?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

@ -0,0 +1,71 @@
<?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

@ -0,0 +1,136 @@
<?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

@ -0,0 +1,54 @@
<?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

@ -0,0 +1,79 @@
<?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

@ -0,0 +1,79 @@
<?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

@ -0,0 +1,96 @@
<?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'
);
}

111
api/v3/TwingleShop/Get.php Normal file
View file

@ -0,0 +1,111 @@
<?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

@ -0,0 +1,54 @@
<?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']];
}

37
css/twingle_shop.css Normal file
View file

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

744
js/twingle_shop.js Normal file
View file

@ -0,0 +1,744 @@
/**
* 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();
}
}
}

1600
l10n/de.systopia.twingle.pot Normal file

File diff suppressed because it is too large Load diff

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -1,276 +0,0 @@
#: ./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 ""

21
sql/auto_uninstall.sql Normal file
View file

@ -0,0 +1,21 @@
-- +--------------------------------------------------------------------+
-- | 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

@ -0,0 +1,66 @@
-- +--------------------------------------------------------------------+
-- | 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}
{htxt id='id-custom_field_mapping'} {htxt id='id-custom_field_mapping'}
{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> {ts domain="de.systopia.twingle"}<p>Map Twingle custom fields to CiviCRM 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> <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>Always use the <code>custom_[id]</code> notation for CiviCRM custom fields.</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>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>Only custom fields extending one of the following CiviCRM entities are allowed:</p> <p>Only custom fields extending one of the following CiviCRM entities are allowed:</p>
<ul> <ul>
<li><strong>Contact</strong> &ndash; Will be set on the Individual contact</li> <li><strong>Contact</strong> &ndash; Will be set on the Individual contact</li>
@ -75,4 +75,23 @@
<li><strong>ContributionRecur</strong> &ndash; Will be set on the recurring contribution and deriving single contributions</li> <li><strong>ContributionRecur</strong> &ndash; Will be set on the recurring contribution and deriving single contributions</li>
</ul>{/ts} </ul>{/ts}
{/htxt} {/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,7 @@
</tr> </tr>
<tr class="crm-section"> <tr class="crm-section">
{if not $form.is_default} {if not $is_default}
<td class="label">{$form.selector.label} <td class="label">{$form.selector.label}
<a <a
onclick=' onclick='
@ -46,7 +46,7 @@
class="helpicon" class="helpicon"
></a> ></a>
</td> </td>
<td class="content">{$form.selector.html}</td> <td id="selectors" class="content">{$form.selector.html}</td>
{/if} {/if}
</tr> </tr>
@ -313,6 +313,22 @@
<td class="content">{$form.contribution_source.html}</td> <td class="content">{$form.contribution_source.html}</td>
</tr> </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"> <tr class="crm-section">
<td class="label"> <td class="label">
{$form.custom_field_mapping.label} {$form.custom_field_mapping.label}
@ -337,6 +353,71 @@
</table> </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> </fieldset>
{elseif $op == 'delete'} {elseif $op == 'delete'}
@ -373,11 +454,18 @@
} }
} }
// register events and run once // register events
cj(document).ready(function (){ cj(document).ready(function (){
cj('#membership_type_id').change(twingle_membership_active_changed); cj('#membership_type_id').change(twingle_membership_active_changed);
cj('#membership_type_id_recur').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(); twingle_membership_active_changed();
</script> </script>
{/literal} {/literal}

View file

@ -27,3 +27,11 @@
{htxt id='id-twingle_prefix'} {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} {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}
{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,47 +18,40 @@
<table class="form-layout-compressed"> <table class="form-layout-compressed">
<tr class="crm-twingle-form-block-use-sepa"> <tr class="crm-twingle-form-block-use-sepa">
<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 class="label">
{$form.twingle_use_sepa.label}
{help id="id-twingle_use_sepa" title=$form.twingle_use_sepa.label}
</td>
<td> <td>
{$form.twingle_use_sepa.html} {$form.twingle_use_sepa.html}
<br />
<span class="description">
{$formElements.twingle_use_sepa.description}
</span>
</td> </td>
</tr> </tr>
<tr class="crm-twingle-form-block-use-sepa-reference"> <tr class="crm-twingle-form-block-use-sepa-reference">
<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 class="label">
{$form.twingle_dont_use_reference.label}
{help id="id-twingle_dont_use_reference" title=$form.twingle_dont_use_reference.label}
</td>
<td> <td>
{$form.twingle_dont_use_reference.html} {$form.twingle_dont_use_reference.html}
<br />
<span class="description">
{$formElements.twingle_dont_use_reference.description}
</span>
</td> </td>
</tr> </tr>
<tr class="crm-twingle-form-block-prefix"> <tr class="crm-twingle-form-block-prefix">
<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 class="label">{$form.twingle_prefix.label}
{help id="id-twingle_prefix" title=$form.twingle_prefix.label}
</td>
<td> <td>
{$form.twingle_prefix.html} {$form.twingle_prefix.html}
<br />
<span class="description">
{$formElements.twingle_prefix.description}
</span>
</td> </td>
</tr> </tr>
<tr class="crm-twingle-form-block-recurring-protection"> <tr class="crm-twingle-form-block-recurring-protection">
<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 class="label">{$form.twingle_protect_recurring.label}
{help id="id-twingle_protect_recurring" title=$form.twingle_protect_recurring.label}
</td>
<td> <td>
{$form.twingle_protect_recurring.html} {$form.twingle_protect_recurring.html}
<br />
<span class="description">
{$formElements.protect_recurring.description}
</span>
</td> </td>
</tr> </tr>
@ -66,10 +59,6 @@
<td class="label">{$form.twingle_protect_recurring_activity_type.label}</td> <td class="label">{$form.twingle_protect_recurring_activity_type.label}</td>
<td> <td>
{$form.twingle_protect_recurring_activity_type.html} {$form.twingle_protect_recurring_activity_type.html}
<br />
<span class="description">
{$formElements.twingle_protect_recurring_activity_type.description}
</span>
</td> </td>
</tr> </tr>
@ -77,10 +66,6 @@
<td class="label">{$form.twingle_protect_recurring_activity_subject.label}</td> <td class="label">{$form.twingle_protect_recurring_activity_subject.label}</td>
<td> <td>
{$form.twingle_protect_recurring_activity_subject.html} {$form.twingle_protect_recurring_activity_subject.html}
<br />
<span class="description">
{$formElements.twingle_protect_recurring_activity_subject.description}
</span>
</td> </td>
</tr> </tr>
@ -88,10 +73,6 @@
<td class="label">{$form.twingle_protect_recurring_activity_status.label}</td> <td class="label">{$form.twingle_protect_recurring_activity_status.label}</td>
<td> <td>
{$form.twingle_protect_recurring_activity_status.html} {$form.twingle_protect_recurring_activity_status.html}
<br />
<span class="description">
{$formElements.twingle_protect_recurring_activity_status.description}
</span>
</td> </td>
</tr> </tr>
@ -99,15 +80,27 @@
<td class="label">{$form.twingle_protect_recurring_activity_assignee.label}</td> <td class="label">{$form.twingle_protect_recurring_activity_assignee.label}</td>
<td> <td>
{$form.twingle_protect_recurring_activity_assignee.html} {$form.twingle_protect_recurring_activity_assignee.html}
<br />
<span class="description">
{$formElements.twingle_protect_recurring_activity_assignee.description}
</span>
</td> </td>
</tr> </tr>
</table> </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"> <div class="crm-submit-buttons">
{include file="CRM/common/formButtons.tpl" location="bottom"} {include file="CRM/common/formButtons.tpl" location="bottom"}
@ -116,22 +109,23 @@
</div> </div>
{literal} {literal}
<script> <script>
/** /**
* Will show/hide the twingle_protect_recurring_activity_* fields based on * Will show/hide the twingle_protect_recurring_activity_* fields based on
* whether activity creation is selected * whether activity creation is selected
*/ */
function twingle_protect_recurring_change() { function twingle_protect_recurring_change() {
if (cj("#twingle_protect_recurring").val() == '2') { if (cj('#twingle_protect_recurring').val() == '2') {
cj("tr.crm-twingle-form-block-recurring-protection-activity").show(); cj('tr.crm-twingle-form-block-recurring-protection-activity').show();
} else { }
cj("tr.crm-twingle-form-block-recurring-protection-activity").hide(); else {
cj('tr.crm-twingle-form-block-recurring-protection-activity').hide();
}
} }
}
cj(document).ready(function () { cj(document).ready(function () {
cj("#twingle_protect_recurring").change(twingle_protect_recurring_change); cj('#twingle_protect_recurring').change(twingle_protect_recurring_change);
twingle_protect_recurring_change(); twingle_protect_recurring_change();
}); });
</script> </script>
{/literal} {/literal}

View file

@ -26,6 +26,9 @@
<tr> <tr>
<th>{ts domain="de.systopia.twingle"}Profile name{/ts}</th> <th>{ts domain="de.systopia.twingle"}Profile name{/ts}</th>
<th>{ts domain="de.systopia.twingle"}Selectors{/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"}Used{/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"}Last Used{/ts}</th>
<th>{ts domain="de.systopia.twingle"}Operations{/ts}</th> <th>{ts domain="de.systopia.twingle"}Operations{/ts}</th>
@ -46,6 +49,9 @@
</ul> </ul>
{/if} {/if}
</td> </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.access_counter_txt}{/ts}</td>
<td>{ts domain="de.systopia.twingle"}{$profile_stats.$profile_name.last_access_txt}{/ts}</td> <td>{ts domain="de.systopia.twingle"}{$profile_stats.$profile_name.last_access_txt}{/ts}</td>
<td> <td>
@ -56,7 +62,6 @@
{else} {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&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>
{/if} {/if}
</td> </td>
</tr> </tr>
{/foreach} {/foreach}

View file

@ -0,0 +1,54 @@
<?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

@ -0,0 +1,54 @@
<?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

@ -0,0 +1,54 @@
<?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

@ -0,0 +1,54 @@
<?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

@ -0,0 +1,54 @@
<?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

@ -0,0 +1,54 @@
<?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

@ -0,0 +1,54 @@
<?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

@ -0,0 +1,54 @@
<?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

@ -0,0 +1,65 @@
<?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

@ -5,11 +5,36 @@ use CRM_Twingle_ExtensionUtil as E;
/** /**
* Implements hook_civicrm_pre(). * 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) { function twingle_civicrm_pre($op, $objectName, $id, &$params) {
if ($objectName == 'ContributionRecur' && $op == 'edit') { if ($objectName == 'ContributionRecur' && $op == 'edit') {
CRM_Twingle_Tools::checkRecurringContributionChange((int) $id, $params); 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();
}
} }
/** /**

View file

@ -0,0 +1,10 @@
<?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

@ -0,0 +1,75 @@
<?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

@ -0,0 +1,10 @@
<?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

@ -0,0 +1,73 @@
<?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>