implement TwingleShop integration
This commit is contained in:
parent
ea46e6a747
commit
8cfa270dff
60 changed files with 5200 additions and 106 deletions
|
@ -1,48 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace CRM\Twingle\Exceptions;
|
||||
|
||||
use CRM_Twingle_ExtensionUtil as E;
|
||||
|
||||
/**
|
||||
* A simple custom exception class that indicates a problem within a class
|
||||
* of the Twingle API extension.
|
||||
*/
|
||||
class BaseException extends \Exception {
|
||||
|
||||
/**
|
||||
* @var int|string
|
||||
*/
|
||||
protected $code;
|
||||
protected string $log_message;
|
||||
|
||||
/**
|
||||
* BaseException Constructor
|
||||
* @param string $message
|
||||
* Error message
|
||||
* @param string $error_code
|
||||
* A meaningful error code
|
||||
*/
|
||||
public function __construct(string $message = '', string $error_code = '') {
|
||||
parent::__construct($message, 1);
|
||||
$this->log_message = !empty($message) ? E::LONG_NAME . ': ' . $message : '';
|
||||
$this->code = $error_code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error message, but with the extension name prefixed.
|
||||
* @return string
|
||||
*/
|
||||
public function getLogMessage() {
|
||||
return $this->log_message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error code.
|
||||
* @return string
|
||||
*/
|
||||
public function getErrorCode() {
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace CRM\Twingle\Exceptions;
|
||||
|
||||
/**
|
||||
* A simple custom exception that indicates a problem within the
|
||||
* CRM_Twingle_Profile class
|
||||
*/
|
||||
class ProfileException extends BaseException {
|
||||
|
||||
public const ERROR_CODE_PROFILE_NOT_FOUND = 'profile_not_found';
|
||||
public const ERROR_CODE_DEFAULT_PROFILE_NOT_FOUND = 'default_profile_not_found';
|
||||
public const ERROR_CODE_COULD_NOT_SAVE_PROFILE = 'could_not_save_profile';
|
||||
public const ERROR_CODE_COULD_NOT_RESET_PROFILE = 'could_not_reset_profile';
|
||||
public const ERROR_CODE_COULD_NOT_DELETE_PROFILE = 'could_not_delete_profile';
|
||||
public const ERROR_CODE_UNKNOWN_PROFILE_ATTRIBUTE = 'unknown_profile_attribute';
|
||||
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace CRM\Twingle\Exceptions;
|
||||
|
||||
/**
|
||||
* A simple custom error indicating a problem with the validation of the
|
||||
* CRM_Twingle_Profile
|
||||
*/
|
||||
class ProfileValidationError extends BaseException {
|
||||
|
||||
private string $affected_field_name;
|
||||
public const ERROR_CODE_PROFILE_VALIDATION_FAILED = 'profile_validation_failed';
|
||||
public const ERROR_CODE_PROFILE_VALIDATION_WARNING = 'profile_validation_warning';
|
||||
|
||||
/**
|
||||
* ProfileValidationError Constructor
|
||||
* @param string $affected_field_name
|
||||
* The name of the profile field which caused the exception
|
||||
* @param string $message
|
||||
* Error message
|
||||
* @param string $error_code
|
||||
* A meaningful error code
|
||||
*/
|
||||
public function __construct(string $affected_field_name, string $message = '', string $error_code = '') {
|
||||
parent::__construct($message, $error_code);
|
||||
$this->affected_field_name = $affected_field_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the profile field that caused the exception.
|
||||
* @return string
|
||||
*/
|
||||
public function getAffectedFieldName() {
|
||||
return $this->affected_field_name;
|
||||
}
|
||||
|
||||
}
|
|
@ -251,6 +251,7 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
|
|||
|
||||
// Assign template variables.
|
||||
$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('is_default', $is_default);
|
||||
|
||||
|
@ -354,6 +355,10 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
|
|||
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();
|
||||
$this->assign('payment_instruments', $payment_instruments);
|
||||
foreach ($payment_instruments as $pi_name => $pi_label) {
|
||||
|
@ -523,6 +528,42 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
|
|||
['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([
|
||||
[
|
||||
'type' => 'submit',
|
||||
|
@ -566,9 +607,6 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
|
|||
CRM_Core_Session::setStatus($e->getMessage(), E::ts('Warning'));
|
||||
}
|
||||
}
|
||||
catch (ProfileValidationError $e) {
|
||||
$this->setElementError($e->getAffectedFieldName(), $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return parent::validate();
|
||||
|
@ -993,5 +1031,4 @@ class CRM_Twingle_Form_Profile extends CRM_Core_Form {
|
|||
}
|
||||
return static::$_campaigns;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -29,14 +29,16 @@ class CRM_Twingle_Form_Settings extends CRM_Core_Form {
|
|||
* List of all settings options.
|
||||
*/
|
||||
public static $SETTINGS_LIST = [
|
||||
'twingle_prefix',
|
||||
'twingle_use_sepa',
|
||||
'twingle_dont_use_reference',
|
||||
'twingle_protect_recurring',
|
||||
'twingle_protect_recurring_activity_type',
|
||||
'twingle_protect_recurring_activity_subject',
|
||||
'twingle_protect_recurring_activity_status',
|
||||
'twingle_protect_recurring_activity_assignee',
|
||||
'twingle_prefix',
|
||||
'twingle_use_sepa',
|
||||
'twingle_dont_use_reference',
|
||||
'twingle_protect_recurring',
|
||||
'twingle_protect_recurring_activity_type',
|
||||
'twingle_protect_recurring_activity_subject',
|
||||
'twingle_protect_recurring_activity_status',
|
||||
'twingle_protect_recurring_activity_assignee',
|
||||
'twingle_use_shop',
|
||||
'twingle_access_key',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -105,13 +107,25 @@ class CRM_Twingle_Form_Settings extends CRM_Core_Form {
|
|||
]
|
||||
);
|
||||
|
||||
$this->addButtons([
|
||||
[
|
||||
'type' => 'submit',
|
||||
'name' => E::ts('Save'),
|
||||
'isDefault' => TRUE,
|
||||
],
|
||||
]);
|
||||
$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(array(
|
||||
array (
|
||||
'type' => 'submit',
|
||||
'name' => E::ts('Save'),
|
||||
'isDefault' => TRUE,
|
||||
)
|
||||
));
|
||||
|
||||
// set defaults
|
||||
foreach (self::$SETTINGS_LIST as $setting) {
|
||||
|
@ -124,8 +138,7 @@ class CRM_Twingle_Form_Settings extends CRM_Core_Form {
|
|||
}
|
||||
|
||||
/**
|
||||
* Custom form validation, because the activity creation fields
|
||||
* are only mandatory if activity creation is active
|
||||
* Custom form validation, as some fields are mandatory only when others are active.
|
||||
* @return bool
|
||||
*/
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ class CRM_Twingle_Page_Profiles extends CRM_Core_Page {
|
|||
}
|
||||
$this->assign('profiles', $profiles);
|
||||
$this->assign('profile_stats', CRM_Twingle_Profile::getProfileStats());
|
||||
$this->assign('twingle_use_shop', (int) Civi::settings()->get('twingle_use_shop'));
|
||||
|
||||
// Add custom css
|
||||
Civi::resources()->addStyleFile(E::LONG_NAME, 'css/twingle.css');
|
||||
|
|
|
@ -43,6 +43,16 @@ class CRM_Twingle_Profile {
|
|||
*/
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* @var array $check_box_fields
|
||||
* List of check box fields
|
||||
*/
|
||||
public $check_box_fields = [
|
||||
'newsletter_double_opt_in',
|
||||
'enable_shop_integration',
|
||||
'shop_map_products',
|
||||
];
|
||||
|
||||
/**
|
||||
* CRM_Twingle_Profile constructor.
|
||||
*
|
||||
|
@ -197,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.
|
||||
*
|
||||
|
@ -532,6 +552,10 @@ class CRM_Twingle_Profile {
|
|||
'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.
|
||||
array_combine(
|
||||
|
@ -650,6 +674,10 @@ class CRM_Twingle_Profile {
|
|||
],
|
||||
'map_as_contribution_notes' => [],
|
||||
'map_as_contact_notes' => [],
|
||||
'enable_shop_integration' => FALSE,
|
||||
'shop_financial_type' => 1,
|
||||
'shop_donation_financial_type' => 1,
|
||||
'shop_map_products' => FALSE,
|
||||
]
|
||||
// Add contribution status for all payment methods.
|
||||
// phpcs:ignore Drupal.Formatting.SpaceUnaryOperator.PlusMinus
|
||||
|
@ -666,7 +694,7 @@ class CRM_Twingle_Profile {
|
|||
* @param string $project_id
|
||||
*
|
||||
* @return CRM_Twingle_Profile
|
||||
* @throws \CRM\Twingle\Exceptions\ProfileException
|
||||
* @throws \Civi\Twingle\Exceptions\ProfileException
|
||||
* @throws \Civi\Core\Exception\DBQueryException
|
||||
*/
|
||||
public static function getProfileForProject($project_id) {
|
||||
|
@ -683,7 +711,6 @@ class CRM_Twingle_Profile {
|
|||
}
|
||||
|
||||
// If none matches, use the default profile.
|
||||
$default_profile = $profiles['default'];
|
||||
if (!empty($default_profile)) {
|
||||
return $default_profile;
|
||||
}
|
||||
|
@ -780,5 +807,4 @@ class CRM_Twingle_Profile {
|
|||
}
|
||||
return $stats;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ declare(strict_types = 1);
|
|||
|
||||
use CRM_Twingle_ExtensionUtil as E;
|
||||
use Civi\Twingle\Exceptions\BaseException;
|
||||
use Civi\Twingle\Shop\Exceptions\LineItemException;
|
||||
use Civi\Twingle\Shop\BAO\TwingleProduct;
|
||||
|
||||
class CRM_Twingle_Submission {
|
||||
|
||||
|
@ -41,7 +43,18 @@ class CRM_Twingle_Submission {
|
|||
public const EMPLOYER_RELATIONSHIP_TYPE_ID = 5;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> &$params
|
||||
* List of allowed product attributes.
|
||||
*/
|
||||
const ALLOWED_PRODUCT_ATTRIBUTES = [
|
||||
'name',
|
||||
'internal_id',
|
||||
'price',
|
||||
'count',
|
||||
'total_value',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param array &$params
|
||||
* A reference to the parameters array of the submission.
|
||||
*
|
||||
* @param \CRM_Twingle_Profile $profile
|
||||
|
@ -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.
|
||||
if (isset($params['campaign_id'])) {
|
||||
// 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 = 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 differs from the submission
|
||||
if ($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 '$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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -137,4 +137,15 @@ class CRM_Twingle_Upgrader extends CRM_Extension_Upgrader_Base {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue