Compare commits
378 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b8f44d962d | ||
![]() |
21f29ce169 | ||
![]() |
c8a577b651 | ||
![]() |
2ee06faf34 | ||
![]() |
b9b26d9524 | ||
![]() |
c7c766d926 | ||
![]() |
82952a0162 | ||
![]() |
eaf9d53169 | ||
![]() |
e26b5c3933 | ||
![]() |
82456d2ae4 | ||
![]() |
9c9fed20d7 | ||
![]() |
30c34f72be | ||
![]() |
355a377c4f | ||
![]() |
612224901a | ||
![]() |
fa301676e3 | ||
![]() |
2834f8028d | ||
![]() |
f6cd3614c3 | ||
![]() |
4ae20a1b04 | ||
![]() |
64d6b48813 | ||
![]() |
7cca29b458 | ||
![]() |
8de34f7b2a | ||
![]() |
26132e785e | ||
![]() |
33cb076d42 | ||
![]() |
d3ccb3b092 | ||
![]() |
477c57ca53 | ||
![]() |
ac892c9afc | ||
![]() |
1fc6529064 | ||
![]() |
8cfa270dff | ||
![]() |
ea46e6a747 | ||
![]() |
1a5f77c090 | ||
![]() |
eacc9cf496 | ||
![]() |
7c7c040b30 | ||
![]() |
0f947e4277 | ||
![]() |
db94f26d6d | ||
![]() |
c971b6f8eb | ||
![]() |
72bfa3fb2c | ||
![]() |
6606d09dce | ||
![]() |
a363e1c888 | ||
![]() |
9ff9234644 | ||
![]() |
07435ad997 | ||
![]() |
8d30b2a52a | ||
![]() |
96d0e5fbec | ||
![]() |
f7b15ac4f6 | ||
![]() |
a8c8401be3 | ||
![]() |
61f45034c6 | ||
![]() |
ff24256bc1 | ||
![]() |
c0af2e16ab | ||
![]() |
67283fa1a7 | ||
![]() |
90f27f70c7 | ||
![]() |
221f9c72f3 | ||
![]() |
1dbc842253 | ||
![]() |
0b9c1709fd | ||
![]() |
0cf7ccb1cb | ||
![]() |
cd008d9545 | ||
![]() |
4432060d05 | ||
![]() |
ab5d0906d7 | ||
![]() |
d9d68fa937 | ||
![]() |
03a37aed31 | ||
![]() |
933c51c48e | ||
![]() |
ac1b08b775 | ||
![]() |
cf9483ca3e | ||
![]() |
f5ab576ed9 | ||
![]() |
0caf9bf98e | ||
![]() |
bea59b4365 | ||
![]() |
089bbdf934 | ||
![]() |
1ddcc217e3 | ||
![]() |
87ca179791 | ||
![]() |
547254158c | ||
![]() |
758b793c0d | ||
![]() |
9836168122 | ||
![]() |
aee56769b7 | ||
![]() |
8020491bf1 | ||
![]() |
f5723a4e7d | ||
![]() |
5169e5a0ce | ||
![]() |
d1f3dd871c | ||
![]() |
75676d42eb | ||
![]() |
a0b2879b69 | ||
![]() |
c7f1b7cb6e | ||
![]() |
2b8ab813db | ||
![]() |
63b7e4d3ee | ||
![]() |
b3f82fbfba | ||
![]() |
df51d59cea | ||
![]() |
10f6ca4e89 | ||
![]() |
48c49c1814 | ||
![]() |
02bac833de | ||
![]() |
670984854f | ||
![]() |
3612122650 | ||
![]() |
6f555c660e | ||
![]() |
313d2f648f | ||
![]() |
a91fbd0c20 | ||
![]() |
692c66a9a5 | ||
![]() |
88850bbce3 | ||
![]() |
b480d87ed7 | ||
![]() |
4f845ca07e | ||
![]() |
6114772d07 | ||
![]() |
8d1d93d77a | ||
![]() |
96c072eb8e | ||
![]() |
aa2c938abe | ||
![]() |
8bcdff4a85 | ||
![]() |
fc40af8db5 | ||
![]() |
d7b066751a | ||
![]() |
6313fffa44 | ||
![]() |
47756f68b4 | ||
![]() |
bd54c039c8 | ||
![]() |
5efed1f6c8 | ||
![]() |
f81b476a30 | ||
![]() |
aef1ae7396 | ||
![]() |
4feeb01611 | ||
![]() |
09dda832a4 | ||
![]() |
c7e51b4d3c | ||
![]() |
6f6b9e0599 | ||
![]() |
a16be91822 | ||
![]() |
d9e51c67f6 | ||
![]() |
1d9e973e46 | ||
![]() |
fe0ce3f57d | ||
![]() |
53745a3e07 | ||
![]() |
4e072b416a | ||
![]() |
b0d5bdefa5 | ||
![]() |
1875861735 | ||
![]() |
e83a898cb8 | ||
![]() |
9baf2c0e2a | ||
![]() |
27675b7219 | ||
![]() |
43be624bf6 | ||
![]() |
7a751e92bf | ||
![]() |
f42bc9b7ed | ||
![]() |
322c2d0dd3 | ||
![]() |
69843bc981 | ||
![]() |
fad228315d | ||
![]() |
8cd928caa9 | ||
![]() |
b4c6581d4f | ||
![]() |
fb25af415c | ||
![]() |
e7040c70d3 | ||
![]() |
30425418e7 | ||
![]() |
75d9516da0 | ||
![]() |
c00314c75d | ||
![]() |
df608dc3d0 | ||
![]() |
c149275e15 | ||
![]() |
dc1118dac9 | ||
![]() |
8daddce005 | ||
![]() |
9ccd86f03a | ||
![]() |
e39a91e477 | ||
![]() |
89df7482a6 | ||
![]() |
b7b0e6d610 | ||
![]() |
ab5b0b3929 | ||
![]() |
fd99f3b24f | ||
![]() |
3241583542 | ||
![]() |
e442ca6249 | ||
![]() |
a868e87ba7 | ||
![]() |
c3f4db8600 | ||
![]() |
0b2b8d6523 | ||
![]() |
3644086ab3 | ||
![]() |
5a9a911c01 | ||
![]() |
225c4efd25 | ||
![]() |
63d713f9f0 | ||
![]() |
ab27dccbe7 | ||
![]() |
bd6c60c539 | ||
![]() |
518f8809c7 | ||
![]() |
aae3a6b6f1 | ||
![]() |
f4dfc4b937 | ||
![]() |
018d2f2ac3 | ||
![]() |
b9f1e32100 | ||
![]() |
bae3461c19 | ||
![]() |
486da1818d | ||
![]() |
c62af9582e | ||
![]() |
c88be90532 | ||
![]() |
b581b37838 | ||
![]() |
945992d8be | ||
![]() |
3b643b2e54 | ||
![]() |
b53befbb41 | ||
![]() |
8cd714362d | ||
![]() |
4d750b1ab6 | ||
![]() |
30ad463549 | ||
![]() |
f5c2127805 | ||
![]() |
6132e7ad80 | ||
![]() |
92d15e7c7c | ||
![]() |
1b68462401 | ||
![]() |
40842ae325 | ||
![]() |
a326a61ade | ||
![]() |
d3c07c10fa | ||
![]() |
29e0b50cd1 | ||
![]() |
c1a1dce52e | ||
![]() |
85081dc3ab | ||
![]() |
4be5471d41 | ||
![]() |
aa0c35ca1b | ||
![]() |
c52dc77532 | ||
![]() |
c96abd8407 | ||
![]() |
65424ab4bc | ||
![]() |
b5b34ff678 | ||
![]() |
bf2a73f519 | ||
![]() |
fd47b91b65 | ||
![]() |
62399657e7 | ||
![]() |
6fbc1d4a5d | ||
![]() |
6036f62a22 | ||
![]() |
3654f92bf8 | ||
![]() |
1c649ba812 | ||
![]() |
c81e10faaa | ||
![]() |
d95813d1ad | ||
![]() |
6779349505 | ||
![]() |
3656b413a1 | ||
![]() |
36958cb9ed | ||
![]() |
dbac84d13a | ||
![]() |
48164479f7 | ||
![]() |
d910ce4846 | ||
![]() |
9969c99f70 | ||
![]() |
89638a172d | ||
![]() |
a30b8f02cb | ||
![]() |
197563920a | ||
![]() |
9751e8cee4 | ||
![]() |
cadf895def | ||
![]() |
e01b1ef8f3 | ||
![]() |
c889cf08e8 | ||
![]() |
c5f3b1081b | ||
![]() |
0ce2d2530b | ||
![]() |
4659b53522 | ||
![]() |
287bb52e90 | ||
![]() |
549916a0bc | ||
![]() |
c06ba098c1 | ||
![]() |
921ea49deb | ||
![]() |
d3060c291e | ||
![]() |
be55ad70a8 | ||
![]() |
677f9b8380 | ||
![]() |
228e964633 | ||
![]() |
69764ffc11 | ||
![]() |
7328b3893d | ||
![]() |
3139ec0fee | ||
![]() |
72a3515e8a | ||
![]() |
9d0ac14d35 | ||
![]() |
a65e98509a | ||
![]() |
d7e42035c8 | ||
![]() |
d140bd9ed7 | ||
![]() |
021cd5257b | ||
![]() |
6b42c72bf8 | ||
![]() |
3553ed83b9 | ||
![]() |
94cc262c21 | ||
![]() |
11558d4fbd | ||
![]() |
d3c1aabfb4 | ||
![]() |
4ff060884a | ||
![]() |
a81b2d4f6f | ||
![]() |
e588ebe2d2 | ||
![]() |
f0215c98c7 | ||
![]() |
735eadd716 | ||
![]() |
f8c7ebfbd6 | ||
![]() |
57b592c8f7 | ||
![]() |
ccc8f2210d | ||
![]() |
bfcd98e973 | ||
![]() |
53c290fabf | ||
![]() |
675b682e0e | ||
![]() |
22c9631ec6 | ||
![]() |
66c469f87d | ||
![]() |
da790863b5 | ||
![]() |
4ec0fef825 | ||
![]() |
25a040f6d8 | ||
![]() |
53555b242c | ||
![]() |
e763ee8a16 | ||
![]() |
fe34d09714 | ||
![]() |
0d46884069 | ||
![]() |
36a756e4dd | ||
![]() |
00c55b261f | ||
![]() |
1d2c66b375 | ||
![]() |
b256b59fe5 | ||
![]() |
acedf890f5 | ||
![]() |
35cf2f80e2 | ||
![]() |
a6468bfaa0 | ||
![]() |
a8fef68799 | ||
![]() |
29c3b04754 | ||
![]() |
b269868c66 | ||
![]() |
3fbe88a3cf | ||
![]() |
84fe5137c1 | ||
![]() |
30a0b81c8b | ||
![]() |
db9c53bd10 | ||
![]() |
ad8fb0eee7 | ||
![]() |
0e8534619e | ||
![]() |
94df842477 | ||
![]() |
cc048fdd3c | ||
![]() |
9b5165f5a6 | ||
![]() |
ceb1f5a753 | ||
![]() |
944c3a70c5 | ||
![]() |
016dfc2a13 | ||
![]() |
e57e50f496 | ||
![]() |
20ab34e338 | ||
![]() |
e3718996b2 | ||
![]() |
5f4bd857d8 | ||
![]() |
eeadc0a894 | ||
![]() |
bee7d10dc7 | ||
![]() |
d4a0120843 | ||
![]() |
dfaf46b8f4 | ||
![]() |
3463da4514 | ||
![]() |
d6350e313a | ||
![]() |
6deffe5f72 | ||
![]() |
c4530e7e24 | ||
![]() |
33ced7bbc3 | ||
![]() |
8a2f91f34a | ||
![]() |
a8734bd1b1 | ||
![]() |
519f4e7625 | ||
![]() |
24801bb6bc | ||
![]() |
48116e5c98 | ||
![]() |
d1b3e2b865 | ||
![]() |
41fa7cc5fc | ||
![]() |
8642ab5cfd | ||
![]() |
8ae3f658c0 | ||
![]() |
3a45e1c1c6 | ||
![]() |
6db38e5706 | ||
![]() |
d6b90dcec2 | ||
![]() |
a2b50fd4d3 | ||
![]() |
f3e6b031b5 | ||
![]() |
082fd934d4 | ||
![]() |
31c87b8fa2 | ||
![]() |
07ec0aac03 | ||
![]() |
3e00a6a46b | ||
![]() |
bc9baea6f2 | ||
![]() |
cd7c7e7d12 | ||
![]() |
746b4304d1 | ||
![]() |
a5066ad780 | ||
![]() |
77a43c6caf | ||
![]() |
8e871fd51b | ||
![]() |
f746a3fe06 | ||
![]() |
a46e6eb7d1 | ||
![]() |
08e9418766 | ||
![]() |
686096f4b3 | ||
![]() |
3d5d6f0d97 | ||
![]() |
dfe433d545 | ||
![]() |
f2b94b72e7 | ||
![]() |
4151963a5c | ||
![]() |
e8bb3a42cc | ||
![]() |
24d93ed1f1 | ||
![]() |
383e392ff0 | ||
![]() |
edebdd234d | ||
![]() |
6da1f76df5 | ||
![]() |
ee05e44995 | ||
![]() |
c9695c1773 | ||
![]() |
c6cb61905a | ||
![]() |
bcadae9a87 | ||
![]() |
cfe498bbbc | ||
![]() |
fc7c7d7d7b | ||
![]() |
30ab55f3ba | ||
![]() |
98ff2f4550 | ||
![]() |
d9a44f07e7 | ||
![]() |
0dff79e86d | ||
![]() |
5ab5b41758 | ||
![]() |
33c81951fc | ||
![]() |
d12094aa52 | ||
![]() |
56f7b07755 | ||
![]() |
b8e39bd903 | ||
![]() |
5e4719d589 | ||
![]() |
113da1c2dd | ||
![]() |
7ff6381ad8 | ||
![]() |
17589ef616 | ||
![]() |
007dd179ba | ||
![]() |
bd35f1dbe6 | ||
![]() |
1f39de4091 | ||
![]() |
b6e8a9b82d | ||
![]() |
a7bcd118cc | ||
![]() |
aea1db611d | ||
![]() |
c43ebf452f | ||
![]() |
2413b09f3b | ||
![]() |
9ffbd5b070 | ||
![]() |
341a0f0e32 | ||
![]() |
b85291527d | ||
![]() |
73481fb7ed | ||
![]() |
4537ac540a | ||
![]() |
876a2c5cee | ||
![]() |
0faf3c054c | ||
![]() |
82cff43c29 | ||
![]() |
ad09cb7339 | ||
![]() |
5ab4f7d728 | ||
![]() |
7fec0d32e9 | ||
![]() |
cd6b5ad458 | ||
![]() |
61182469c0 | ||
![]() |
b3f2f78146 | ||
![]() |
49056d8192 | ||
![]() |
e2f4e51f94 | ||
![]() |
264da53033 | ||
![]() |
3df3e00ff9 | ||
![]() |
3c18343722 | ||
![]() |
abe78b4883 | ||
![]() |
3fec603eae | ||
![]() |
4d0bcfec55 | ||
![]() |
59ddafa417 |
240
.editorconfig
Normal file
|
@ -0,0 +1,240 @@
|
|||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
max_line_length = 120
|
||||
tab_width = 2
|
||||
ij_continuation_indent_size = 2
|
||||
ij_visual_guides = 80,120
|
||||
|
||||
[{*.php}]
|
||||
ij_php_align_assignments = false
|
||||
ij_php_align_class_constants = false
|
||||
ij_php_align_enum_cases = false
|
||||
ij_php_align_group_field_declarations = false
|
||||
ij_php_align_inline_comments = false
|
||||
ij_php_align_key_value_pairs = false
|
||||
ij_php_align_match_arm_bodies = false
|
||||
ij_php_align_multiline_array_initializer_expression = false
|
||||
ij_php_align_multiline_binary_operation = false
|
||||
ij_php_align_multiline_chained_methods = false
|
||||
ij_php_align_multiline_extends_list = false
|
||||
ij_php_align_multiline_for = true
|
||||
ij_php_align_multiline_parameters = false
|
||||
ij_php_align_multiline_parameters_in_calls = false
|
||||
ij_php_align_multiline_ternary_operation = false
|
||||
ij_php_align_named_arguments = false
|
||||
ij_php_align_phpdoc_comments = false
|
||||
ij_php_align_phpdoc_param_names = false
|
||||
ij_php_anonymous_brace_style = end_of_line
|
||||
ij_php_api_weight = 28
|
||||
ij_php_array_initializer_new_line_after_left_brace = true
|
||||
ij_php_array_initializer_right_brace_on_new_line = true
|
||||
ij_php_array_initializer_wrap = on_every_item
|
||||
ij_php_assignment_wrap = normal
|
||||
ij_php_attributes_wrap = normal
|
||||
ij_php_author_weight = 28
|
||||
ij_php_binary_operation_sign_on_next_line = false
|
||||
ij_php_binary_operation_wrap = normal
|
||||
ij_php_blank_lines_after_class_header = 1
|
||||
ij_php_blank_lines_after_function = 1
|
||||
ij_php_blank_lines_after_imports = 1
|
||||
ij_php_blank_lines_after_opening_tag = 0
|
||||
ij_php_blank_lines_after_package = 1
|
||||
ij_php_blank_lines_around_class = 1
|
||||
ij_php_blank_lines_around_constants = 1
|
||||
ij_php_blank_lines_around_enum_cases = 0
|
||||
ij_php_blank_lines_around_field = 1
|
||||
ij_php_blank_lines_around_method = 1
|
||||
ij_php_blank_lines_before_class_end = 1
|
||||
ij_php_blank_lines_before_imports = 1
|
||||
ij_php_blank_lines_before_method_body = 0
|
||||
ij_php_blank_lines_before_package = 1
|
||||
ij_php_blank_lines_before_return_statement = 1
|
||||
ij_php_blank_lines_between_imports = 0
|
||||
ij_php_block_brace_style = end_of_line
|
||||
ij_php_call_parameters_new_line_after_left_paren = true
|
||||
ij_php_call_parameters_right_paren_on_new_line = true
|
||||
ij_php_call_parameters_wrap = on_every_item
|
||||
ij_php_catch_on_new_line = true
|
||||
ij_php_category_weight = 28
|
||||
ij_php_class_brace_style = end_of_line
|
||||
ij_php_comma_after_last_argument = false
|
||||
ij_php_comma_after_last_array_element = true
|
||||
ij_php_comma_after_last_closure_use_var = false
|
||||
ij_php_comma_after_last_match_arm = false
|
||||
ij_php_comma_after_last_parameter = false
|
||||
ij_php_concat_spaces = true
|
||||
ij_php_copyright_weight = 28
|
||||
ij_php_deprecated_weight = 4
|
||||
ij_php_do_while_brace_force = always
|
||||
ij_php_else_if_style = as_is
|
||||
ij_php_else_on_new_line = true
|
||||
ij_php_example_weight = 28
|
||||
ij_php_extends_keyword_wrap = off
|
||||
ij_php_extends_list_wrap = off
|
||||
ij_php_fields_default_visibility = private
|
||||
ij_php_filesource_weight = 28
|
||||
ij_php_finally_on_new_line = true
|
||||
ij_php_for_brace_force = always
|
||||
ij_php_for_statement_new_line_after_left_paren = false
|
||||
ij_php_for_statement_right_paren_on_new_line = false
|
||||
ij_php_for_statement_wrap = off
|
||||
ij_php_force_empty_methods_in_one_line = false
|
||||
ij_php_force_short_declaration_array_style = true
|
||||
ij_php_getters_setters_naming_style = camel_case
|
||||
ij_php_getters_setters_order_style = getters_first
|
||||
ij_php_global_weight = 28
|
||||
ij_php_group_use_wrap = on_every_item
|
||||
ij_php_if_brace_force = always
|
||||
ij_php_if_lparen_on_next_line = false
|
||||
ij_php_if_rparen_on_next_line = false
|
||||
ij_php_ignore_weight = 28
|
||||
ij_php_import_sorting = alphabetic
|
||||
ij_php_indent_break_from_case = true
|
||||
ij_php_indent_case_from_switch = true
|
||||
ij_php_indent_code_in_php_tags = false
|
||||
ij_php_internal_weight = 28
|
||||
ij_php_keep_blank_lines_after_lbrace = 1
|
||||
ij_php_keep_blank_lines_before_right_brace = 1
|
||||
ij_php_keep_blank_lines_in_code = 1
|
||||
ij_php_keep_blank_lines_in_declarations = 1
|
||||
ij_php_keep_control_statement_in_one_line = false
|
||||
ij_php_keep_first_column_comment = false
|
||||
ij_php_keep_indents_on_empty_lines = false
|
||||
ij_php_keep_line_breaks = false
|
||||
ij_php_keep_rparen_and_lbrace_on_one_line = true
|
||||
ij_php_keep_simple_classes_in_one_line = false
|
||||
ij_php_keep_simple_methods_in_one_line = false
|
||||
ij_php_lambda_brace_style = end_of_line
|
||||
ij_php_license_weight = 28
|
||||
ij_php_line_comment_add_space = false
|
||||
ij_php_line_comment_at_first_column = true
|
||||
ij_php_link_weight = 28
|
||||
ij_php_lower_case_boolean_const = false
|
||||
ij_php_lower_case_keywords = true
|
||||
ij_php_lower_case_null_const = false
|
||||
ij_php_method_brace_style = end_of_line
|
||||
ij_php_method_call_chain_wrap = on_every_item
|
||||
ij_php_method_parameters_new_line_after_left_paren = true
|
||||
ij_php_method_parameters_right_paren_on_new_line = true
|
||||
ij_php_method_parameters_wrap = on_every_item
|
||||
ij_php_method_weight = 28
|
||||
ij_php_modifier_list_wrap = false
|
||||
ij_php_multiline_chained_calls_semicolon_on_new_line = true
|
||||
ij_php_namespace_brace_style = 1
|
||||
ij_php_new_line_after_php_opening_tag = true
|
||||
ij_php_null_type_position = in_the_end
|
||||
ij_php_package_weight = 28
|
||||
ij_php_param_weight = 1
|
||||
ij_php_parameters_attributes_wrap = normal
|
||||
ij_php_parentheses_expression_new_line_after_left_paren = false
|
||||
ij_php_parentheses_expression_right_paren_on_new_line = false
|
||||
ij_php_phpdoc_blank_line_before_tags = true
|
||||
ij_php_phpdoc_blank_lines_around_parameters = true
|
||||
ij_php_phpdoc_keep_blank_lines = true
|
||||
ij_php_phpdoc_param_spaces_between_name_and_description = 1
|
||||
ij_php_phpdoc_param_spaces_between_tag_and_type = 1
|
||||
ij_php_phpdoc_param_spaces_between_type_and_name = 1
|
||||
ij_php_phpdoc_use_fqcn = true
|
||||
ij_php_phpdoc_wrap_long_lines = true
|
||||
ij_php_place_assignment_sign_on_next_line = false
|
||||
ij_php_place_parens_for_constructor = 1
|
||||
ij_php_property_read_weight = 28
|
||||
ij_php_property_weight = 28
|
||||
ij_php_property_write_weight = 28
|
||||
ij_php_return_type_on_new_line = false
|
||||
ij_php_return_weight = 2
|
||||
ij_php_see_weight = 5
|
||||
ij_php_since_weight = 28
|
||||
ij_php_sort_phpdoc_elements = true
|
||||
ij_php_space_after_colon = true
|
||||
ij_php_space_after_colon_in_enum_backed_type = true
|
||||
ij_php_space_after_colon_in_named_argument = true
|
||||
ij_php_space_after_colon_in_return_type = true
|
||||
ij_php_space_after_comma = true
|
||||
ij_php_space_after_for_semicolon = true
|
||||
ij_php_space_after_quest = true
|
||||
ij_php_space_after_type_cast = true
|
||||
ij_php_space_after_unary_not = false
|
||||
ij_php_space_before_array_initializer_left_brace = false
|
||||
ij_php_space_before_catch_keyword = true
|
||||
ij_php_space_before_catch_left_brace = true
|
||||
ij_php_space_before_catch_parentheses = true
|
||||
ij_php_space_before_class_left_brace = true
|
||||
ij_php_space_before_closure_left_parenthesis = true
|
||||
ij_php_space_before_colon = true
|
||||
ij_php_space_before_colon_in_enum_backed_type = false
|
||||
ij_php_space_before_colon_in_named_argument = false
|
||||
ij_php_space_before_colon_in_return_type = false
|
||||
ij_php_space_before_comma = false
|
||||
ij_php_space_before_do_left_brace = true
|
||||
ij_php_space_before_else_keyword = true
|
||||
ij_php_space_before_else_left_brace = true
|
||||
ij_php_space_before_finally_keyword = true
|
||||
ij_php_space_before_finally_left_brace = true
|
||||
ij_php_space_before_for_left_brace = true
|
||||
ij_php_space_before_for_parentheses = true
|
||||
ij_php_space_before_for_semicolon = false
|
||||
ij_php_space_before_if_left_brace = true
|
||||
ij_php_space_before_if_parentheses = true
|
||||
ij_php_space_before_method_call_parentheses = false
|
||||
ij_php_space_before_method_left_brace = true
|
||||
ij_php_space_before_method_parentheses = false
|
||||
ij_php_space_before_quest = true
|
||||
ij_php_space_before_short_closure_left_parenthesis = false
|
||||
ij_php_space_before_switch_left_brace = true
|
||||
ij_php_space_before_switch_parentheses = true
|
||||
ij_php_space_before_try_left_brace = true
|
||||
ij_php_space_before_unary_not = false
|
||||
ij_php_space_before_while_keyword = true
|
||||
ij_php_space_before_while_left_brace = true
|
||||
ij_php_space_before_while_parentheses = true
|
||||
ij_php_space_between_ternary_quest_and_colon = false
|
||||
ij_php_spaces_around_additive_operators = true
|
||||
ij_php_spaces_around_arrow = false
|
||||
ij_php_spaces_around_assignment_in_declare = true
|
||||
ij_php_spaces_around_assignment_operators = true
|
||||
ij_php_spaces_around_bitwise_operators = true
|
||||
ij_php_spaces_around_equality_operators = true
|
||||
ij_php_spaces_around_logical_operators = true
|
||||
ij_php_spaces_around_multiplicative_operators = true
|
||||
ij_php_spaces_around_null_coalesce_operator = true
|
||||
ij_php_spaces_around_pipe_in_union_type = false
|
||||
ij_php_spaces_around_relational_operators = true
|
||||
ij_php_spaces_around_shift_operators = true
|
||||
ij_php_spaces_around_unary_operator = false
|
||||
ij_php_spaces_around_var_within_brackets = false
|
||||
ij_php_spaces_within_array_initializer_braces = false
|
||||
ij_php_spaces_within_brackets = false
|
||||
ij_php_spaces_within_catch_parentheses = false
|
||||
ij_php_spaces_within_for_parentheses = false
|
||||
ij_php_spaces_within_if_parentheses = false
|
||||
ij_php_spaces_within_method_call_parentheses = false
|
||||
ij_php_spaces_within_method_parentheses = false
|
||||
ij_php_spaces_within_parentheses = false
|
||||
ij_php_spaces_within_short_echo_tags = true
|
||||
ij_php_spaces_within_switch_parentheses = false
|
||||
ij_php_spaces_within_while_parentheses = false
|
||||
ij_php_special_else_if_treatment = false
|
||||
ij_php_subpackage_weight = 28
|
||||
ij_php_ternary_operation_signs_on_next_line = true
|
||||
ij_php_ternary_operation_wrap = on_every_item
|
||||
ij_php_throws_weight = 3
|
||||
ij_php_todo_weight = 6
|
||||
ij_php_treat_multiline_arrays_and_lambdas_multiline = false
|
||||
ij_php_unknown_tag_weight = 28
|
||||
ij_php_upper_case_boolean_const = true
|
||||
ij_php_upper_case_null_const = true
|
||||
ij_php_uses_weight = 28
|
||||
ij_php_var_weight = 0
|
||||
ij_php_variable_naming_style = camel_case
|
||||
ij_php_version_weight = 28
|
||||
ij_php_while_brace_force = always
|
||||
ij_php_while_on_new_line = false
|
||||
|
||||
[{*.neon,*.neon.dist,*neon.template}]
|
||||
indent_style = tab
|
||||
tab_width = 4
|
42
.github/workflows/phpcs.yml
vendored
Normal file
|
@ -0,0 +1,42 @@
|
|||
name: PHP_CodeSniffer
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.php'
|
||||
- tools/phpcs/composer.json
|
||||
- phpcs.xml.dist
|
||||
|
||||
jobs:
|
||||
phpcs:
|
||||
runs-on: ubuntu-latest
|
||||
name: PHP_CodeSniffer
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.1
|
||||
coverage: none
|
||||
tools: cs2pr
|
||||
env:
|
||||
fail-fast: true
|
||||
|
||||
- name: Get composer cache directory
|
||||
id: composer-cache
|
||||
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('tools/phpcs/composer.json') }}
|
||||
restore-keys: ${{ runner.os }}-composer-
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer composer-phpcs -- update --no-progress --prefer-dist
|
||||
|
||||
- name: Run PHP_CodeSniffer
|
||||
run: composer phpcs -- -q --report=checkstyle | cs2pr
|
52
.github/workflows/phpstan.yml
vendored
Normal file
|
@ -0,0 +1,52 @@
|
|||
name: PHPStan
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.php'
|
||||
- composer.json
|
||||
- tools/phpstan/composer.json
|
||||
- ci/composer.json
|
||||
- phpstan.ci.neon
|
||||
- phpstan.neon.dist
|
||||
|
||||
jobs:
|
||||
phpstan:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
php-versions: ['7.4', '8.0', '8.1']
|
||||
prefer: ['prefer-stable', 'prefer-lowest']
|
||||
name: PHPStan with PHP ${{ matrix.php-versions }} ${{ matrix.prefer }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
coverage: none
|
||||
env:
|
||||
fail-fast: true
|
||||
|
||||
- name: Get composer cache directory
|
||||
id: composer-cache
|
||||
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ matrix.prefer }}-${{ hashFiles('**/composer.json') }}
|
||||
restore-keys: ${{ runner.os }}-composer-${{ matrix.prefer }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
composer update --no-progress --prefer-dist --${{ matrix.prefer }} &&
|
||||
composer composer-phpunit -- update --no-progress --prefer-dist &&
|
||||
composer composer-phpstan -- update --no-progress --prefer-dist --optimize-autoloader &&
|
||||
composer --working-dir=ci update --no-progress --prefer-dist --${{ matrix.prefer }} --optimize-autoloader --ignore-platform-req=ext-gd
|
||||
|
||||
- name: Run PHPStan
|
||||
run: composer phpstan -- analyse -c phpstan.ci.neon
|
37
.github/workflows/phpunit.yml
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
name: PHPUnit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.php'
|
||||
- composer.json
|
||||
- tools/phpunit/composer.json
|
||||
- phpunit.xml.dist
|
||||
- tests/docker-prepare.sh
|
||||
|
||||
env:
|
||||
# On github CI machine creating the "/vendor" volume fails otherwise with: read-only file system: unknown
|
||||
BIND_VOLUME_PERMISSIONS: rw
|
||||
|
||||
jobs:
|
||||
phpunit:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
civicrm-image-tags: [ '5-drupal-php8.1', '5-drupal-php7.4', '5.56-drupal-php7.4' ]
|
||||
name: PHPUnit with Docker image michaelmcandrew/civicrm:${{ matrix.civicrm-image-tags }}
|
||||
env:
|
||||
CIVICRM_IMAGE_TAG: ${{ matrix.civicrm-image-tags }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Pull images
|
||||
run: docker compose -f tests/docker-compose.yml pull --quiet
|
||||
- name: Start containers
|
||||
run: docker compose -f tests/docker-compose.yml up -d
|
||||
- name: Prepare environment
|
||||
run: docker compose -f tests/docker-compose.yml exec civicrm sites/default/files/civicrm/ext/de.systopia.twingle/tests/docker-prepare.sh
|
||||
- name: Run PHPUnit
|
||||
run: docker compose -f tests/docker-compose.yml exec civicrm sites/default/files/civicrm/ext/de.systopia.twingle/tests/docker-phpunit.sh
|
||||
- name: Remove containers
|
||||
run: docker compose -f tests/docker-compose.yml down -v
|
10
.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
/.phpcs.cache
|
||||
/.phpunit.result.cache
|
||||
/.phpstan/
|
||||
/ci/composer.lock
|
||||
/ci/vendor/
|
||||
/composer.lock
|
||||
/phpstan.neon
|
||||
/tools/*/vendor/
|
||||
/tools/*/composer.lock
|
||||
/vendor/
|
676
CRM/Twingle/BAO/TwingleProduct.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
475
CRM/Twingle/BAO/TwingleShop.php
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
40
CRM/Twingle/Config.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
/*------------------------------------------------------------+
|
||||
| SYSTOPIA Twingle Integration |
|
||||
| Copyright (C) 2020 SYSTOPIA |
|
||||
| Author: B. Endres (endres@systopia.de) |
|
||||
+-------------------------------------------------------------+
|
||||
| This program is released as free software under the |
|
||||
| Affero GPL license. You can redistribute it and/or |
|
||||
| modify it under the terms of this license which you |
|
||||
| can read by viewing the included agpl.txt or online |
|
||||
| at www.gnu.org/licenses/agpl.html. Removal of this |
|
||||
| copyright header is strictly prohibited without |
|
||||
| written permission from the original author(s). |
|
||||
+-------------------------------------------------------------*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
use CRM_Twingle_ExtensionUtil as E;
|
||||
|
||||
class CRM_Twingle_Config {
|
||||
|
||||
public const RCUR_PROTECTION_OFF = 0;
|
||||
public const RCUR_PROTECTION_EXCEPTION = 1;
|
||||
public const RCUR_PROTECTION_ACTIVITY = 2;
|
||||
|
||||
/**
|
||||
* Get the options for protecting a recurring contribution linked Twingle
|
||||
* against ending or cancellation (because Twingle would keep on collecting them)
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function getRecurringProtectionOptions() {
|
||||
return [
|
||||
self::RCUR_PROTECTION_OFF => E::ts('No'),
|
||||
self::RCUR_PROTECTION_EXCEPTION => E::ts('Raise Exception'),
|
||||
self::RCUR_PROTECTION_ACTIVITY => E::ts('Create Activity'),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
325
CRM/Twingle/DAO/TwingleProduct.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
305
CRM/Twingle/DAO/TwingleShop.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -13,6 +13,8 @@
|
|||
| written permission from the original author(s). |
|
||||
+-------------------------------------------------------------*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
use CRM_Twingle_ExtensionUtil as E;
|
||||
|
||||
/**
|
||||
|
@ -22,171 +24,187 @@ use CRM_Twingle_ExtensionUtil as E;
|
|||
*/
|
||||
class CRM_Twingle_Form_Settings extends CRM_Core_Form {
|
||||
|
||||
private $_settingFilter = array('group' => 'de.systopia.twingle');
|
||||
|
||||
//everything from this line down is generic & can be re-used for a setting form in another extension
|
||||
//actually - I lied - I added a specific call in getFormSettings
|
||||
private $_submittedValues = array();
|
||||
private $_settings = array();
|
||||
/**
|
||||
* @var array<string>
|
||||
* List of all settings options.
|
||||
*/
|
||||
public static $SETTINGS_LIST = [
|
||||
'twingle_prefix',
|
||||
'twingle_use_sepa',
|
||||
'twingle_dont_use_reference',
|
||||
'twingle_protect_recurring',
|
||||
'twingle_protect_recurring_activity_type',
|
||||
'twingle_protect_recurring_activity_subject',
|
||||
'twingle_protect_recurring_activity_status',
|
||||
'twingle_protect_recurring_activity_assignee',
|
||||
'twingle_use_shop',
|
||||
'twingle_access_key',
|
||||
];
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
function buildQuickForm() {
|
||||
public function buildQuickForm(): void {
|
||||
// Set redirect destination.
|
||||
$this->controller->_destination = CRM_Utils_System::url('civicrm/admin/settings/twingle', 'reset=1');
|
||||
|
||||
$settings = $this->getFormSettings();
|
||||
$form_elements = array();
|
||||
|
||||
foreach ($settings as $name => $setting) {
|
||||
if (isset($setting['quick_form_type'])) {
|
||||
$add = 'add' . $setting['quick_form_type'];
|
||||
if ($add == 'addElement') {
|
||||
$this->$add(
|
||||
$setting['html_type'],
|
||||
$name,
|
||||
ts($setting['title']),
|
||||
CRM_Utils_Array::value('html_attributes', $setting, array())
|
||||
$this->add(
|
||||
'text',
|
||||
'twingle_prefix',
|
||||
E::ts('Twingle ID Prefix')
|
||||
);
|
||||
}
|
||||
elseif ($setting['html_type'] == 'Select') {
|
||||
$optionValues = array();
|
||||
if (!empty($setting['pseudoconstant']) && !empty($setting['pseudoconstant']['optionGroupName'])) {
|
||||
$optionValues = CRM_Core_OptionGroup::values($setting['pseudoconstant']['optionGroupName'], FALSE, FALSE, FALSE, NULL, 'name');
|
||||
}
|
||||
|
||||
$this->add(
|
||||
'checkbox',
|
||||
'twingle_use_sepa',
|
||||
E::ts('Use CiviSEPA')
|
||||
);
|
||||
|
||||
$this->add(
|
||||
'checkbox',
|
||||
'twingle_dont_use_reference',
|
||||
E::ts('Use CiviSEPA generated reference')
|
||||
);
|
||||
|
||||
$this->add(
|
||||
'select',
|
||||
$setting['name'],
|
||||
$setting['title'],
|
||||
$optionValues,
|
||||
FALSE,
|
||||
CRM_Utils_Array::value('html_attributes', $setting, array())
|
||||
'twingle_protect_recurring',
|
||||
E::ts('Protect Recurring Contributions'),
|
||||
CRM_Twingle_Config::getRecurringProtectionOptions()
|
||||
);
|
||||
}
|
||||
else {
|
||||
$this->$add($name, ts($setting['title']));
|
||||
}
|
||||
$form_elements[$setting['name']] = array('description' => ts($setting['description']));
|
||||
|
||||
// Disable CiviSEPA setting if the extension is not installed.
|
||||
if ($name == 'twingle_use_sepa') {
|
||||
$sepa_extension = civicrm_api3('Extension', 'get', array(
|
||||
'full_name' => 'org.project60.sepa',
|
||||
'is_active' => 1,
|
||||
));
|
||||
if ($sepa_extension['count'] == 0) {
|
||||
$element = $this->getElement('twingle_use_sepa');
|
||||
$element->freeze();
|
||||
$form_elements['twingle_use_sepa']['description'] .= ' <span class="error">The <a href="https://github.com/project60/org.project60.sepa" target="_blank" title="Extension page">CiviSEPA (<kbd>org.project60.sepa</kbd>) extension</a> is not installed.</span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->add(
|
||||
'select',
|
||||
'twingle_protect_recurring_activity_type',
|
||||
E::ts('Activity Type'),
|
||||
$this->getOptionValueList('activity_type', [0])
|
||||
);
|
||||
|
||||
$this->assign('formElements', $form_elements);
|
||||
$this->add(
|
||||
'text',
|
||||
'twingle_protect_recurring_activity_subject',
|
||||
E::ts('Subject'),
|
||||
['class' => 'huge']
|
||||
);
|
||||
|
||||
$this->addButtons(array(
|
||||
array (
|
||||
$this->add(
|
||||
'select',
|
||||
'twingle_protect_recurring_activity_status',
|
||||
E::ts('Status'),
|
||||
$this->getOptionValueList('activity_status')
|
||||
);
|
||||
|
||||
$this->addEntityRef(
|
||||
'twingle_protect_recurring_activity_assignee',
|
||||
E::ts('Assigned To'),
|
||||
[
|
||||
'api' => [
|
||||
'params' => [
|
||||
'contact_type' => ['IN' => ['Individual', 'Organization']],
|
||||
'check_permissions' => 0,
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$this->add(
|
||||
'checkbox',
|
||||
'twingle_use_shop',
|
||||
E::ts('Use Twingle Shop Integration')
|
||||
);
|
||||
|
||||
$this->add(
|
||||
'text',
|
||||
'twingle_access_key',
|
||||
E::ts('Twingle Access Key')
|
||||
);
|
||||
|
||||
$this->addButtons([
|
||||
[
|
||||
'type' => 'submit',
|
||||
'name' => ts('Save'),
|
||||
'name' => E::ts('Save'),
|
||||
'isDefault' => TRUE,
|
||||
)
|
||||
));
|
||||
],
|
||||
]);
|
||||
|
||||
// set defaults
|
||||
foreach (self::$SETTINGS_LIST as $setting) {
|
||||
$this->setDefaults([
|
||||
$setting => Civi::settings()->get($setting),
|
||||
]);
|
||||
}
|
||||
|
||||
// Export form elements.
|
||||
$this->assign('elementNames', $this->getRenderableElementNames());
|
||||
parent::buildQuickForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom form validation, as some fields are mandatory only when others are active.
|
||||
* @return bool
|
||||
*/
|
||||
public function validate() {
|
||||
parent::validate();
|
||||
|
||||
// if activity creation is active, make sure the fields are set
|
||||
$protection_mode = $this->_submitValues['twingle_protect_recurring'] ?? NULL;
|
||||
if ($protection_mode == CRM_Twingle_Config::RCUR_PROTECTION_ACTIVITY) {
|
||||
foreach ([
|
||||
'twingle_protect_recurring_activity_type',
|
||||
'twingle_protect_recurring_activity_subject',
|
||||
'twingle_protect_recurring_activity_status',
|
||||
'twingle_protect_recurring_activity_assignee',
|
||||
] as $activity_field) {
|
||||
if (NULL !== ($this->_submitValues[$activity_field] ?? NULL)) {
|
||||
$this->_errors[$activity_field] = E::ts('This is required for activity creation');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Twingle Access Key is required if Shop Integration is enabled
|
||||
if (
|
||||
CRM_Utils_Array::value('twingle_use_shop', $this->_submitValues) &&
|
||||
!CRM_Utils_Array::value('twingle_access_key', $this->_submitValues, FALSE)
|
||||
) {
|
||||
$this->_errors['twingle_access_key'] = E::ts('An Access Key is required to enable Twingle Shop Integration');
|
||||
}
|
||||
|
||||
return (0 == count($this->_errors));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
function postProcess() {
|
||||
$this->_submittedValues = $this->exportValues();
|
||||
$this->saveSettings();
|
||||
public function postProcess(): void {
|
||||
$values = $this->exportValues();
|
||||
|
||||
// store settings
|
||||
foreach (self::$SETTINGS_LIST as $setting) {
|
||||
Civi::settings()->set($setting, $values[$setting] ?? NULL);
|
||||
}
|
||||
|
||||
parent::postProcess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fields/elements defined in this form.
|
||||
*
|
||||
* @return array (string)
|
||||
* Get a list of option group items
|
||||
* @param string $group_id
|
||||
* Group ID or name.
|
||||
* @param array<int> $reserved
|
||||
* @return array<int|string, string> list of ID(value) => label
|
||||
* @throws \CRM_Core_Exception
|
||||
*/
|
||||
function getRenderableElementNames() {
|
||||
// The _elements list includes some items which should not be
|
||||
// auto-rendered in the loop -- such as "qfKey" and "buttons". These
|
||||
// items don't have labels. We'll identify renderable by filtering on
|
||||
// the 'label'.
|
||||
$elementNames = array();
|
||||
foreach ($this->_elements as $element) {
|
||||
/* @var \HTML_QuickForm_element $element */
|
||||
$label = $element->getLabel();
|
||||
if (!empty($label)) {
|
||||
$elementNames[] = $element->getName();
|
||||
protected function getOptionValueList(string $group_id, array $reserved = [0, 1]): array {
|
||||
$list = ['' => E::ts('-select-')];
|
||||
$query = civicrm_api3('OptionValue', 'get', [
|
||||
'option_group_id' => $group_id,
|
||||
'option.limit' => 0,
|
||||
'is_active' => 1,
|
||||
'is_reserved' => ['IN' => $reserved],
|
||||
'return' => 'value,label',
|
||||
]);
|
||||
foreach ($query['values'] as $value) {
|
||||
$list[$value['value']] = $value['label'];
|
||||
}
|
||||
}
|
||||
return $elementNames;
|
||||
}
|
||||
/**
|
||||
* Get the settings we are going to allow to be set on this form.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws \CiviCRM_API3_Exception
|
||||
*/
|
||||
function getFormSettings() {
|
||||
if (empty($this->_settings)) {
|
||||
$settings = civicrm_api3('setting', 'getfields', array('filters' => $this->_settingFilter));
|
||||
$settings = $settings['values'];
|
||||
}
|
||||
else {
|
||||
$settings = $this->_settings;
|
||||
}
|
||||
return $settings;
|
||||
}
|
||||
/**
|
||||
* Save the settings set on this form.
|
||||
*/
|
||||
function saveSettings() {
|
||||
$settings = $this->getFormSettings();
|
||||
$values = array_intersect_key($this->_submittedValues, $settings);
|
||||
civicrm_api3('setting', 'create', $values);
|
||||
}
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
function setDefaultValues() {
|
||||
$existing = civicrm_api3('setting', 'get', array('return' => array_keys($this->getFormSettings())));
|
||||
$defaults = array();
|
||||
$domainID = CRM_Core_Config::domainID();
|
||||
foreach ($existing['values'][$domainID] as $name => $value) {
|
||||
$defaults[$name] = $value;
|
||||
}
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function addRules() {
|
||||
$this->addFormRule(array('CRM_Twingle_Form_Settings', 'validateSettingsForm'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the profile form.
|
||||
*
|
||||
* @param array $values
|
||||
* The submitted form values, keyed by form element name.
|
||||
*
|
||||
* @return bool | array
|
||||
* TRUE when the form was successfully validated, or an array of error
|
||||
* messages, keyed by form element name.
|
||||
*/
|
||||
public static function validateSettingsForm($values) {
|
||||
$errors = array();
|
||||
|
||||
return empty($errors) ? TRUE : $errors;
|
||||
return $list;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,11 +13,13 @@
|
|||
| written permission from the original author(s). |
|
||||
+-------------------------------------------------------------*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
use CRM_Twingle_ExtensionUtil as E;
|
||||
|
||||
class CRM_Twingle_Page_Configuration extends CRM_Core_Page {
|
||||
|
||||
public function run() {
|
||||
public function run(): void {
|
||||
parent::run();
|
||||
}
|
||||
|
||||
|
|
|
@ -13,19 +13,30 @@
|
|||
| written permission from the original author(s). |
|
||||
+-------------------------------------------------------------*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
use CRM_Twingle_ExtensionUtil as E;
|
||||
|
||||
class CRM_Twingle_Page_Profiles extends CRM_Core_Page {
|
||||
|
||||
public function run() {
|
||||
$profiles = array();
|
||||
foreach (CRM_Twingle_Profile::getProfiles() as $profile_name => $profile) {
|
||||
$profiles[$profile_name]['name'] = $profile_name;
|
||||
public function run():void {
|
||||
CRM_Utils_System::setTitle(E::ts('Twingle API Profiles'));
|
||||
$profiles = [];
|
||||
foreach (CRM_Twingle_Profile::getProfiles() as $profile_id => $profile) {
|
||||
$profiles[$profile_id]['id'] = $profile_id;
|
||||
$profiles[$profile_id]['name'] = $profile->getName();
|
||||
$profiles[$profile_id]['is_default'] = $profile->is_default();
|
||||
$profiles[$profile_id]['selectors'] = $profile->getProjectIds();
|
||||
foreach (CRM_Twingle_Profile::allowedAttributes() as $attribute) {
|
||||
$profiles[$profile_name][$attribute] = $profile->getAttribute($attribute);
|
||||
$profiles[$profile_id][$attribute] = $profile->getAttribute($attribute);
|
||||
}
|
||||
}
|
||||
$this->assign('profiles', $profiles);
|
||||
$this->assign('profile_stats', CRM_Twingle_Profile::getProfileStats());
|
||||
$this->assign('twingle_use_shop', (int) Civi::settings()->get('twingle_use_shop'));
|
||||
|
||||
// Add custom css
|
||||
Civi::resources()->addStyleFile(E::LONG_NAME, 'css/twingle.css');
|
||||
|
||||
parent::run();
|
||||
}
|
||||
|
|
|
@ -13,7 +13,11 @@
|
|||
| written permission from the original author(s). |
|
||||
+-------------------------------------------------------------*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
use CRM_Twingle_ExtensionUtil as E;
|
||||
use Civi\Twingle\Exceptions\ProfileException as ProfileException;
|
||||
use Civi\Twingle\Exceptions\ProfileValidationError;
|
||||
|
||||
/**
|
||||
* Profiles define how incoming submissions from the Twingle API are
|
||||
|
@ -22,32 +26,44 @@ use CRM_Twingle_ExtensionUtil as E;
|
|||
class CRM_Twingle_Profile {
|
||||
|
||||
/**
|
||||
* @var CRM_Twingle_Profile[] $_profiles
|
||||
* Caches the profile objects.
|
||||
* @var int
|
||||
* The id of the profile.
|
||||
*/
|
||||
protected static $_profiles = NULL;
|
||||
protected ?int $id;
|
||||
|
||||
/**
|
||||
* @var string $name
|
||||
* @var string
|
||||
* The name of the profile.
|
||||
*/
|
||||
protected $name = NULL;
|
||||
protected string $name;
|
||||
|
||||
/**
|
||||
* @var array $data
|
||||
* @var array<string, mixed>
|
||||
* The properties of the profile.
|
||||
*/
|
||||
protected $data = NULL;
|
||||
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.
|
||||
*
|
||||
* @param string $name
|
||||
* The name of the profile.
|
||||
* @param array $data
|
||||
* @param array<string, mixed> $data
|
||||
* The properties of the profile
|
||||
* @param int|NULL $id
|
||||
*/
|
||||
public function __construct($name, $data) {
|
||||
public function __construct($name, $data, $id = NULL) {
|
||||
$this->id = $id;
|
||||
$this->name = $name;
|
||||
$allowed_attributes = self::allowedAttributes();
|
||||
$this->data = $data + array_combine(
|
||||
|
@ -56,6 +72,36 @@ class CRM_Twingle_Profile {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs (production) access to this profile
|
||||
*/
|
||||
public function logAccess(): void {
|
||||
CRM_Core_DAO::executeQuery('
|
||||
UPDATE civicrm_twingle_profile
|
||||
SET
|
||||
last_access = NOW(),
|
||||
access_counter = access_counter + 1
|
||||
WHERE name = %1', [1 => [$this->name, 'String']]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy this profile by returning a clone with all unique information removed.
|
||||
*
|
||||
* @return CRM_Twingle_Profile
|
||||
*/
|
||||
public function copy() {
|
||||
$copy = clone $this;
|
||||
|
||||
// Remove unique data
|
||||
$copy->id = NULL;
|
||||
$copy->data['selector'] = NULL;
|
||||
|
||||
// Propose a new name for this profile.
|
||||
$profile_name = $this->getName() . '_copy';
|
||||
$copy->setName($profile_name);
|
||||
return $copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the profile's selector matches the given project ID.
|
||||
*
|
||||
|
@ -64,52 +110,125 @@ class CRM_Twingle_Profile {
|
|||
* @return bool
|
||||
*/
|
||||
public function matches($project_id) {
|
||||
$selector = $this->getAttribute('selector');
|
||||
$project_ids = explode(',', $selector);
|
||||
return in_array($project_id, $project_ids);
|
||||
return in_array($project_id, $this->getProjectIds(), TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the profile's configured custom field mapping.
|
||||
*
|
||||
* @return array<string, string>
|
||||
* The profile's configured custom field mapping
|
||||
*/
|
||||
public function getCustomFieldMapping() {
|
||||
$custom_field_mapping = [];
|
||||
if ('' !== ($custom_field_definition = $this->getAttribute('custom_field_mapping', ''))) {
|
||||
/** @var string $custom_field_definition */
|
||||
$custom_field_maps = preg_split(
|
||||
'/\r\n|\r|\n/',
|
||||
$custom_field_definition,
|
||||
-1,
|
||||
PREG_SPLIT_NO_EMPTY
|
||||
);
|
||||
if (FALSE !== $custom_field_maps) {
|
||||
foreach ($custom_field_maps as $custom_field_map) {
|
||||
[$twingle_field_name, $custom_field_name] = explode('=', $custom_field_map);
|
||||
$custom_field_mapping[$twingle_field_name] = $custom_field_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $custom_field_mapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all data attributes of the profile.
|
||||
*
|
||||
* @return array
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getData() {
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the profile id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId(): ?int {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the profile id.
|
||||
*
|
||||
* @param int $id
|
||||
*/
|
||||
public function setId(int $id): void {
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the profile name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName() {
|
||||
public function getName(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the profile name.
|
||||
*
|
||||
* @param $name
|
||||
* @param string $name
|
||||
*/
|
||||
public function setName($name) {
|
||||
public function setName(string $name): void {
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this the default profile?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_default(): bool {
|
||||
return $this->name === 'default';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the profile's project IDs.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getProjectIds(): array {
|
||||
return array_map(
|
||||
function($project_id) {
|
||||
return trim($project_id);
|
||||
},
|
||||
explode(',', $this->getAttribute('selector') ?? '')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if Twingle Shop integration is enabled in general and
|
||||
* specifically for this profile.
|
||||
* @return bool
|
||||
*/
|
||||
public function isShopEnabled(): bool {
|
||||
return Civi::settings()->get('twingle_use_shop') &&
|
||||
$this->data['enable_shop_integration'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an attribute of the profile.
|
||||
*
|
||||
* @param string $attribute_name
|
||||
* @param mixed $default
|
||||
*
|
||||
* @return mixed | NULL
|
||||
*/
|
||||
public function getAttribute($attribute_name) {
|
||||
if (isset($this->data[$attribute_name])) {
|
||||
return $this->data[$attribute_name];
|
||||
}
|
||||
else {
|
||||
return NULL;
|
||||
}
|
||||
public function getAttribute($attribute_name, $default = NULL) {
|
||||
return (isset($this->data[$attribute_name]) && $this->data[$attribute_name] !== '')
|
||||
? $this->data[$attribute_name]
|
||||
: $default;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -118,88 +237,365 @@ class CRM_Twingle_Profile {
|
|||
* @param string $attribute_name
|
||||
* @param mixed $value
|
||||
*
|
||||
* @throws \Exception
|
||||
* @throws \Civi\Twingle\Exceptions\ProfileException
|
||||
* When the attribute name is not known.
|
||||
*/
|
||||
public function setAttribute($attribute_name, $value) {
|
||||
if (!in_array($attribute_name, self::allowedAttributes())) {
|
||||
throw new Exception(E::ts('Unknown attribute %1.', array(1 => $attribute_name)));
|
||||
public function setAttribute($attribute_name, $value): void {
|
||||
if (!in_array($attribute_name, self::allowedAttributes(), TRUE)) {
|
||||
throw new ProfileException(
|
||||
E::ts('Unknown attribute %1.', [1 => $attribute_name]),
|
||||
ProfileException::ERROR_CODE_UNKNOWN_PROFILE_ATTRIBUTE
|
||||
);
|
||||
}
|
||||
// TODO: Check if value is acceptable.
|
||||
$this->data[$attribute_name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the CiviCRM transaction ID (to be used in contributions and recurring contributions)
|
||||
*
|
||||
* @param string $twingle_id Twingle ID
|
||||
* @return string CiviCRM transaction ID
|
||||
*/
|
||||
public function getTransactionID(string $twingle_id) {
|
||||
$prefix = Civi::settings()->get('twingle_prefix');
|
||||
return ($prefix ?? '') . $twingle_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies whether the profile is valid (i.e. consistent and not colliding
|
||||
* with other profiles).
|
||||
*
|
||||
* @throws Exception
|
||||
* @throws \Civi\Twingle\Exceptions\ProfileValidationError
|
||||
* @throws \Civi\Core\Exception\DBQueryException
|
||||
* When the profile could not be successfully validated.
|
||||
*/
|
||||
public function verifyProfile() {
|
||||
// TODO: check
|
||||
// data of this profile consistent?
|
||||
// conflicts with other profiles?
|
||||
public function validate(): void {
|
||||
|
||||
// Name cannot be empty
|
||||
if ('' === $this->getName()) {
|
||||
throw new ProfileValidationError(
|
||||
'name',
|
||||
E::ts('Profile name cannot be empty.'),
|
||||
ProfileValidationError::ERROR_CODE_PROFILE_VALIDATION_FAILED
|
||||
);
|
||||
}
|
||||
|
||||
// Restrict profile names to alphanumeric characters, space and the underscore.
|
||||
if (1 === preg_match('/[^A-Za-z0-9_\s]/', $this->getName())) {
|
||||
throw new ProfileValidationError(
|
||||
'name',
|
||||
E::ts('Only alphanumeric characters, space and the underscore (_) are allowed for profile names.'),
|
||||
ProfileValidationError::ERROR_CODE_PROFILE_VALIDATION_FAILED
|
||||
);
|
||||
}
|
||||
|
||||
// Check if profile name is already used for other profile
|
||||
$profile_name_duplicates = array_filter(
|
||||
CRM_Twingle_Profile::getProfiles(),
|
||||
function($profile) {
|
||||
return $profile->getName() == $this->getName() && $this->getId() != $profile->getId();
|
||||
});
|
||||
if ([] !== $profile_name_duplicates) {
|
||||
throw new ProfileValidationError(
|
||||
'name',
|
||||
E::ts("A profile with the name '%1' already exists.", [1 => $this->getName()]),
|
||||
ProfileValidationError::ERROR_CODE_PROFILE_VALIDATION_FAILED
|
||||
);
|
||||
}
|
||||
|
||||
// Check if project_id is already used in other profile
|
||||
$profiles = $this::getProfiles();
|
||||
foreach ($profiles as $profile) {
|
||||
if ($profile->getId() == $this->getId() || $profile->is_default()) {
|
||||
continue;
|
||||
};
|
||||
$project_ids = $this->getProjectIds();
|
||||
$id_duplicates = array_intersect($profile->getProjectIds(), $project_ids);
|
||||
if ([] !== $id_duplicates) {
|
||||
throw new ProfileValidationError(
|
||||
'selector',
|
||||
E::ts(
|
||||
"Project ID(s) [%1] already used in profile '%2'.",
|
||||
[
|
||||
1 => implode(', ', $id_duplicates),
|
||||
2 => $profile->getName(),
|
||||
]
|
||||
),
|
||||
ProfileValidationError::ERROR_CODE_PROFILE_VALIDATION_WARNING
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate custom field mapping.
|
||||
$custom_field_mapping = $this->getAttribute('custom_field_mapping');
|
||||
if (is_string($custom_field_mapping)) {
|
||||
$custom_field_mapping = preg_split('/\r\n|\r|\n/', $custom_field_mapping, -1, PREG_SPLIT_NO_EMPTY);
|
||||
$parsing_error = new ProfileValidationError(
|
||||
'custom_field_mapping',
|
||||
E::ts('Could not parse custom field mapping.'),
|
||||
ProfileValidationError::ERROR_CODE_PROFILE_VALIDATION_FAILED
|
||||
);
|
||||
if (!is_array($custom_field_mapping)) {
|
||||
throw $parsing_error;
|
||||
}
|
||||
foreach ($custom_field_mapping as $custom_field_map) {
|
||||
$custom_field_map = explode('=', $custom_field_map);
|
||||
if (count($custom_field_map) !== 2) {
|
||||
throw $parsing_error;
|
||||
}
|
||||
[$twingle_field_name, $custom_field_name] = $custom_field_map;
|
||||
$custom_field_id = substr($custom_field_name, strlen('custom_'));
|
||||
|
||||
// Check for custom field existence
|
||||
try {
|
||||
/**
|
||||
* @phpstan-var array<string, mixed> $custom_field
|
||||
*/
|
||||
$custom_field = civicrm_api3(
|
||||
'CustomField', 'getsingle', ['id' => $custom_field_id]
|
||||
);
|
||||
}
|
||||
catch (CRM_Core_Exception $exception) {
|
||||
throw new ProfileValidationError(
|
||||
'custom_field_mapping',
|
||||
E::ts(
|
||||
'Custom field custom_%1 does not exist.',
|
||||
[1 => $custom_field_id]
|
||||
),
|
||||
ProfileValidationError::ERROR_CODE_PROFILE_VALIDATION_FAILED,
|
||||
$exception
|
||||
);
|
||||
}
|
||||
|
||||
// Only allow custom fields on relevant entities.
|
||||
try {
|
||||
civicrm_api3('CustomGroup', 'getsingle',
|
||||
[
|
||||
'id' => $custom_field['custom_group_id'],
|
||||
'extends' => [
|
||||
'IN' => [
|
||||
'Contact',
|
||||
'Individual',
|
||||
'Organization',
|
||||
'Contribution',
|
||||
'ContributionRecur',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
catch (CRM_Core_Exception $exception) {
|
||||
throw new ProfileValidationError(
|
||||
'custom_field_mapping',
|
||||
E::ts(
|
||||
'Custom field custom_%1 is not in a CustomGroup that extends one of the supported CiviCRM entities.',
|
||||
[1 => $custom_field['id']]
|
||||
),
|
||||
ProfileValidationError::ERROR_CODE_PROFILE_VALIDATION_FAILED,
|
||||
$exception
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the profile within the CiviCRM settings.
|
||||
* Persists the profile within the database.
|
||||
*
|
||||
* @throws \Civi\Twingle\Exceptions\ProfileException
|
||||
*/
|
||||
public function saveProfile() {
|
||||
self::$_profiles[$this->getName()] = $this;
|
||||
$this->verifyProfile();
|
||||
self::storeProfiles();
|
||||
public function saveProfile(): void {
|
||||
try {
|
||||
if (isset($this->id)) {
|
||||
// existing profile -> just update the config
|
||||
CRM_Core_DAO::executeQuery(
|
||||
'UPDATE civicrm_twingle_profile SET config = %2, name = %3 WHERE id = %1',
|
||||
[
|
||||
1 => [$this->id, 'String'],
|
||||
2 => [json_encode($this->data), 'String'],
|
||||
3 => [$this->name, 'String'],
|
||||
]);
|
||||
}
|
||||
else {
|
||||
// new profile -> add new entry to the DB
|
||||
CRM_Core_DAO::executeQuery(
|
||||
<<<SQL
|
||||
INSERT IGNORE INTO civicrm_twingle_profile(name,config,last_access,access_counter) VALUES (%1, %2, null, 0)
|
||||
SQL,
|
||||
[
|
||||
1 => [$this->name, 'String'],
|
||||
2 => [json_encode($this->data), 'String'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
catch (Exception $exception) {
|
||||
throw new ProfileException(
|
||||
E::ts('Could not save/update profile: %1', [1 => $exception->getMessage()]),
|
||||
ProfileException::ERROR_CODE_COULD_NOT_SAVE_PROFILE,
|
||||
$exception
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the profile from the CiviCRM settings.
|
||||
* Deletes the profile from the database
|
||||
*
|
||||
* @throws \Civi\Twingle\Exceptions\ProfileException
|
||||
*/
|
||||
public function deleteProfile() {
|
||||
unset(self::$_profiles[$this->getName()]);
|
||||
self::storeProfiles();
|
||||
public function deleteProfile(): void {
|
||||
// Do only reset default profile
|
||||
if ($this->getName() == 'default') {
|
||||
try {
|
||||
$default_profile = CRM_Twingle_Profile::createDefaultProfile();
|
||||
$default_profile->setId($this->getId());
|
||||
$default_profile->saveProfile();
|
||||
|
||||
// Reset counter
|
||||
CRM_Core_DAO::executeQuery(
|
||||
'UPDATE civicrm_twingle_profile SET access_counter = 0, last_access = NULL WHERE id = %1',
|
||||
[1 => [$this->id, 'Integer']]
|
||||
);
|
||||
}
|
||||
catch (Exception $exception) {
|
||||
throw new ProfileException(
|
||||
E::ts('Could not reset default profile: %1', [1 => $exception->getMessage()]),
|
||||
ProfileException::ERROR_CODE_COULD_NOT_RESET_PROFILE,
|
||||
$exception
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
try {
|
||||
CRM_Core_DAO::executeQuery(
|
||||
'DELETE FROM civicrm_twingle_profile WHERE id = %1',
|
||||
[1 => [$this->id, 'Integer']]
|
||||
);
|
||||
}
|
||||
catch (Exception $exception) {
|
||||
throw new ProfileException(
|
||||
E::ts('Could not delete profile: %1', [1 => $exception->getMessage()]),
|
||||
ProfileException::ERROR_CODE_COULD_NOT_DELETE_PROFILE,
|
||||
$exception
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of attributes allowed for a profile.
|
||||
*
|
||||
* @return array
|
||||
* @return array<string>|array<string, bool>
|
||||
*/
|
||||
public static function allowedAttributes() {
|
||||
return array(
|
||||
'selector',
|
||||
'location_type_id',
|
||||
'location_type_id_organisation',
|
||||
'financial_type_id',
|
||||
'pi_banktransfer',
|
||||
'pi_debit_manual',
|
||||
'pi_debit_automatic',
|
||||
'pi_creditcard',
|
||||
'pi_mobilephone_germany',
|
||||
'pi_paypal',
|
||||
'pi_sofortueberweisung',
|
||||
'pi_amazonpay',
|
||||
'pi_paydirekt',
|
||||
'pi_applepay',
|
||||
'pi_googlepay',
|
||||
'sepa_creditor_id',
|
||||
'gender_male',
|
||||
'gender_female',
|
||||
'gender_other',
|
||||
'newsletter_groups',
|
||||
'postinfo_groups',
|
||||
'donation_receipt_groups',
|
||||
'campaign',
|
||||
'contribution_source',
|
||||
public static function allowedAttributes(bool $asMetadata = FALSE) {
|
||||
$attributes = array_merge(
|
||||
[
|
||||
'selector' => [
|
||||
'label' => E::ts('Project IDs'),
|
||||
'required' => TRUE,
|
||||
],
|
||||
'xcm_profile' => ['required' => FALSE],
|
||||
'location_type_id' => [
|
||||
'label' => E::ts('Location type'),
|
||||
'required' => TRUE,
|
||||
],
|
||||
'location_type_id_organisation' => [
|
||||
'label' => E::ts('Location type for organisations'),
|
||||
'required' => TRUE,
|
||||
],
|
||||
'financial_type_id' => [
|
||||
'label' => E::ts('Financial type'),
|
||||
'required' => TRUE,
|
||||
],
|
||||
'financial_type_id_recur' => [
|
||||
'label' => E::ts('Financial type (recurring)'),
|
||||
'required' => TRUE,
|
||||
],
|
||||
'sepa_creditor_id' => [
|
||||
'label' => E::ts('CiviSEPA creditor'),
|
||||
'required' => CRM_Twingle_Submission::civiSepaEnabled(),
|
||||
],
|
||||
'gender_male' => [
|
||||
'label' => E::ts('Gender option for submitted value "male"'),
|
||||
'required' => TRUE,
|
||||
],
|
||||
'gender_female' => [
|
||||
'label' => E::ts('Gender option for submitted value "female"'),
|
||||
'required' => TRUE,
|
||||
],
|
||||
'gender_other' => [
|
||||
'label' => E::ts('Gender option for submitted value "other"'),
|
||||
'required' => TRUE,
|
||||
],
|
||||
'prefix_male' => [
|
||||
'label' => E::ts('Prefix option for submitted value "male"'),
|
||||
'required' => TRUE,
|
||||
],
|
||||
'prefix_female' => [
|
||||
'label' => E::ts('Prefix option for submitted value "female"'),
|
||||
'required' => TRUE,
|
||||
],
|
||||
'prefix_other' => [
|
||||
'label' => E::ts('Prefix option for submitted value "other"'),
|
||||
'required' => TRUE,
|
||||
],
|
||||
'newsletter_groups' => ['required' => FALSE],
|
||||
'postinfo_groups' => ['required' => FALSE],
|
||||
'donation_receipt_groups' => ['required' => FALSE],
|
||||
'campaign' => ['required' => FALSE],
|
||||
'campaign_targets' => ['required' => FALSE],
|
||||
'contribution_source' => ['required' => FALSE],
|
||||
'custom_field_mapping' => ['required' => FALSE],
|
||||
'membership_type_id' => ['required' => FALSE],
|
||||
'membership_type_id_recur' => ['required' => FALSE],
|
||||
'membership_postprocess_call' => ['required' => FALSE],
|
||||
'newsletter_double_opt_in' => ['required' => FALSE],
|
||||
'required_address_components' => ['required' => FALSE],
|
||||
'map_as_contribution_notes' => ['required' => FALSE],
|
||||
'map_as_contact_notes' => ['required' => FALSE],
|
||||
'enable_shop_integration' => ['required' => FALSE],
|
||||
'shop_financial_type' => ['required' => FALSE],
|
||||
'shop_donation_financial_type' => ['required' => FALSE],
|
||||
'shop_map_products' => ['required' => FALSE],
|
||||
],
|
||||
// Add payment methods.
|
||||
array_combine(
|
||||
array_keys(static::paymentInstruments()),
|
||||
array_map(
|
||||
function ($value) {
|
||||
return [
|
||||
'label' => $value,
|
||||
'required' => TRUE,
|
||||
];
|
||||
},
|
||||
static::paymentInstruments()
|
||||
)),
|
||||
|
||||
// Add contribution status for all payment methods.
|
||||
array_combine(
|
||||
array_map(function($attribute) {
|
||||
return $attribute . '_status';
|
||||
}, array_keys(static::paymentInstruments())),
|
||||
array_map(
|
||||
function($value) {
|
||||
return [
|
||||
'label' => $value . ' - ' . E::ts('Contribution Status'),
|
||||
'required' => TRUE,
|
||||
];
|
||||
},
|
||||
static::paymentInstruments()
|
||||
)),
|
||||
);
|
||||
|
||||
return $asMetadata ? $attributes : array_keys($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of supported payment methods.
|
||||
*
|
||||
* @return array
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function paymentInstruments() {
|
||||
return array(
|
||||
public static function paymentInstruments(): array {
|
||||
return [
|
||||
'pi_banktransfer' => E::ts('Bank transfer'),
|
||||
'pi_debit_manual' => E::ts('Debit manual'),
|
||||
'pi_debit_automatic' => E::ts('Debit automatic'),
|
||||
|
@ -208,10 +604,15 @@ class CRM_Twingle_Profile {
|
|||
'pi_paypal' => E::ts('PayPal'),
|
||||
'pi_sofortueberweisung' => E::ts('SOFORT Überweisung'),
|
||||
'pi_amazonpay' => E::ts('Amazon Pay'),
|
||||
'pi_paydirekt' => E::ts('paydirekt'),
|
||||
'pi_applepay' => E::ts('Apple Pay'),
|
||||
'pi_googlepay' => E::ts('Google Pay'),
|
||||
);
|
||||
'pi_paydirekt' => E::ts('Paydirekt'),
|
||||
'pi_twint' => E::ts('Twint'),
|
||||
'pi_ideal' => E::ts('iDEAL'),
|
||||
'pi_post_finance' => E::ts('Postfinance'),
|
||||
'pi_bancontact' => E::ts('Bancontact'),
|
||||
'pi_generic' => E::ts('Generic Payment Method'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -223,15 +624,22 @@ class CRM_Twingle_Profile {
|
|||
* @return CRM_Twingle_Profile
|
||||
*/
|
||||
public static function createDefaultProfile($name = 'default') {
|
||||
return new CRM_Twingle_Profile($name, array(
|
||||
'selector' => '',
|
||||
return new CRM_Twingle_Profile($name, [
|
||||
'selector' => NULL,
|
||||
'xcm_profile' => '',
|
||||
'location_type_id' => CRM_Twingle_Submission::LOCATION_TYPE_ID_WORK,
|
||||
'location_type_id_organisation' => CRM_Twingle_Submission::LOCATION_TYPE_ID_WORK,
|
||||
'financial_type_id' => 1, // "Donation"
|
||||
'pi_banktransfer' => 5, // "EFT"
|
||||
// "Donation"
|
||||
'financial_type_id' => 1,
|
||||
// "Donation"
|
||||
'financial_type_id_recur' => 1,
|
||||
// "EFT"
|
||||
'pi_banktransfer' => 5,
|
||||
'pi_debit_manual' => NULL,
|
||||
'pi_debit_automatic' => 3, // Debit
|
||||
'pi_creditcard' => 1, // "Credit Card"
|
||||
// Debit
|
||||
'pi_debit_automatic' => 2,
|
||||
// "Credit Card"
|
||||
'pi_creditcard' => 1,
|
||||
'pi_mobilephone_germany' => NULL,
|
||||
'pi_paypal' => NULL,
|
||||
'pi_sofortueberweisung' => NULL,
|
||||
|
@ -239,6 +647,11 @@ class CRM_Twingle_Profile {
|
|||
'pi_paydirekt' => NULL,
|
||||
'pi_applepay' => NULL,
|
||||
'pi_googlepay' => NULL,
|
||||
'pi_twint' => NULL,
|
||||
'pi_ideal' => NULL,
|
||||
'pi_post_finance' => NULL,
|
||||
'pi_bancontact' => NULL,
|
||||
'pi_generic' => NULL,
|
||||
'sepa_creditor_id' => NULL,
|
||||
'gender_male' => 2,
|
||||
'gender_female' => 1,
|
||||
|
@ -247,8 +660,30 @@ class CRM_Twingle_Profile {
|
|||
'postinfo_groups' => NULL,
|
||||
'donation_receipt_groups' => NULL,
|
||||
'campaign' => NULL,
|
||||
'campaign_targets' => ['contribution', 'contact'],
|
||||
'contribution_source' => NULL,
|
||||
));
|
||||
'custom_field_mapping' => NULL,
|
||||
'membership_type_id' => NULL,
|
||||
'membership_type_id_recur' => NULL,
|
||||
'newsletter_double_opt_in' => NULL,
|
||||
'required_address_components' => [
|
||||
'street_address',
|
||||
'postal_code',
|
||||
'city',
|
||||
'country',
|
||||
],
|
||||
'map_as_contribution_notes' => [],
|
||||
'map_as_contact_notes' => [],
|
||||
'enable_shop_integration' => FALSE,
|
||||
'shop_financial_type' => 1,
|
||||
'shop_donation_financial_type' => 1,
|
||||
'shop_map_products' => FALSE,
|
||||
]
|
||||
// Add contribution status for all payment methods.
|
||||
// phpcs:ignore Drupal.Formatting.SpaceUnaryOperator.PlusMinus
|
||||
+ array_fill_keys(array_map(function($attribute) {
|
||||
return $attribute . '_status';
|
||||
}, array_keys(static::paymentInstruments())), CRM_Twingle_Submission::CONTRIBUTION_STATUS_COMPLETED));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -256,76 +691,120 @@ class CRM_Twingle_Profile {
|
|||
* which is responsible for processing the project's data.
|
||||
* Returns the default profile if no match was found.
|
||||
*
|
||||
* @param $project_id
|
||||
* @param string $project_id
|
||||
*
|
||||
* @return CRM_Twingle_Profile
|
||||
* @throws \Civi\Twingle\Exceptions\ProfileException
|
||||
* @throws \Civi\Core\Exception\DBQueryException
|
||||
*/
|
||||
public static function getProfileForProject($project_id) {
|
||||
$profiles = self::getProfiles();
|
||||
|
||||
// If none matches, use the default profile.
|
||||
$profile = $profiles['default'];
|
||||
$default_profile = NULL;
|
||||
|
||||
foreach ($profiles as $profile) {
|
||||
if ($profile->matches($project_id)) {
|
||||
break;
|
||||
return $profile;
|
||||
}
|
||||
if ($profile->is_default()) {
|
||||
$default_profile = $profile;
|
||||
}
|
||||
}
|
||||
|
||||
return $profile;
|
||||
// If none matches, use the default profile.
|
||||
if (!empty($default_profile)) {
|
||||
return $default_profile;
|
||||
}
|
||||
else {
|
||||
throw new ProfileException(
|
||||
'Could not find default profile',
|
||||
ProfileException::ERROR_CODE_DEFAULT_PROFILE_NOT_FOUND
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the profile with the given name.
|
||||
* Retrieves the profile with the given ID.
|
||||
*
|
||||
* @param $name
|
||||
* @param int|NULL $id
|
||||
*
|
||||
* @return CRM_Twingle_Profile | NULL
|
||||
* @throws \Civi\Core\Exception\DBQueryException
|
||||
* @throws \Civi\Twingle\Exceptions\ProfileException
|
||||
*/
|
||||
public static function getProfile($name) {
|
||||
$profiles = self::getProfiles();
|
||||
if (isset($profiles[$name])) {
|
||||
return $profiles[$name];
|
||||
public static function getProfile(int $id = NULL) {
|
||||
if (isset($id)) {
|
||||
/**
|
||||
* @var CRM_Core_DAO $profile_data
|
||||
*/
|
||||
$profile_data = CRM_Core_DAO::executeQuery(
|
||||
'SELECT id, name, config FROM civicrm_twingle_profile WHERE id = %1',
|
||||
[1 => [$id, 'Integer']]
|
||||
);
|
||||
if ($profile_data->fetch()) {
|
||||
return new CRM_Twingle_Profile(
|
||||
$profile_data->name,
|
||||
json_decode($profile_data->config, TRUE),
|
||||
(int) $profile_data->id
|
||||
);
|
||||
}
|
||||
else {
|
||||
return NULL;
|
||||
}
|
||||
throw new ProfileException('Profile not found.', ProfileException::ERROR_CODE_PROFILE_NOT_FOUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of all profiles persisted within the current CiviCRM
|
||||
* settings, including the default profile.
|
||||
*
|
||||
* @return CRM_Twingle_Profile[]
|
||||
* @return array<int, \CRM_Twingle_Profile>
|
||||
* An array of profiles with profile IDs as keys and profile objects as values.
|
||||
* @throws \Civi\Core\Exception\DBQueryException
|
||||
*/
|
||||
public static function getProfiles() {
|
||||
if (self::$_profiles === NULL) {
|
||||
self::$_profiles = array();
|
||||
if ($profiles_data = CRM_Core_BAO_Setting::getItem('de.systopia.twingle', 'twingle_profiles')) {
|
||||
foreach ($profiles_data as $profile_name => $profile_data) {
|
||||
self::$_profiles[$profile_name] = new CRM_Twingle_Profile($profile_name, $profile_data);
|
||||
public static function getProfiles(): array {
|
||||
// todo: cache?
|
||||
$profiles = [];
|
||||
/**
|
||||
* @var CRM_Core_DAO $profile_data
|
||||
*/
|
||||
$profile_data = CRM_Core_DAO::executeQuery('SELECT id, name, config FROM civicrm_twingle_profile');
|
||||
while ($profile_data->fetch()) {
|
||||
$profiles[(int) $profile_data->id] = new CRM_Twingle_Profile(
|
||||
$profile_data->name,
|
||||
json_decode($profile_data->config, TRUE),
|
||||
(int) $profile_data->id
|
||||
);
|
||||
}
|
||||
return $profiles;
|
||||
}
|
||||
}
|
||||
|
||||
// Include the default profile if it was not overridden within the settings.
|
||||
if (!isset(self::$_profiles['default'])) {
|
||||
self::$_profiles['default'] = self::createDefaultProfile();
|
||||
self::storeProfiles();
|
||||
}
|
||||
|
||||
return self::$_profiles;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Persists the list of profiles into the CiviCRM settings.
|
||||
* Get the stats (access_count, last_access) for all twingle profiles
|
||||
*
|
||||
* @return array<string, array<string, mixed>>
|
||||
* @throws \Civi\Core\Exception\DBQueryException
|
||||
*/
|
||||
public static function storeProfiles() {
|
||||
$profile_data = array();
|
||||
foreach (self::$_profiles as $profile_name => $profile) {
|
||||
$profile_data[$profile_name] = $profile->data;
|
||||
public static function getProfileStats() {
|
||||
$stats = [];
|
||||
/**
|
||||
* @var CRM_Core_DAO $profile_data
|
||||
*/
|
||||
$profile_data = CRM_Core_DAO::executeQuery(
|
||||
'SELECT name, last_access, access_counter FROM civicrm_twingle_profile'
|
||||
);
|
||||
while ($profile_data->fetch()) {
|
||||
// phpcs:disable Drupal.Arrays.Array.ArrayIndentation
|
||||
$stats[(string) $profile_data->name] = [
|
||||
'name' => $profile_data->name,
|
||||
'last_access' => $profile_data->last_access,
|
||||
'last_access_txt' => $profile_data->last_access
|
||||
? date('Y-m-d H:i:s', strtotime($profile_data->last_access))
|
||||
: E::ts('never'),
|
||||
'access_counter' => $profile_data->access_counter,
|
||||
'access_counter_txt' => $profile_data->access_counter
|
||||
? ((int) $profile_data->access_counter) . 'x'
|
||||
: E::ts('never'),
|
||||
];
|
||||
// phpcs:enable
|
||||
}
|
||||
CRM_Core_BAO_Setting::setItem((object) $profile_data, 'de.systopia.twingle', 'twingle_profiles');
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,24 +13,45 @@
|
|||
| written permission from the original author(s). |
|
||||
+-------------------------------------------------------------*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
use CRM_Twingle_ExtensionUtil as E;
|
||||
use Civi\Twingle\Exceptions\BaseException;
|
||||
use Civi\Twingle\Shop\Exceptions\LineItemException;
|
||||
|
||||
class CRM_Twingle_Submission {
|
||||
|
||||
/**
|
||||
* The default ID of the "Work" location type.
|
||||
*/
|
||||
const LOCATION_TYPE_ID_WORK = 2;
|
||||
public const LOCATION_TYPE_ID_WORK = 2;
|
||||
|
||||
/**
|
||||
* The option value name of the group type for newsletter subscribers.
|
||||
*/
|
||||
const GROUP_TYPE_NEWSLETTER = 'Mailing List';
|
||||
public const GROUP_TYPE_NEWSLETTER = 'Mailing List';
|
||||
|
||||
/**
|
||||
* The option value for the contribution type for completed contributions.
|
||||
*/
|
||||
public const CONTRIBUTION_STATUS_COMPLETED = 'Completed';
|
||||
|
||||
/**
|
||||
* The default ID of the "Employer of" relationship type.
|
||||
*/
|
||||
const EMPLOYER_RELATIONSHIP_TYPE_ID = 5;
|
||||
public const EMPLOYER_RELATIONSHIP_TYPE_ID = 5;
|
||||
|
||||
/**
|
||||
* List of allowed product attributes.
|
||||
*/
|
||||
const ALLOWED_PRODUCT_ATTRIBUTES = [
|
||||
'id',
|
||||
'name',
|
||||
'internal_id',
|
||||
'price',
|
||||
'count',
|
||||
'total_value',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param array &$params
|
||||
|
@ -40,23 +61,23 @@ class CRM_Twingle_Submission {
|
|||
* The Twingle profile to use for validation, defaults to the default
|
||||
* profile.
|
||||
*
|
||||
* @throws \CiviCRM_API3_Exception
|
||||
* @throws \CRM_Core_Exception
|
||||
* When invalid parameters have been submitted.
|
||||
*/
|
||||
public static function validateSubmission(&$params, $profile = NULL) {
|
||||
if (!$profile) {
|
||||
public static function validateSubmission(&$params, $profile = NULL): void {
|
||||
if (!isset($profile)) {
|
||||
$profile = CRM_Twingle_Profile::createDefaultProfile();
|
||||
}
|
||||
|
||||
// Validate donation rhythm.
|
||||
if (!in_array($params['donation_rhythm'], array(
|
||||
if (!in_array($params['donation_rhythm'], [
|
||||
'one_time',
|
||||
'halfyearly',
|
||||
'quarterly',
|
||||
'yearly',
|
||||
'monthly',
|
||||
))) {
|
||||
throw new CiviCRM_API3_Exception(
|
||||
], TRUE)) {
|
||||
throw new CRM_Core_Exception(
|
||||
E::ts('Invalid donation rhythm.'),
|
||||
'invalid_format'
|
||||
);
|
||||
|
@ -64,8 +85,9 @@ class CRM_Twingle_Submission {
|
|||
|
||||
// Get the payment instrument defined within the profile, or return an error
|
||||
// if none matches (i.e. an unknown payment method was submitted).
|
||||
if (!$payment_instrument_id = $profile->getAttribute('pi_' . $params['payment_method'])) {
|
||||
throw new CiviCRM_API3_Exception(
|
||||
$payment_instrument_id = $profile->getAttribute('pi_' . $params['payment_method'], '');
|
||||
if ('' === $payment_instrument_id) {
|
||||
throw new CRM_Core_Exception(
|
||||
E::ts('Payment method could not be matched to existing payment instrument.'),
|
||||
'invalid_format'
|
||||
);
|
||||
|
@ -73,16 +95,16 @@ class CRM_Twingle_Submission {
|
|||
$params['payment_instrument_id'] = $payment_instrument_id;
|
||||
|
||||
// Validate date for parameter "confirmed_at".
|
||||
if (!DateTime::createFromFormat('YmdHis', $params['confirmed_at'])) {
|
||||
throw new CiviCRM_API3_Exception(
|
||||
if (FALSE === DateTime::createFromFormat('YmdHis', $params['confirmed_at'])) {
|
||||
throw new CRM_Core_Exception(
|
||||
E::ts('Invalid date for parameter "confirmed_at".'),
|
||||
'invalid_format'
|
||||
);
|
||||
}
|
||||
|
||||
// Validate date for parameter "user_birthdate".
|
||||
if (!empty($params['user_birthdate']) && !DateTime::createFromFormat('Ymd', $params['user_birthdate'])) {
|
||||
throw new CiviCRM_API3_Exception(
|
||||
if (!empty($params['user_birthdate']) && FALSE === DateTime::createFromFormat('Ymd', $params['user_birthdate'])) {
|
||||
throw new CRM_Core_Exception(
|
||||
E::ts('Invalid date for parameter "user_birthdate".'),
|
||||
'invalid_format'
|
||||
);
|
||||
|
@ -90,15 +112,70 @@ class CRM_Twingle_Submission {
|
|||
|
||||
// Get the gender ID defined within the profile, or return an error if none
|
||||
// matches (i.e. an unknown gender was submitted).
|
||||
if (!empty($params['user_gender'])) {
|
||||
if (!$gender_id = $profile->getAttribute('gender_' . $params['user_gender'])) {
|
||||
throw new CiviCRM_API3_Exception(
|
||||
if (is_string($params['user_gender'])) {
|
||||
$gender_id = $profile->getAttribute('gender_' . $params['user_gender']);
|
||||
if (!is_numeric($gender_id)) {
|
||||
throw new CRM_Core_Exception(
|
||||
E::ts('Gender could not be matched to existing gender.'),
|
||||
'invalid_format'
|
||||
);
|
||||
}
|
||||
$params['gender_id'] = $gender_id;
|
||||
}
|
||||
|
||||
// Validate custom fields parameter, if given.
|
||||
if (isset($params['custom_fields'])) {
|
||||
if (is_string($params['custom_fields'])) {
|
||||
$params['custom_fields'] = json_decode($params['custom_fields'], TRUE);
|
||||
}
|
||||
if (!is_array($params['custom_fields'])) {
|
||||
throw new CRM_Core_Exception(
|
||||
E::ts('Invalid format for custom fields.'),
|
||||
'invalid_format'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate products
|
||||
if (!empty($params['products']) && $profile->isShopEnabled()) {
|
||||
if (is_string($params['products'])) {
|
||||
$products = json_decode($params['products'], TRUE);
|
||||
$params['products'] = array_map(function ($product) {
|
||||
return array_intersect_key($product, array_flip(self::ALLOWED_PRODUCT_ATTRIBUTES));
|
||||
}, $products);
|
||||
}
|
||||
if (!is_array($params['products'])) {
|
||||
throw new CiviCRM_API3_Exception(
|
||||
E::ts('Invalid format for products.'),
|
||||
'invalid_format'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate campaign_id, if given.
|
||||
if (isset($params['campaign_id'])) {
|
||||
// Check whether campaign_id is a numeric string and cast it to an integer.
|
||||
if (is_numeric($params['campaign_id'])) {
|
||||
$params['campaign_id'] = intval($params['campaign_id']);
|
||||
}
|
||||
else {
|
||||
throw new CRM_Core_Exception(
|
||||
E::ts('campaign_id must be a numeric string. '),
|
||||
'invalid_format'
|
||||
);
|
||||
}
|
||||
// Check whether given campaign_id exists and if not, unset the parameter.
|
||||
try {
|
||||
civicrm_api3(
|
||||
'Campaign',
|
||||
'getsingle',
|
||||
['id' => $params['campaign_id']]
|
||||
);
|
||||
}
|
||||
catch (CRM_Core_Exception $e) {
|
||||
unset($params['campaign_id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -107,23 +184,41 @@ class CRM_Twingle_Submission {
|
|||
*
|
||||
* @param string $contact_type
|
||||
* The contact type to look for/to create.
|
||||
* @param array $contact_data
|
||||
* @param array<string, mixed> $contact_data
|
||||
* Data to use for contact lookup/to create a contact with.
|
||||
* @param CRM_Twingle_Profile $profile
|
||||
* Profile used for this process
|
||||
* @param array<string, mixed> $submission
|
||||
* Submission data
|
||||
*
|
||||
* @return int|NULL
|
||||
* The ID of the matching/created contact, or NULL if no matching contact
|
||||
* was found and no new contact could be created.
|
||||
* @throws \CiviCRM_API3_Exception
|
||||
* @throws \CRM_Core_Exception
|
||||
* When invalid data was given.
|
||||
*/
|
||||
public static function getContact($contact_type, $contact_data) {
|
||||
public static function getContact(
|
||||
string $contact_type,
|
||||
array $contact_data,
|
||||
CRM_Twingle_Profile $profile,
|
||||
array $submission = []
|
||||
) {
|
||||
// If no parameters are given, do nothing.
|
||||
if (empty($contact_data)) {
|
||||
if ([] === $contact_data) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// add xcm profile
|
||||
$xcm_profile = $profile->getAttribute('xcm_profile');
|
||||
if (isset($xcm_profile) && '' !== $xcm_profile) {
|
||||
$contact_data['xcm_profile'] = $xcm_profile;
|
||||
}
|
||||
|
||||
// add campaign, see issue #17
|
||||
CRM_Twingle_Submission::setCampaign($contact_data, 'contact', $submission, $profile);
|
||||
|
||||
// Prepare values: country.
|
||||
if (!empty($contact_data['country'])) {
|
||||
if (isset($contact_data['country'])) {
|
||||
if (is_numeric($contact_data['country'])) {
|
||||
// If a country ID is given, update the parameters.
|
||||
$contact_data['country_id'] = $contact_data['country'];
|
||||
|
@ -131,65 +226,71 @@ class CRM_Twingle_Submission {
|
|||
}
|
||||
else {
|
||||
// Look up the country depending on the given ISO code.
|
||||
$country = civicrm_api3('Country', 'get', array('iso_code' => $contact_data['country']));
|
||||
if (!empty($country['id'])) {
|
||||
$country = civicrm_api3('Country', 'get', ['iso_code' => $contact_data['country']]);
|
||||
if (isset($country['id'])) {
|
||||
$contact_data['country_id'] = $country['id'];
|
||||
unset($contact_data['country']);
|
||||
}
|
||||
else {
|
||||
throw new \CiviCRM_API3_Exception(
|
||||
E::ts('Unknown country %1.', array(1 => $contact_data['country'])),
|
||||
throw new \CRM_Core_Exception(
|
||||
E::ts('Unknown country %1.', [1 => $contact_data['country']]),
|
||||
'invalid_format'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare values: language.
|
||||
if (is_string($contact_data['preferred_language']) && '' !== $contact_data['preferred_language']) {
|
||||
$mapping = CRM_Core_I18n_PseudoConstant::longForShortMapping();
|
||||
// Override the default mapping for German.
|
||||
$mapping['de'] = 'de_DE';
|
||||
$contact_data['preferred_language'] = $mapping[$contact_data['preferred_language']];
|
||||
}
|
||||
|
||||
// Pass to XCM.
|
||||
$contact_data['contact_type'] = $contact_type;
|
||||
$contact = civicrm_api3('Contact', 'getorcreate', $contact_data);
|
||||
if (empty($contact['id'])) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return $contact['id'];
|
||||
return isset($contact['id']) ? (int) $contact['id'] : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shares an organisation's work address, unless the contact already has one.
|
||||
*
|
||||
* @param $contact_id
|
||||
* @param int $contact_id
|
||||
* The ID of the contact to share the organisation address with.
|
||||
* @param $organisation_id
|
||||
* @param int $organisation_id
|
||||
* The ID of the organisation whose address to share with the contact.
|
||||
* @param $location_type_id
|
||||
* @param int $location_type_id
|
||||
* The ID of the location type to use for address lookup.
|
||||
*
|
||||
* @return boolean
|
||||
* Whether the organisation address has been shared with the contact.
|
||||
*
|
||||
* @throws \CiviCRM_API3_Exception
|
||||
* @throws \CRM_Core_Exception
|
||||
* When looking up or creating the shared address failed.
|
||||
*/
|
||||
public static function shareWorkAddress($contact_id, $organisation_id, $location_type_id = self::LOCATION_TYPE_ID_WORK) {
|
||||
if (empty($organisation_id)) {
|
||||
// Only if organisation exists.
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
public static function shareWorkAddress(
|
||||
int $contact_id,
|
||||
int $organisation_id,
|
||||
int $location_type_id = self::LOCATION_TYPE_ID_WORK
|
||||
) {
|
||||
// Check whether organisation has a WORK address.
|
||||
$existing_org_addresses = civicrm_api3('Address', 'get', array(
|
||||
$existing_org_addresses = civicrm_api3('Address', 'get', [
|
||||
'contact_id' => $organisation_id,
|
||||
'location_type_id' => $location_type_id));
|
||||
'location_type_id' => $location_type_id,
|
||||
]);
|
||||
if ($existing_org_addresses['count'] <= 0) {
|
||||
// Organisation does not have a WORK address.
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Check whether contact already has a WORK address.
|
||||
$existing_contact_addresses = civicrm_api3('Address', 'get', array(
|
||||
$existing_contact_addresses = civicrm_api3('Address', 'get', [
|
||||
'contact_id' => $contact_id,
|
||||
'location_type_id' => $location_type_id));
|
||||
'location_type_id' => $location_type_id,
|
||||
]);
|
||||
if ($existing_contact_addresses['count'] > 0) {
|
||||
// Contact already has a WORK address.
|
||||
return FALSE;
|
||||
|
@ -213,29 +314,25 @@ class CRM_Twingle_Submission {
|
|||
* @param int $organisation_id
|
||||
* The ID of the employer contact.
|
||||
*
|
||||
* @throws \CiviCRM_API3_Exception
|
||||
* @throws \CRM_Core_Exception
|
||||
*/
|
||||
public static function updateEmployerRelation($contact_id, $organisation_id) {
|
||||
if (empty($contact_id) || empty($organisation_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
public static function updateEmployerRelation(int $contact_id, int $organisation_id): void {
|
||||
// see if there is already one
|
||||
$existing_relationship = civicrm_api3('Relationship', 'get', array(
|
||||
$existing_relationship = civicrm_api3('Relationship', 'get', [
|
||||
'relationship_type_id' => self::EMPLOYER_RELATIONSHIP_TYPE_ID,
|
||||
'contact_id_a' => $contact_id,
|
||||
'contact_id_b' => $organisation_id,
|
||||
'is_active' => 1,
|
||||
));
|
||||
]);
|
||||
|
||||
if ($existing_relationship['count'] == 0) {
|
||||
// There is currently no (active) relationship between these contacts.
|
||||
$new_relationship_data = array(
|
||||
$new_relationship_data = [
|
||||
'relationship_type_id' => self::EMPLOYER_RELATIONSHIP_TYPE_ID,
|
||||
'contact_id_a' => $contact_id,
|
||||
'contact_id_b' => $organisation_id,
|
||||
'is_active' => 1,
|
||||
);
|
||||
];
|
||||
|
||||
civicrm_api3('Relationship', 'create', $new_relationship_data);
|
||||
}
|
||||
|
@ -246,19 +343,15 @@ class CRM_Twingle_Submission {
|
|||
* functionality is activated within the Twingle extension settings.
|
||||
*
|
||||
* @return bool
|
||||
* @throws \CiviCRM_API3_Exception
|
||||
* @throws \CRM_Core_Exception
|
||||
*/
|
||||
public static function civiSepaEnabled() {
|
||||
$sepa_extension = civicrm_api3('Extension', 'get', array(
|
||||
$sepa_extension = civicrm_api3('Extension', 'get', [
|
||||
'full_name' => 'org.project60.sepa',
|
||||
'is_active' => 1,
|
||||
));
|
||||
return
|
||||
CRM_Core_BAO_Setting::getItem(
|
||||
'de.systopia.twingle',
|
||||
'twingle_use_sepa'
|
||||
)
|
||||
&& $sepa_extension['count'];
|
||||
]);
|
||||
return (bool) Civi::settings()->get('twingle_use_sepa')
|
||||
&& $sepa_extension['count'] >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -269,30 +362,30 @@ class CRM_Twingle_Submission {
|
|||
* The submitted "donation_rhythm" paramter according to the API action
|
||||
* specification.
|
||||
*
|
||||
* @return array
|
||||
* @return array{'frequency_unit'?: string, 'frequency_interval'?: int}
|
||||
* An array with "frequency_unit" and "frequency_interval" keys, to be added
|
||||
* to contribution parameter arrays.
|
||||
*/
|
||||
public static function getFrequencyMapping($donation_rhythm) {
|
||||
$mapping = array(
|
||||
'halfyearly' => array(
|
||||
$mapping = [
|
||||
'halfyearly' => [
|
||||
'frequency_unit' => 'month',
|
||||
'frequency_interval' => 6,
|
||||
),
|
||||
'quarterly' => array(
|
||||
],
|
||||
'quarterly' => [
|
||||
'frequency_unit' => 'month',
|
||||
'frequency_interval' => 3,
|
||||
),
|
||||
'yearly' => array(
|
||||
],
|
||||
'yearly' => [
|
||||
'frequency_unit' => 'month',
|
||||
'frequency_interval' => 12,
|
||||
),
|
||||
'monthly' => array(
|
||||
],
|
||||
'monthly' => [
|
||||
'frequency_unit' => 'month',
|
||||
'frequency_interval' => 1,
|
||||
),
|
||||
'one_time' => array(),
|
||||
);
|
||||
],
|
||||
'one_time' => [],
|
||||
];
|
||||
|
||||
return $mapping[$donation_rhythm];
|
||||
}
|
||||
|
@ -310,19 +403,190 @@ class CRM_Twingle_Submission {
|
|||
* @return int
|
||||
* The next possible day of this or the next month to start collecting.
|
||||
*/
|
||||
public static function getSEPACycleDay($start_date, $creditor_id) {
|
||||
$buffer_days = (int) CRM_Sepa_Logic_Settings::getSetting("pp_buffer_days");
|
||||
$frst_notice_days = (int) CRM_Sepa_Logic_Settings::getSetting("batching.FRST.notice", $creditor_id);
|
||||
$earliest_rcur_date = strtotime("$start_date + $frst_notice_days days + $buffer_days days");
|
||||
public static function getSEPACycleDay($start_date, $creditor_id): int {
|
||||
$buffer_days = (int) CRM_Sepa_Logic_Settings::getSetting('pp_buffer_days');
|
||||
$frst_notice_days = (int) CRM_Sepa_Logic_Settings::getSetting('batching.FRST.notice', $creditor_id);
|
||||
if (FALSE === ($earliest_rcur_date = strtotime("$start_date + $frst_notice_days days + $buffer_days days"))) {
|
||||
throw new BaseException(E::ts('Could not calculate SEPA cycle day from configuration.'));
|
||||
}
|
||||
|
||||
// Find the next cycle day
|
||||
$cycle_days = CRM_Sepa_Logic_Settings::getListSetting("cycledays", range(1, 28), $creditor_id);
|
||||
$cycle_days = CRM_Sepa_Logic_Settings::getListSetting('cycledays', range(1, 28), $creditor_id);
|
||||
$earliest_cycle_day = $earliest_rcur_date;
|
||||
while (!in_array(date('j', $earliest_cycle_day), $cycle_days)) {
|
||||
$earliest_cycle_day = strtotime("+ 1 day", $earliest_cycle_day);
|
||||
while (!in_array(date('j', $earliest_cycle_day), $cycle_days, TRUE)) {
|
||||
$earliest_cycle_day = strtotime('+ 1 day', $earliest_cycle_day);
|
||||
}
|
||||
|
||||
return date('j', $earliest_cycle_day);
|
||||
return (int) date('j', $earliest_cycle_day);
|
||||
}
|
||||
|
||||
/**
|
||||
* Will set the campaign_id to the entity_data set, if the
|
||||
* profile is configured to do so. In that case the campaign is taken
|
||||
* from the submission data. Should that be empty, the profile's default
|
||||
* campaign is used.
|
||||
*
|
||||
* @param array<string, mixed> $entity_data
|
||||
* the data set where the campaign_id should be set
|
||||
* @param string $context
|
||||
* defines the type of the entity_data: one of 'contribution', 'membership','mandate', 'recurring', 'contact'
|
||||
* @param array<string, mixed> $submission
|
||||
* the submitted data
|
||||
* @param CRM_Twingle_Profile $profile
|
||||
* the twingle profile used
|
||||
*/
|
||||
public static function setCampaign(
|
||||
array &$entity_data,
|
||||
string $context,
|
||||
array $submission,
|
||||
CRM_Twingle_Profile $profile
|
||||
): void {
|
||||
// first: make sure it's not set from other workflows
|
||||
unset($entity_data['campaign_id']);
|
||||
|
||||
// then: check if campaign should be set it this context
|
||||
$enabled_contexts = $profile->getAttribute('campaign_targets');
|
||||
if ($enabled_contexts === NULL || !is_array($enabled_contexts)) {
|
||||
// backward compatibility:
|
||||
$enabled_contexts = ['contribution', 'contact'];
|
||||
}
|
||||
if (in_array($context, $enabled_contexts, TRUE)) {
|
||||
// use the submitted campaign if set
|
||||
if (is_numeric($submission['campaign_id'])) {
|
||||
$entity_data['campaign_id'] = $submission['campaign_id'];
|
||||
}
|
||||
// otherwise use the profile's
|
||||
elseif (is_numeric($campaign = $profile->getAttribute('campaign'))) {
|
||||
$entity_data['campaign_id'] = $campaign;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $values
|
||||
* Processed data
|
||||
* @param $submission
|
||||
* Submission data
|
||||
* @param $profile
|
||||
* The twingle profile used
|
||||
*
|
||||
* @throws \CiviCRM_API3_Exception
|
||||
* @throws \CRM_Core_Exception
|
||||
* @throws \Civi\Twingle\Shop\Exceptions\LineItemException
|
||||
*/
|
||||
public static function createLineItems($values, $submission, $profile): array {
|
||||
$line_items = [];
|
||||
$sum_line_items = 0;
|
||||
|
||||
$contribution_id = $values['contribution']['id'];
|
||||
if (empty($contribution_id)) {
|
||||
throw new LineItemException(
|
||||
"Could not find contribution id for line item assignment.",
|
||||
LineItemException::ERROR_CODE_CONTRIBUTION_NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($submission['products'] as $product) {
|
||||
|
||||
$line_item_data = [
|
||||
'entity_table' => "civicrm_contribution",
|
||||
'contribution_id' => $contribution_id,
|
||||
'entity_id' => $contribution_id,
|
||||
'label' => $product['name'],
|
||||
'qty' => $product['count'],
|
||||
'unit_price' => $product['price'],
|
||||
'line_total' => $product['total_value'],
|
||||
'sequential' => 1,
|
||||
];
|
||||
|
||||
// Try to find the TwingleProduct with its corresponding PriceField
|
||||
// for this product
|
||||
try {
|
||||
$price_field = CRM_Twingle_BAO_TwingleProduct::findByExternalId($product['id']);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
Civi::log()->error(E::LONG_NAME .
|
||||
": An error occurred when searching for TwingleShop with the external ID " .
|
||||
$product['id'], ['exception' => $e]);
|
||||
$price_field = NULL;
|
||||
}
|
||||
// If found, use the financial type and price field id from the price field
|
||||
if ($price_field) {
|
||||
|
||||
// Log warning if price is not variable and differs from the submission
|
||||
if ($price_field->price !== Null && $price_field->price != (int) $product['price']) {
|
||||
Civi::log()->warning(E::LONG_NAME .
|
||||
": Price for product " . $product['name'] . " differs from the PriceField. " .
|
||||
"Using the price from the submission.", ['price_field' => $price_field->price, 'submission' => $product['price']]);
|
||||
}
|
||||
|
||||
// Log warning if name differs from the submission
|
||||
if ($price_field->name != $product['name']) {
|
||||
Civi::log()->warning(E::LONG_NAME .
|
||||
": Name for product " . $product['name'] . " differs from the PriceField " .
|
||||
"Using the name from the submission.", ['price_field' => $price_field->name, 'submission' => $product['name']]);
|
||||
}
|
||||
|
||||
// Set the financial type and price field id
|
||||
$line_item_data['financial_type_id'] = $price_field->financial_type_id;
|
||||
$line_item_data['price_field_value_id'] = $price_field->getPriceFieldValueId();
|
||||
$line_item_data['price_field_id'] = $price_field->price_field_id;
|
||||
$line_item_data['description'] = $price_field->description;
|
||||
}
|
||||
// If not found, use the shops default financial type
|
||||
else {
|
||||
$financial_type_id = $profile->getAttribute('shop_financial_type', 1);
|
||||
$line_item_data['financial_type_id'] = $financial_type_id;
|
||||
}
|
||||
|
||||
// Create the line item
|
||||
$line_item = civicrm_api3('LineItem', 'create', $line_item_data);
|
||||
|
||||
if (!empty($line_item['is_error'])) {
|
||||
$line_item_name = $line_item_data['name'];
|
||||
throw new CiviCRM_API3_Exception(
|
||||
E::ts("Could not create line item for product '%1'", [1 => $line_item_name]),
|
||||
'api_error'
|
||||
);
|
||||
}
|
||||
$line_items[] = array_pop($line_item['values']);
|
||||
|
||||
$sum_line_items += $product['total_value'];
|
||||
}
|
||||
|
||||
// Create line item for donation part
|
||||
$donation_sum = (float) $values['contribution']['total_amount'] - $sum_line_items;
|
||||
if ($donation_sum > 0) {
|
||||
$donation_financial_type_id = $profile->getAttribute('shop_donation_financial_type', 1);
|
||||
$donation_label = civicrm_api3('FinancialType', 'getsingle', [
|
||||
'return' => ['name'],
|
||||
'id' => $donation_financial_type_id,
|
||||
])['name'];
|
||||
|
||||
$donation_line_item_data = [
|
||||
'entity_table' => "civicrm_contribution",
|
||||
'contribution_id' => $contribution_id,
|
||||
'entity_id' => $contribution_id,
|
||||
'label' => $donation_label,
|
||||
'qty' => 1,
|
||||
'unit_price' => $donation_sum,
|
||||
'line_total' => $donation_sum,
|
||||
'financial_type_id' => $donation_financial_type_id,
|
||||
'sequential' => 1,
|
||||
];
|
||||
|
||||
$donation_line_item = civicrm_api3('LineItem', 'create', $donation_line_item_data);
|
||||
|
||||
if (!empty($donation_line_item['is_error'])) {
|
||||
throw new CiviCRM_API3_Exception(
|
||||
E::ts("Could not create line item for donation"),
|
||||
'api_error'
|
||||
);
|
||||
}
|
||||
|
||||
$line_items[] = array_pop($donation_line_item['values']);
|
||||
}
|
||||
|
||||
return $line_items;
|
||||
}
|
||||
}
|
||||
|
|
254
CRM/Twingle/Tools.php
Normal file
|
@ -0,0 +1,254 @@
|
|||
<?php
|
||||
/*------------------------------------------------------------+
|
||||
| SYSTOPIA Twingle Integration |
|
||||
| Copyright (C) 2019 SYSTOPIA |
|
||||
| Author: B. Endres (endres@systopia.de) |
|
||||
+-------------------------------------------------------------+
|
||||
| This program is released as free software under the |
|
||||
| Affero GPL license. You can redistribute it and/or |
|
||||
| modify it under the terms of this license which you |
|
||||
| can read by viewing the included agpl.txt or online |
|
||||
| at www.gnu.org/licenses/agpl.html. Removal of this |
|
||||
| copyright header is strictly prohibited without |
|
||||
| written permission from the original author(s). |
|
||||
+-------------------------------------------------------------*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
use CRM_Twingle_ExtensionUtil as E;
|
||||
use Civi\Twingle\Exceptions\BaseException;
|
||||
|
||||
class CRM_Twingle_Tools {
|
||||
|
||||
/**
|
||||
* This flag can be used to temporarily suspend twingle protection
|
||||
* @var bool
|
||||
*/
|
||||
public static $protection_suspended = FALSE;
|
||||
|
||||
/**
|
||||
* Check if the attempted modification of the recurring contribution is allowed.
|
||||
* If not, an exception will be raised
|
||||
*
|
||||
* @param int $recurring_contribution_id
|
||||
* @param array<mixed> $change
|
||||
* @throws Exception if the change is not allowed
|
||||
*/
|
||||
public static function checkRecurringContributionChange(int $recurring_contribution_id, array $change): void {
|
||||
// check if a change to the status is planned
|
||||
if (empty($change['contribution_status_id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the target status is not closed
|
||||
if (in_array($change['contribution_status_id'], [2, 5])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if we're suspended
|
||||
if (self::$protection_suspended) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if protection is turned on
|
||||
$protection_on = Civi::settings()->get('twingle_protect_recurring');
|
||||
if (empty($protection_on)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// load the recurring contribution
|
||||
$recurring_contribution = civicrm_api3('ContributionRecur', 'getsingle', [
|
||||
'return' => 'trxn_id,contribution_status_id,payment_instrument_id,contact_id',
|
||||
'id' => $recurring_contribution_id,
|
||||
]);
|
||||
|
||||
// check if this is a SEPA transaction (doesn't concern us)
|
||||
if (self::isSDD($recurring_contribution['payment_instrument_id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// see if this recurring contribution is from Twingle
|
||||
if (!self::isTwingleRecurringContribution($recurring_contribution_id, $recurring_contribution)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if it's really a termination (i.e. current status is 2 or 5)
|
||||
if (!in_array($recurring_contribution['contribution_status_id'], [2, 5])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this _IS_ on of the cases where we should step in:
|
||||
CRM_Twingle_Tools::processRecurringContributionTermination(
|
||||
$recurring_contribution_id,
|
||||
$recurring_contribution
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $recurring_contribution_id int recurring contribution ID to check
|
||||
* @param $recurring_contribution array recurring contribution data, optional
|
||||
* @return bool|null true, false or null if can't be determined
|
||||
* @throws \CRM_Core_Exception
|
||||
*/
|
||||
public static function isTwingleRecurringContribution($recurring_contribution_id, $recurring_contribution = NULL) {
|
||||
// this currently only works with prefixes
|
||||
$prefix = Civi::settings()->get('twingle_prefix');
|
||||
if (empty($prefix)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// load recurring contribution if necessary
|
||||
if (empty($recurring_contribution['trxn_id'])) {
|
||||
$recurring_contribution = civicrm_api3('ContributionRecur', 'getsingle', ['id' => $recurring_contribution_id]);
|
||||
}
|
||||
|
||||
// check if it's a Twingle contribution by checking the prefix
|
||||
// fixme: better ways (e.g. tags) should be used to mark twingle contributions
|
||||
return (substr($recurring_contribution['trxn_id'], 0, strlen($prefix)) == $prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the recurring contribution protection
|
||||
*
|
||||
* @param int $recurring_contribution_id
|
||||
* Recurring contribution ID.
|
||||
* @param array<mixed> $recurring_contribution
|
||||
* Recurring contribution fields.
|
||||
* @throws Exception could be one of the measures
|
||||
*/
|
||||
public static function processRecurringContributionTermination(
|
||||
int $recurring_contribution_id,
|
||||
array $recurring_contribution
|
||||
) {
|
||||
// check if we're suspended
|
||||
if (self::$protection_suspended) {
|
||||
return;
|
||||
}
|
||||
|
||||
$protection_mode = Civi::settings()->get('twingle_protect_recurring');
|
||||
switch ($protection_mode) {
|
||||
case CRM_Twingle_Config::RCUR_PROTECTION_OFF:
|
||||
// do nothing
|
||||
break;
|
||||
|
||||
case CRM_Twingle_Config::RCUR_PROTECTION_EXCEPTION:
|
||||
// phpcs:disable Generic.Files.LineLength.TooLong
|
||||
throw new BaseException(E::ts(
|
||||
'This is a Twingle recurring contribution. It should be terminated through the Twingle interface, otherwise it will still be collected.'
|
||||
));
|
||||
|
||||
// phpcs:enable
|
||||
|
||||
case CRM_Twingle_Config::RCUR_PROTECTION_ACTIVITY:
|
||||
// create contact source activity
|
||||
// first: get the contact ID
|
||||
if (!empty($recurring_contribution['contact_id'])) {
|
||||
$target_id = (int) $recurring_contribution['contact_id'];
|
||||
}
|
||||
else {
|
||||
$target_id = (int) civicrm_api3('ContributionRecur', 'getvalue', [
|
||||
'id' => $recurring_contribution_id,
|
||||
'return' => 'contact_id',
|
||||
]);
|
||||
}
|
||||
if (!empty($recurring_contribution['trxn_id'])) {
|
||||
$trxn_id = $recurring_contribution['trxn_id'];
|
||||
}
|
||||
else {
|
||||
$trxn_id = civicrm_api3('ContributionRecur', 'getvalue', [
|
||||
'id' => $recurring_contribution_id,
|
||||
'return' => 'trxn_id',
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
civicrm_api3('Activity', 'create', [
|
||||
'activity_type_id' => Civi::settings()->get('twingle_protect_recurring_activity_type'),
|
||||
'subject' => Civi::settings()->get('twingle_protect_recurring_activity_subject'),
|
||||
'activity_date_time' => date('YmdHis'),
|
||||
'target_id' => $target_id,
|
||||
'assignee_id' => Civi::settings()->get('twingle_protect_recurring_activity_assignee'),
|
||||
'status_id' => Civi::settings()->get('twingle_protect_recurring_activity_status'),
|
||||
// phpcs:disable Generic.Files.LineLength.TooLong
|
||||
'details' => E::ts(
|
||||
"Recurring contribution [%1] (Transaction ID '%2') was terminated by a user. You need to end the corresponding record in Twingle as well, or it will still be collected.",
|
||||
[1 => $recurring_contribution_id, 2 => $trxn_id]
|
||||
),
|
||||
// phpcs:enable
|
||||
'source_contact_id' => CRM_Core_Session::getLoggedInContactID(),
|
||||
]);
|
||||
}
|
||||
catch (Exception $ex) {
|
||||
Civi::log()->warning("TwingleAPI: Couldn't create recurring protection activity: " . $ex->getMessage());
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
Civi::log()->warning("TwingleAPI: Unknown recurring contribution protection mode: '{$protection_mode}'");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given payment instrument is SEPA
|
||||
*
|
||||
* @param string $payment_instrument_id
|
||||
* @return boolean
|
||||
*/
|
||||
public static function isSDD(string $payment_instrument_id) {
|
||||
static $sepa_payment_instruments = NULL;
|
||||
if ($sepa_payment_instruments === NULL) {
|
||||
// init with instrument names
|
||||
$sepa_payment_instruments = ['FRST', 'RCUR', 'OOFF'];
|
||||
|
||||
// lookup and add instrument IDs
|
||||
$lookup = civicrm_api3('OptionValue', 'get', [
|
||||
'option_group_id' => 'payment_instrument',
|
||||
'name' => ['IN' => $sepa_payment_instruments],
|
||||
'return' => 'value',
|
||||
]);
|
||||
foreach ($lookup['values'] as $payment_instrument) {
|
||||
$sepa_payment_instruments[] = $payment_instrument['value'];
|
||||
}
|
||||
}
|
||||
return in_array($payment_instrument_id, $sepa_payment_instruments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a CiviSEPA mandate for the given contribution ID
|
||||
*
|
||||
* @param int $contribution_id contribution ID *or* recurring contribution ID
|
||||
* @return array<string, mixed>|null mandate or null
|
||||
*/
|
||||
public static function getMandateFor(int $contribution_id): ?array {
|
||||
if ($contribution_id) {
|
||||
try {
|
||||
// try recurring mandate
|
||||
$rcur_mandate = civicrm_api3('SepaMandate', 'get', [
|
||||
'entity_id' => $contribution_id,
|
||||
'entity_table' => 'civicrm_contribution_recur',
|
||||
'type' => 'RCUR',
|
||||
]);
|
||||
if ($rcur_mandate['count'] == 1) {
|
||||
return reset($rcur_mandate['values']);
|
||||
}
|
||||
|
||||
// try OOFF mandate
|
||||
// try recurring mandate
|
||||
$ooff_mandate = civicrm_api3('SepaMandate', 'get', [
|
||||
'entity_id' => $contribution_id,
|
||||
'entity_table' => 'civicrm_contribution',
|
||||
'type' => 'OOFF',
|
||||
]);
|
||||
if ($ooff_mandate['count'] == 1) {
|
||||
return reset($ooff_mandate['values']);
|
||||
}
|
||||
}
|
||||
catch (Exception $ex) {
|
||||
Civi::log()->warning("CRM_Twingle_Tools::getMandate failed for [{$contribution_id}]: " . $ex->getMessage());
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
}
|
151
CRM/Twingle/Upgrader.php
Normal file
|
@ -0,0 +1,151 @@
|
|||
<?php
|
||||
/*------------------------------------------------------------+
|
||||
| SYSTOPIA Twingle Integration |
|
||||
| Copyright (C) 2019 SYSTOPIA |
|
||||
| Author: J. Schuppe (schuppe@systopia.de) |
|
||||
+-------------------------------------------------------------+
|
||||
| This program is released as free software under the |
|
||||
| Affero GPL license. You can redistribute it and/or |
|
||||
| modify it under the terms of this license which you |
|
||||
| can read by viewing the included agpl.txt or online |
|
||||
| at www.gnu.org/licenses/agpl.html. Removal of this |
|
||||
| copyright header is strictly prohibited without |
|
||||
| written permission from the original author(s). |
|
||||
+-------------------------------------------------------------*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
use CRM_Twingle_ExtensionUtil as E;
|
||||
|
||||
/**
|
||||
* Collection of upgrade steps.
|
||||
*/
|
||||
class CRM_Twingle_Upgrader extends CRM_Extension_Upgrader_Base {
|
||||
|
||||
/**
|
||||
* Installer script
|
||||
*/
|
||||
public function install(): void {
|
||||
// create a DB table for the twingle profiles
|
||||
$this->executeSqlFile('sql/civicrm_twingle_profile.sql');
|
||||
|
||||
// add a default profile
|
||||
CRM_Twingle_Profile::createDefaultProfile()->saveProfile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: Run an external SQL script when the module is uninstalled.
|
||||
*
|
||||
* public function uninstall() {
|
||||
* $this->executeSqlFile('sql/myuninstall.sql');
|
||||
* }
|
||||
*
|
||||
* /**
|
||||
* Copy financial_type_id setting to new setting financial_type_id_recur.
|
||||
*/
|
||||
public function upgrade_4000(): bool {
|
||||
$this->ctx->log->info('Applying update 4000: Copying Financial type to new setting Financial type (recurring).');
|
||||
foreach (CRM_Twingle_Profile::getProfiles() as $profile) {
|
||||
$profile->setAttribute('financial_type_id_recur', $profile->getAttribute('financial_type_id'));
|
||||
$profile->saveProfile();
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert serialized settings from objects to arrays.
|
||||
*
|
||||
* @link https://civicrm.org/advisory/civi-sa-2019-21-poi-saved-search-and-report-instance-apis
|
||||
*/
|
||||
public function upgrade_5011(): bool {
|
||||
// Do not use CRM_Core_BAO::getItem() or Civi::settings()->get().
|
||||
// Extract and unserialize directly from the database.
|
||||
$twingle_profiles_query = CRM_Core_DAO::executeQuery("
|
||||
SELECT `value`
|
||||
FROM `civicrm_setting`
|
||||
WHERE `name` = 'twingle_profiles';");
|
||||
if ($twingle_profiles_query->fetch()) {
|
||||
$profiles = unserialize($twingle_profiles_query->value);
|
||||
Civi::settings()->set('twingle_profiles', (array) $profiles);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrading to 1.4.0 needs to convert the profiles into the new infrastructure
|
||||
*
|
||||
* @return TRUE on success
|
||||
* @throws Exception
|
||||
*/
|
||||
public function upgrade_5140(): bool {
|
||||
$this->ctx->log->info('Converting twingle profiles.');
|
||||
|
||||
// create a DB table for the twingle profiles
|
||||
$this->executeSqlFile('sql/civicrm_twingle_profile.sql');
|
||||
|
||||
// migrate the current profiles
|
||||
if (is_array($profiles_data = Civi::settings()->get('twingle_profiles'))) {
|
||||
foreach ($profiles_data as $profile_name => $profile_data) {
|
||||
$profile = new CRM_Twingle_Profile($profile_name, $profile_data);
|
||||
$data = json_encode($profile->getData());
|
||||
CRM_Core_DAO::executeQuery(<<<SQL
|
||||
INSERT IGNORE INTO civicrm_twingle_profile(name,config,last_access,access_counter) VALUES (%1, %2, NOW(), 0)
|
||||
SQL,
|
||||
[
|
||||
1 => [$profile_name, 'String'],
|
||||
2 => [$data, 'String'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade to 1.5.0
|
||||
*
|
||||
* - Activate mapping of `purpose` and `user_extra_field` to notes in each existing profile to
|
||||
* maintain default behavior after making the fields optional.
|
||||
*
|
||||
* @return bool
|
||||
* @throws \Civi\Core\Exception\DBQueryException
|
||||
* @throws \Civi\Twingle\Exceptions\ProfileException
|
||||
*/
|
||||
public function upgrade_5150(): bool {
|
||||
$this->ctx->log->info('Activate mapping of `purpose` and `user_extra_field` to notes in each existing profile.');
|
||||
|
||||
foreach (CRM_Twingle_Profile::getProfiles() as $profile) {
|
||||
$profile_changed = FALSE;
|
||||
/** @phpstan-var array<string> $contribution_notes */
|
||||
$contribution_notes = $profile->getAttribute('map_as_contribution_notes', []);
|
||||
/** @phpstan-var array<string> $contact_notes */
|
||||
$contact_notes = $profile->getAttribute('map_as_contact_notes', []);
|
||||
if (!in_array('purpose', $contribution_notes, TRUE)) {
|
||||
$profile->setAttribute('map_as_contribution_notes', array_merge($contribution_notes, ['purpose']));
|
||||
$profile_changed = TRUE;
|
||||
}
|
||||
if (!in_array('user_extrafield', $contact_notes, TRUE)) {
|
||||
$profile->setAttribute('map_as_contact_notes', array_merge($contact_notes, ['user_extrafield']));
|
||||
$profile_changed = TRUE;
|
||||
}
|
||||
if ($profile_changed) {
|
||||
$profile->saveProfile();
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Upgrade to 1.5.1 creates the tables civicrm_twingle_product and
|
||||
* civicrm_twingle_shop.
|
||||
*
|
||||
* @return TRUE on success
|
||||
*/
|
||||
public function upgrade_5151() {
|
||||
$this->ctx->log->info('Creating tables for Twingle Shop.');
|
||||
$this->executeSqlFile('sql/civicrm_twingle_shop.sql');
|
||||
return TRUE;
|
||||
}
|
||||
}
|
12
Civi/Api4/TwingleProduct.php
Normal 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
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
namespace Civi\Api4;
|
||||
|
||||
/**
|
||||
* TwingleShop entity.
|
||||
*
|
||||
* @package Civi\Api4
|
||||
*/
|
||||
class TwingleShop extends Generic\DAOEntity {
|
||||
|
||||
}
|
65
Civi/Twingle/Exceptions/BaseException.php
Normal file
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
/*
|
||||
* Copyright (C) 2024 SYSTOPIA GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation in version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Civi\Twingle\Exceptions;
|
||||
|
||||
use CRM_Twingle_ExtensionUtil as E;
|
||||
|
||||
/**
|
||||
* A simple custom exception class that indicates a problem within a class
|
||||
* of the Twingle API extension.
|
||||
*/
|
||||
class BaseException extends \Exception {
|
||||
|
||||
/**
|
||||
* @var int|string
|
||||
*/
|
||||
protected $code;
|
||||
protected string $log_message;
|
||||
|
||||
/**
|
||||
* BaseException Constructor
|
||||
* @param string $message
|
||||
* Error message
|
||||
* @param string $error_code
|
||||
* A meaningful error code
|
||||
*/
|
||||
public function __construct(string $message = '', string $error_code = '') {
|
||||
parent::__construct($message, 1);
|
||||
$this->log_message = !empty($message) ? E::LONG_NAME . ': ' . $message : '';
|
||||
$this->code = $error_code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error message, but with the extension name prefixed.
|
||||
* @return string
|
||||
*/
|
||||
public function getLogMessage() {
|
||||
return $this->log_message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error code.
|
||||
* @return string
|
||||
*/
|
||||
public function getErrorCode() {
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
}
|
35
Civi/Twingle/Exceptions/ProfileException.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
/*
|
||||
* Copyright (C) 2024 SYSTOPIA GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation in version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Civi\Twingle\Exceptions;
|
||||
|
||||
/**
|
||||
* A simple custom exception that indicates a problem within the
|
||||
* CRM_Twingle_Profile class
|
||||
*/
|
||||
class ProfileException extends BaseException {
|
||||
|
||||
public const ERROR_CODE_PROFILE_NOT_FOUND = 'profile_not_found';
|
||||
public const ERROR_CODE_DEFAULT_PROFILE_NOT_FOUND = 'default_profile_not_found';
|
||||
public const ERROR_CODE_COULD_NOT_SAVE_PROFILE = 'could_not_save_profile';
|
||||
public const ERROR_CODE_COULD_NOT_RESET_PROFILE = 'could_not_reset_profile';
|
||||
public const ERROR_CODE_COULD_NOT_DELETE_PROFILE = 'could_not_delete_profile';
|
||||
public const ERROR_CODE_UNKNOWN_PROFILE_ATTRIBUTE = 'unknown_profile_attribute';
|
||||
|
||||
}
|
54
Civi/Twingle/Exceptions/ProfileValidationError.php
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
/*
|
||||
* Copyright (C) 2024 SYSTOPIA GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation in version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Civi\Twingle\Exceptions;
|
||||
|
||||
/**
|
||||
* A simple custom error indicating a problem with the validation of the
|
||||
* CRM_Twingle_Profile
|
||||
*/
|
||||
class ProfileValidationError extends BaseException {
|
||||
|
||||
private string $affected_field_name;
|
||||
public const ERROR_CODE_PROFILE_VALIDATION_FAILED = 'profile_validation_failed';
|
||||
public const ERROR_CODE_PROFILE_VALIDATION_WARNING = 'profile_validation_warning';
|
||||
|
||||
/**
|
||||
* ProfileValidationError Constructor
|
||||
* @param string $affected_field_name
|
||||
* The name of the profile field which caused the exception
|
||||
* @param string $message
|
||||
* Error message
|
||||
* @param string $error_code
|
||||
* A meaningful error code
|
||||
*/
|
||||
public function __construct(string $affected_field_name, string $message = '', string $error_code = '') {
|
||||
parent::__construct($message, $error_code);
|
||||
$this->affected_field_name = $affected_field_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the profile field that caused the exception.
|
||||
* @return string
|
||||
*/
|
||||
public function getAffectedFieldName() {
|
||||
return $this->affected_field_name;
|
||||
}
|
||||
|
||||
}
|
269
Civi/Twingle/Shop/ApiCall.php
Normal 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);
|
||||
}
|
||||
}
|
19
Civi/Twingle/Shop/Exceptions/ApiCallError.php
Normal 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";
|
||||
|
||||
}
|
14
Civi/Twingle/Shop/Exceptions/LineItemException.php
Normal 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";
|
||||
|
||||
}
|
26
Civi/Twingle/Shop/Exceptions/ProductException.php
Normal 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';
|
||||
}
|
22
Civi/Twingle/Shop/Exceptions/ShopException.php
Normal 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";
|
||||
|
||||
}
|
154
Civi/Twingle/Shop/Utils/TwingleShopUtils.php
Normal 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."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
139
README.md
|
@ -1,134 +1,15 @@
|
|||
# Twingle API
|
||||
|
||||
Extension to connect to the Twingle fundraising service via its API.
|
||||
This extension integrates [Twingle donation and membership forms](https://www.twingle.de/) with CiviCRM.
|
||||
|
||||
* [About Twingle](https://www.twingle.de/)
|
||||
You can read the full documentation, including installation and configuration instructions and API specification, [here](https://docs.civicrm.org/twingle/en/latest/).
|
||||
|
||||
The extension is licensed under
|
||||
[AGPL-3.0](https://github.com/systopia/de.systopia.twingle/blob/master/LICENSE.txt).
|
||||
## We need your support
|
||||
This CiviCRM extension is provided as Free and Open Source Software,
|
||||
and we are happy if you find it useful. However, we have put a lot of work into it
|
||||
(and continue to do so), much of it unpaid for. So if you benefit from our software,
|
||||
please consider making a financial contribution so we can continue to maintain and develop it further.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Configure Twingle
|
||||
|
||||
*This section is yet to be completed.*
|
||||
|
||||
### Configure CiviCRM
|
||||
|
||||
- Go to the Administration console `/civicrm/admin`
|
||||
- Open "Twingle API Configuration" at `/civicrm/admin/settings/twingle`
|
||||
|
||||
#### Configure CiviSEPA integration
|
||||
|
||||
Open "Configure extension settings" at
|
||||
`/civicrm/admin/settings/twingle/settings` and configure whether to integrate
|
||||
with the [CiviSEPA](https://github.com/project60/org.project60.sepa) extension.
|
||||
|
||||
This enables you to map incoming donations from Twingle with a specific payment
|
||||
method (e.g. *debit_manual*) to be processed with CiviSEPA, that is, creating a
|
||||
SEPA mandate and managing recurring payments.
|
||||
|
||||
#### Configure profiles
|
||||
|
||||
Open "Configure profiles" at `/civicrm/admin/settings/twingle/profiles`.
|
||||
|
||||
The *default* profile is used whenever the plugin cannot match the Twingle
|
||||
project ID from any other profile. Therefore the default profile will be used
|
||||
for all newly created Twingle projects.
|
||||
|
||||
| Label | Description |
|
||||
|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Profile name | Internal name, used inside the extension. |
|
||||
| Project IDs | Twingle project IDs. Separate multiple IDs with commas. |
|
||||
| Location type | Specify how the address data sent by the form should be categorised in CiviCRM. The list is based on your CiviCRM configuration. |
|
||||
| Financial type | Specify which financial type incoming donations should be recorded with in CiviCRM. The list is based on your CiviCRM configuration. |
|
||||
| Gender options | Specify which CiviCRM gender option the incoming Twingle gender value should be mapped to. The list is based on your CiviCRM configuration. |
|
||||
| Record *Payment method* as | Specifiy the payment methods mapping for incoming donations for each Twingle payment method. |
|
||||
| CiviSEPA creditor | When enabled to integrate with CiviSEPA, specify the CiviSEPA creditor to use. |
|
||||
| Sign up for groups | Whenever the donor checked the newsletter/postal mailing/donation receipt checkbox on the Twingle form, the contact will be added to the groups listed here. |
|
||||
|
||||
|
||||
## API documentation
|
||||
|
||||
The extension provides a new CiviCRM API entity `TwingleDonation` with API
|
||||
actions to record a new donation, end a previously submitted recurring donation
|
||||
and cancel previously submitted donation.
|
||||
|
||||
### Submit donation
|
||||
|
||||
This API action processes submitted Twingle donations and donor information.
|
||||
|
||||
- Entity: `TwingleDonation`
|
||||
- Action: `Submit`
|
||||
|
||||
The action accepts the following parameters:
|
||||
|
||||
| Parameter | Type | Description | Values/Format | Required |
|
||||
|----------------------------------------|---------|-------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------|
|
||||
| <nobr>`project_id`</nobr> | String | The Twingle project ID | | Yes |
|
||||
| <nobr>`trx_id`</nobr> | String | The unique transaction ID of the donation | A unique transaction ID for the donation. | Yes |
|
||||
| <nobr>`confirmed_at`</nobr> | String | The date when the donation was issued | A string representing a date in the format `YmdHis` | Yes |
|
||||
| <nobr>`purpose`</nobr> | String | The purpose of the donation | | |
|
||||
| <nobr>`amount`</nobr> | Integer | The donation amount in minor currency unit | | Yes |
|
||||
| <nobr>`currency`</nobr> | String | The ISO-4217 currency code of the donation | A valid ISO-4217 currency code | Yes |
|
||||
| <nobr>`newsletter`</nobr> | Boolean | Whether to subscribe the contact to the newsletter group defined in the profile | | |
|
||||
| <nobr>`postinfo`</nobr> | Boolean | Whether to subscribe the contact to the postal mailing group defined in the profile | | |
|
||||
| <nobr>`donation_receipt`</nobr> | Boolean | Whether the contact requested a donation receipt | | |
|
||||
| <nobr>`payment_method`</nobr> | String | The Twingle payment method used for the donation | One of:<br /><ul><li><nobr>`banktransfer`</nobr></li><li><nobr>`debit_manual`</nobr></li><li><nobr>`debit_automatic`</nobr></li><li><nobr>`creditcard`</nobr></li><li><nobr>`mobilephone_germany`</nobr></li><li><nobr>`paypal`</nobr></li><li><nobr>`sofortueberweisung`</nobr></li><li><nobr>`amazonpay`</nobr></li><li><nobr>`paydirekt`</nobr></li><li><nobr>`applepay`</nobr></li><li><nobr>`googlepay`</nobr></li></ul> | Yes |
|
||||
| <nobr>`donation_rhythm`</nobr> | String | The interval which the donation is recurring in | One of:<br /><ul><li><nobr>`'one_time',`</nobr></li><li><nobr>`'halfyearly',`</nobr></li><li><nobr>`'quarterly',`</nobr></li><li><nobr>`'yearly',`</nobr></li><li><nobr>`'monthly'`</nobr></li></ul> | Yes |
|
||||
| <nobr>`debit_iban`</nobr> | String | The IBAN for SEPA Direct Debit payments | A valid ISO 13616-1:2007 IBAN | Yes, if `payment_method` is `debit_manual` and CiviSEPA is used |
|
||||
| <nobr>`debit_bic`</nobr> | String | The BIC for SEPA Direct Debit payments | A valid ISO 9362 BIC | Yes, if `payment_method` is `debit_manual` and CiviSEPA is used |
|
||||
| <nobr>`debit_mandate_reference`</nobr> | String | The mandate reference for SEPA Direct Debit payments | | |
|
||||
| <nobr>`debit_account_holder`</nobr> | String | The account holder for SEPA Direct Debit payments | | |
|
||||
| <nobr>`is_anonymous`</nobr> | Boolean | Whether the donation is submitted anonymously | | |
|
||||
| <nobr>`user_gender`</nobr> | String | The gender of the contact | | |
|
||||
| <nobr>`user_birthdate`</nobr> | String | The date of birth of the contact | A string representing a date in the format `Ymd` | |
|
||||
| <nobr>`user_title`</nobr> | String | The formal title of the contact | | |
|
||||
| <nobr>`user_email`</nobr> | String | The e-mail address of the contact | A valid e-mail address | |
|
||||
| <nobr>`user_firstname`</nobr> | String | The first name of the contact | | |
|
||||
| <nobr>`user_lastname`</nobr> | String | The last name of the contact | | |
|
||||
| <nobr>`user_street`</nobr> | String | The street address of the contact | | |
|
||||
| <nobr>`user_postal_code`</nobr> | String | The postal code of the contact | | |
|
||||
| <nobr>`user_city`</nobr> | String | The city of the contact | | |
|
||||
| <nobr>`user_telephone`</nobr> | String | The telephone number of the contact | | |
|
||||
| <nobr>`user_company`</nobr> | String | The company of the contact | | |
|
||||
| <nobr>`user_extrafield`</nobr> | String | Additional information of the contact | | |
|
||||
|
||||
You may also refer to
|
||||
[the code](https://github.com/systopia/de.systopia.twingle/blob/master/api/v3/TwingleDonation/Submit.php)
|
||||
for more insight into this API action.
|
||||
|
||||
### End recurring donation
|
||||
|
||||
- Entity: `TwingleDonation`
|
||||
- Action: `Endrecurring`
|
||||
|
||||
The action accepts the following parameters:
|
||||
|
||||
| Parameter | Type | Description | Values/Format | Required |
|
||||
|---------------------------|---------|------------------------------------------------|-------------------------------------------------------|----------|
|
||||
| <nobr>`project_id`</nobr> | String | The Twingle project ID | | Yes |
|
||||
| <nobr>`trx_id`</nobr> | String | The unique transaction ID of the donation | A unique transaction ID for the donation. | Yes |
|
||||
| <nobr>`ended_at`</nobr> | Integer | The date when the recurring donation was ended | A string representing a date in the format `YmdHis` | Yes |
|
||||
|
||||
You may also refer to
|
||||
[the code](https://github.com/systopia/de.systopia.twingle/blob/master/api/v3/TwingleDonation/Endrecurring.php)
|
||||
for more insight into this API action.
|
||||
|
||||
### Cancel donation
|
||||
|
||||
- Entity: `TwingleDonation`
|
||||
- Action: `Cancel`
|
||||
|
||||
The action accepts the following parameters:
|
||||
|
||||
| Parameter | Type | Description | Values/Format | Required |
|
||||
|------------------------------|--------|----------------------------------------------------|-------------------------------------------------------|----------|
|
||||
| <nobr>`project_id`</nobr> | String | The Twingle project ID | | Yes |
|
||||
| <nobr>`trx_id`</nobr> | String | The unique transaction ID of the donation | A unique transaction ID for the donation. | Yes |
|
||||
| <nobr>`cancelled_at`</nobr> | String | The date when the recurring donation was cancelled | A string representing a date in the format `YmdHis` | Yes |
|
||||
| <nobr>`cancel_reason`</nobr> | String | The reason for the donation being cancelled | | Yes |
|
||||
|
||||
You may also refer to
|
||||
[the code](https://github.com/systopia/de.systopia.twingle/blob/master/api/v3/TwingleDonation/Cancel.php)
|
||||
for more insight into this API action.
|
||||
If you are willing to support us in developing this CiviCRM extension,
|
||||
please send an email to info@systopia.de to get an invoice or agree a different payment method.
|
||||
Thank you!
|
||||
|
|
|
@ -13,125 +13,147 @@
|
|||
| written permission from the original author(s). |
|
||||
+-------------------------------------------------------------*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
use CRM_Twingle_ExtensionUtil as E;
|
||||
|
||||
/**
|
||||
* TwingleDonation.Cancel API specification (optional)
|
||||
* This is used for documentation and validation.
|
||||
*
|
||||
* @param array $params description of fields supported by this API call
|
||||
* @param array<string, array<string, mixed>> $params description of fields supported by this API call
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @see http://wiki.civicrm.org/confluence/display/CRMDOC/API+Architecture+Standards
|
||||
*/
|
||||
function _civicrm_api3_twingle_donation_Cancel_spec(&$params) {
|
||||
$params['project_id'] = array(
|
||||
$params['project_id'] = [
|
||||
'name' => 'project_id',
|
||||
'title' => E::ts('Project ID'),
|
||||
'type' => CRM_Utils_Type::T_STRING,
|
||||
'api.required' => 1,
|
||||
'description' => E::ts('The Twingle project ID.'),
|
||||
);
|
||||
$params['trx_id'] = array(
|
||||
];
|
||||
$params['trx_id'] = [
|
||||
'name' => 'trx_id',
|
||||
'title' => E::ts('Transaction ID'),
|
||||
'type' => CRM_Utils_Type::T_STRING,
|
||||
'api.required' => 1,
|
||||
'description' => E::ts('The unique transaction ID of the donation'),
|
||||
);
|
||||
$params['cancelled_at'] = array(
|
||||
];
|
||||
$params['cancelled_at'] = [
|
||||
'name' => 'cancelled_at',
|
||||
'title' => E::ts('Cancelled at'),
|
||||
'type' => CRM_Utils_Type::T_INT,
|
||||
'api.required' => 1,
|
||||
'description' => E::ts('The date when the donation was cancelled, format: YmdHis.'),
|
||||
);
|
||||
$params['cancel_reason'] = array(
|
||||
];
|
||||
$params['cancel_reason'] = [
|
||||
'name' => 'cancel_reason',
|
||||
'title' => E::ts('Cancel reason'),
|
||||
'type' => CRM_Utils_Type::T_STRING,
|
||||
'api.required' => 1,
|
||||
'description' => E::ts('The reason for the donation being cancelled.'),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* TwingleDonation.Cancel API
|
||||
*
|
||||
* @param array $params
|
||||
* @return array API result descriptor
|
||||
* @param array<string, mixed> $params
|
||||
* @return array<string, mixed> API result descriptor
|
||||
* @see civicrm_api3_create_success
|
||||
* @see civicrm_api3_create_error
|
||||
*/
|
||||
function civicrm_api3_twingle_donation_Cancel($params) {
|
||||
// Log call if debugging is enabled within civicrm.settings.php.
|
||||
if (defined('TWINGLE_API_LOGGING') && TWINGLE_API_LOGGING) {
|
||||
CRM_Core_Error::debug_log_message('TwingleDonation.Cancel: ' . json_encode($params, JSON_PRETTY_PRINT));
|
||||
Civi::log()->debug('TwingleDonation.Cancel: ' . json_encode($params, JSON_PRETTY_PRINT));
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate date for parameter "cancelled_at".
|
||||
if (!DateTime::createFromFormat('Ymd', $params['cancelled_at'])) {
|
||||
throw new CiviCRM_API3_Exception(
|
||||
if (!DateTime::createFromFormat('YmdHis', $params['cancelled_at'])) {
|
||||
throw new CRM_Core_Exception(
|
||||
E::ts('Invalid date for parameter "cancelled_at".'),
|
||||
'invalid_format'
|
||||
);
|
||||
}
|
||||
|
||||
// Retrieve (recurring) contribution.
|
||||
$default_profile = CRM_Twingle_Profile::getProfile('default');
|
||||
try {
|
||||
$contribution = civicrm_api3('Contribution', 'getsingle', array(
|
||||
'trxn_id' => $params['trx_id'],
|
||||
));
|
||||
$contribution = civicrm_api3('Contribution', 'getsingle', [
|
||||
'trxn_id' => $default_profile->getTransactionID($params['trx_id']),
|
||||
]);
|
||||
$contribution_type = 'Contribution';
|
||||
}
|
||||
catch (CiviCRM_API3_Exception $exception) {
|
||||
$contribution = civicrm_api3('ContributionRecur', 'getsingle', array(
|
||||
'trxn_id' => $params['trx_id'],
|
||||
));
|
||||
catch (CRM_Core_Exception $exception) {
|
||||
$contribution = civicrm_api3('ContributionRecur', 'getsingle', [
|
||||
'trxn_id' => $default_profile->getTransactionID($params['trx_id']),
|
||||
]);
|
||||
$contribution_type = 'ContributionRecur';
|
||||
}
|
||||
|
||||
// End SEPA mandate if applicable.
|
||||
if (
|
||||
CRM_Twingle_Submission::civiSepaEnabled()
|
||||
&& CRM_Sepa_Logic_Settings::isSDD($contribution)
|
||||
&& CRM_Twingle_Tools::isSDD($contribution['payment_instrument_id'])
|
||||
) {
|
||||
$mandate_id = CRM_Sepa_Logic_Settings::getMandateFor($contribution['id']);
|
||||
// End SEPA mandate if applicable.
|
||||
$mandate = CRM_Twingle_Tools::getMandateFor((int) $contribution['id']);
|
||||
if (!$mandate) {
|
||||
throw new CRM_Core_Exception(
|
||||
E::ts('SEPA Mandate for contribution [%1 not found.', [1 => $contribution['id']]),
|
||||
'api_error'
|
||||
);
|
||||
}
|
||||
$mandate_id = (int) $mandate['id'];
|
||||
|
||||
// Mandates can not be terminated in the past.
|
||||
$end_date = date_create_from_format('YmdHis', $params['cancelled_at']);
|
||||
if (FALSE !== $end_date) {
|
||||
// Mandates can not be terminated in the past:
|
||||
$end_date = date('Ymd', max(
|
||||
time(),
|
||||
date_create_from_format('Ymd', $params['cancelled_at'])->getTimestamp()
|
||||
));
|
||||
$end_date->getTimestamp()));
|
||||
}
|
||||
else {
|
||||
// end date couldn't be parsed, use 'now'
|
||||
$end_date = date('Ymd');
|
||||
}
|
||||
|
||||
if (!CRM_Sepa_BAO_SEPAMandate::terminateMandate(
|
||||
$mandate_id,
|
||||
$end_date,
|
||||
$params['cancel_reason']
|
||||
)) {
|
||||
throw new CiviCRM_API3_Exception(
|
||||
throw new CRM_Core_Exception(
|
||||
E::ts('Could not terminate SEPA mandate'),
|
||||
'api_error'
|
||||
);
|
||||
}
|
||||
|
||||
// Retrieve updated contribution for return value.
|
||||
$contribution = civicrm_api3($contribution_type, 'getsingle', array(
|
||||
$contribution = civicrm_api3($contribution_type, 'getsingle', [
|
||||
'id' => $contribution['id'],
|
||||
));
|
||||
]);
|
||||
}
|
||||
else {
|
||||
$contribution = civicrm_api3($contribution_type, 'create', array(
|
||||
// regular contribution
|
||||
CRM_Twingle_Tools::$protection_suspended = TRUE;
|
||||
$contribution = civicrm_api3($contribution_type, 'create', [
|
||||
'id' => $contribution['id'],
|
||||
'cancel_date' => $params['cancelled_at'],
|
||||
'contribution_status_id' => 'Cancelled',
|
||||
'cancel_reason' => $params['cancel_reason'],
|
||||
));
|
||||
]);
|
||||
CRM_Twingle_Tools::$protection_suspended = FALSE;
|
||||
}
|
||||
|
||||
$result = civicrm_api3_create_success($contribution);
|
||||
}
|
||||
catch (CiviCRM_API3_Exception $exception) {
|
||||
catch (Exception $exception) {
|
||||
$result = civicrm_api3_create_error($exception->getMessage());
|
||||
}
|
||||
|
||||
|
|
|
@ -13,105 +13,136 @@
|
|||
| written permission from the original author(s). |
|
||||
+-------------------------------------------------------------*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
use CRM_Twingle_ExtensionUtil as E;
|
||||
|
||||
/**
|
||||
* TwingleDonation.Endrecurring API specification (optional)
|
||||
* This is used for documentation and validation.
|
||||
*
|
||||
* @param array $params description of fields supported by this API call
|
||||
* @param array<string, array<string, mixed>> $params description of fields supported by this API call
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @see http://wiki.civicrm.org/confluence/display/CRMDOC/API+Architecture+Standards
|
||||
*/
|
||||
function _civicrm_api3_twingle_donation_endrecurring_spec(&$params) {
|
||||
$params['project_id'] = array(
|
||||
$params['project_id'] = [
|
||||
'name' => 'project_id',
|
||||
'title' => E::ts('Project ID'),
|
||||
'type' => CRM_Utils_Type::T_STRING,
|
||||
'api.required' => 1,
|
||||
'description' => E::ts('The Twingle project ID.'),
|
||||
);
|
||||
$params['trx_id'] = array(
|
||||
];
|
||||
$params['trx_id'] = [
|
||||
'name' => 'trx_id',
|
||||
'title' => E::ts('Transaction ID'),
|
||||
'type' => CRM_Utils_Type::T_STRING,
|
||||
'api.required' => 1,
|
||||
'description' => E::ts('The unique transaction ID of the donation'),
|
||||
);
|
||||
$params['ended_at'] = array(
|
||||
];
|
||||
$params['ended_at'] = [
|
||||
'name' => 'ended_at',
|
||||
'title' => E::ts('Ended at'),
|
||||
'type' => CRM_Utils_Type::T_INT,
|
||||
'api.required' => 1,
|
||||
'description' => E::ts('The date when the recurring donation was ended, format: YmdHis.'),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* TwingleDonation.Endrecurring API
|
||||
*
|
||||
* @param array $params
|
||||
* @return array API result descriptor
|
||||
* @param array<string, mixed> $params
|
||||
* @return array<string, mixed> API result descriptor
|
||||
* @see civicrm_api3_create_success
|
||||
* @see civicrm_api3_create_error
|
||||
*/
|
||||
function civicrm_api3_twingle_donation_endrecurring($params) {
|
||||
// Log call if debugging is enabled within civicrm.settings.php.
|
||||
if (defined('TWINGLE_API_LOGGING') && TWINGLE_API_LOGGING) {
|
||||
CRM_Core_Error::debug_log_message('TwingleDonation.Endrecurring: ' . json_encode($params, JSON_PRETTY_PRINT));
|
||||
Civi::log()->debug('TwingleDonation.Endrecurring: ' . json_encode($params, JSON_PRETTY_PRINT));
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate date for parameter "ended_at".
|
||||
if (!DateTime::createFromFormat('Ymd', $params['ended_at'])) {
|
||||
throw new CiviCRM_API3_Exception(
|
||||
if (FALSE === DateTime::createFromFormat('YmdHis', $params['ended_at'])) {
|
||||
throw new CRM_Core_Exception(
|
||||
E::ts('Invalid date for parameter "ended_at".'),
|
||||
'invalid_format'
|
||||
);
|
||||
}
|
||||
|
||||
$contribution = civicrm_api3('ContributionRecur', 'getsingle', array(
|
||||
'trxn_id' => $params['trx_id'],
|
||||
));
|
||||
$default_profile = CRM_Twingle_Profile::getProfile('default');
|
||||
$contribution = civicrm_api3('ContributionRecur', 'getsingle', [
|
||||
'trxn_id' => $default_profile->getTransactionID($params['trx_id']),
|
||||
]);
|
||||
|
||||
// End SEPA mandate (which ends the associated recurring contribution) or
|
||||
// recurring contributions.
|
||||
if (
|
||||
CRM_Twingle_Submission::civiSepaEnabled()
|
||||
&& CRM_Sepa_Logic_Settings::isSDD($contribution)
|
||||
&& CRM_Twingle_Tools::isSDD($contribution['payment_instrument_id'])
|
||||
) {
|
||||
$mandate_id = CRM_Sepa_Logic_Settings::getMandateFor($contribution['id']);
|
||||
// Mandates can not be terminated in the past.
|
||||
// END SEPA MANDATE
|
||||
$mandate = CRM_Twingle_Tools::getMandateFor((int) $contribution['id']);
|
||||
if (!isset($mandate)) {
|
||||
throw new CRM_Core_Exception(
|
||||
E::ts('SEPA Mandate for recurring contribution [%1 not found.', [1 => $contribution['id']]),
|
||||
'api_error'
|
||||
);
|
||||
}
|
||||
|
||||
$mandate_id = $mandate['id'];
|
||||
$end_date = date_create_from_format('YmdHis', $params['ended_at']);
|
||||
if (FALSE !== $end_date) {
|
||||
// Mandates can not be terminated in the past:
|
||||
$end_date = date('Ymd', max(
|
||||
time(),
|
||||
date_create_from_format('Ymd', $params['cancelled_at'])->getTimestamp()
|
||||
));
|
||||
$end_date->getTimestamp()));
|
||||
}
|
||||
else {
|
||||
// end date couldn't be parsed, use 'now'
|
||||
$end_date = date('Ymd');
|
||||
}
|
||||
|
||||
// verify that the mandate has not been terminated in the past
|
||||
if ($mandate['status'] != 'FRST' && $mandate['status'] != 'RCUR') {
|
||||
throw new CRM_Core_Exception(
|
||||
E::ts('SEPA Mandate [%1] already terminated.', [1 => $mandate_id]),
|
||||
'api_error'
|
||||
);
|
||||
}
|
||||
|
||||
if (!CRM_Sepa_BAO_SEPAMandate::terminateMandate(
|
||||
$mandate_id,
|
||||
$end_date,
|
||||
E::ts('Mandate closed by TwingleDonation.Endrecurring API call')
|
||||
)) {
|
||||
throw new CiviCRM_API3_Exception(
|
||||
throw new CRM_Core_Exception(
|
||||
E::ts('Could not terminate SEPA mandate'),
|
||||
'api_error'
|
||||
);
|
||||
}
|
||||
$contribution = civicrm_api3('ContributionRecur', 'getsingle', array(
|
||||
$contribution = civicrm_api3('ContributionRecur', 'getsingle', [
|
||||
'id' => $contribution['id'],
|
||||
));
|
||||
]);
|
||||
}
|
||||
else {
|
||||
$contribution = civicrm_api3('ContributionRecur', 'create', array(
|
||||
// END RECURRING CONTRIBUTION
|
||||
CRM_Twingle_Tools::$protection_suspended = TRUE;
|
||||
$contribution = civicrm_api3('ContributionRecur', 'create', [
|
||||
'id' => $contribution['id'],
|
||||
'end_date' => $params['ended_at'],
|
||||
'contribution_status_id' => 'Completed',
|
||||
));
|
||||
'contribution_status_id' => CRM_Twingle_Submission::CONTRIBUTION_STATUS_COMPLETED,
|
||||
]);
|
||||
CRM_Twingle_Tools::$protection_suspended = FALSE;
|
||||
}
|
||||
|
||||
$result = civicrm_api3_create_success($contribution);
|
||||
}
|
||||
catch (CiviCRM_API3_Exception $exception) {
|
||||
catch (Exception $exception) {
|
||||
$result = civicrm_api3_create_error($exception->getMessage());
|
||||
}
|
||||
|
||||
|
|
137
api/v3/TwingleProduct/Create.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
71
api/v3/TwingleProduct/Delete.php
Normal 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]
|
||||
);
|
||||
}
|
||||
}
|
136
api/v3/TwingleProduct/Get.php
Normal 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');
|
||||
}
|
54
api/v3/TwingleProduct/Getsingle.php
Normal 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']];
|
||||
}
|
79
api/v3/TwingleShop/Create.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
79
api/v3/TwingleShop/Delete.php
Normal 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]
|
||||
);
|
||||
}
|
||||
}
|
96
api/v3/TwingleShop/Fetch.php
Normal 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
|
@ -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');
|
||||
}
|
54
api/v3/TwingleShop/Getsingle.php
Normal 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']];
|
||||
}
|
2
ci/README.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
The dependencies specified in composer.json of this directory are required to
|
||||
run phpstan in CI.
|
33
ci/composer.json
Normal file
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"civicrm/composer-compile-plugin": false,
|
||||
"civicrm/composer-downloads-plugin": true,
|
||||
"cweagans/composer-patches": true
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"civicrm/civicrm-core": "^5"
|
||||
},
|
||||
"scripts": {
|
||||
"post-install-or-update": [
|
||||
"# The following statements are only necessary when the extension is inside a CiviCRM installation, actually not required in CI.",
|
||||
"# Avoid redeclaration of function \\GuzzleHttp\\http_build_query()",
|
||||
"if [ -e vendor/civicrm/civicrm-core/guzzle_php81_shim.php ]; then echo '' >vendor/civicrm/civicrm-core/guzzle_php81_shim.php; fi",
|
||||
"# Avoid redeclaration of function \\GuzzleHttp\\Promise\\queue()",
|
||||
"if [ -e vendor/guzzlehttp/promises/src/functions.php ]; then echo '' >vendor/guzzlehttp/promises/src/functions.php; fi",
|
||||
"# Avoid CiviCRM load extensions in vendor",
|
||||
"if [ -e vendor/civicrm ]; then find vendor/civicrm -name 'info.xml' -delete; fi",
|
||||
"# Avoid Class 'CRM_AfformAdmin_ExtensionUtil' not found",
|
||||
"find vendor -name '*.mgd.php' -delete"
|
||||
],
|
||||
"post-install-cmd": [
|
||||
"@post-install-or-update"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@post-install-or-update"
|
||||
]
|
||||
}
|
||||
}
|
45
composer.json
Normal file
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"name": "systopia/de.systopia.twingle",
|
||||
"type": "civicrm-ext",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
},
|
||||
"require": {
|
||||
},
|
||||
"scripts": {
|
||||
"composer-phpcs": [
|
||||
"@composer --working-dir=tools/phpcs"
|
||||
],
|
||||
"composer-phpstan": [
|
||||
"@composer --working-dir=tools/phpstan"
|
||||
],
|
||||
"composer-phpunit": [
|
||||
"@composer --working-dir=tools/phpunit"
|
||||
],
|
||||
"composer-tools": [
|
||||
"@composer-phpcs",
|
||||
"@composer-phpstan",
|
||||
"@composer-phpunit"
|
||||
],
|
||||
"phpcs": [
|
||||
"@php tools/phpcs/vendor/bin/phpcs"
|
||||
],
|
||||
"phpcbf": [
|
||||
"@php tools/phpcs/vendor/bin/phpcbf"
|
||||
],
|
||||
"phpstan": [
|
||||
"@php tools/phpstan/vendor/bin/phpstan"
|
||||
],
|
||||
"phpunit": [
|
||||
"@php tools/phpunit/vendor/bin/simple-phpunit --coverage-text"
|
||||
],
|
||||
"test": [
|
||||
"@phpcs",
|
||||
"@phpstan",
|
||||
"@phpunit"
|
||||
]
|
||||
}
|
||||
}
|
3
css/twingle.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.twingle-profile-list {
|
||||
border-bottom: 1px solid #cfcec3;
|
||||
}
|
37
css/twingle_shop.css
Normal 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); }
|
||||
}
|
87
docs/api.md
Normal file
|
@ -0,0 +1,87 @@
|
|||
# API documentation
|
||||
|
||||
The extension provides a new CiviCRM API 3 entity `TwingleDonation` with API
|
||||
actions to record a new donation, end a previously submitted recurring donation
|
||||
and cancel previously submitted donation.
|
||||
|
||||
### Submit donation
|
||||
|
||||
This API action processes submitted Twingle donations and donor information.
|
||||
|
||||
- Entity: `TwingleDonation`
|
||||
- Action: `Submit`
|
||||
|
||||
The action accepts the following parameters:
|
||||
|
||||
| Parameter | Type | Description | Values/Format | Required |
|
||||
| -------------------------------------- | ------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| <nobr>`project_id`</nobr> | String | The Twingle project ID | | Yes |
|
||||
| <nobr>`trx_id`</nobr> | String | The unique transaction ID of the donation | A unique transaction ID for the donation. | Yes |
|
||||
| <nobr>`confirmed_at`</nobr> | String | The date when the donation was issued | A string representing a date in the format `YmdHis` | Yes |
|
||||
| <nobr>`purpose`</nobr> | String | The purpose of the donation | | |
|
||||
| <nobr>`amount`</nobr> | Integer | The donation amount in minor currency unit | | Yes |
|
||||
| <nobr>`currency`</nobr> | String | The ISO-4217 currency code of the donation | A valid ISO-4217 currency code | Yes |
|
||||
| <nobr>`newsletter`</nobr> | Boolean | Whether to subscribe the contact to the newsletter group defined in the profile | | |
|
||||
| <nobr>`postinfo`</nobr> | Boolean | Whether to subscribe the contact to the postal mailing group defined in the profile | | |
|
||||
| <nobr>`donation_receipt`</nobr> | Boolean | Whether the contact requested a donation receipt | | |
|
||||
| <nobr>`payment_method`</nobr> | String | The Twingle payment method used for the donation | One of:<br /><ul><li><nobr>`banktransfer`</nobr></li><li><nobr>`debit_manual`</nobr></li><li><nobr>`debit_automatic`</nobr></li><li><nobr>`creditcard`</nobr></li><li><nobr>`mobilephone_germany`</nobr></li><li><nobr>`paypal`</nobr></li><li><nobr>`sofortueberweisung`</nobr></li><li><nobr>`amazonpay`</nobr></li><li><nobr>`paydirekt`</nobr></li><li><nobr>`applepay`</nobr></li><li><nobr>`googlepay`</nobr></li></ul> | Yes |
|
||||
| <nobr>`donation_rhythm`</nobr> | String | The interval which the donation is recurring in | One of:<br /><ul><li><nobr>`'one_time',`</nobr></li><li><nobr>`'halfyearly',`</nobr></li><li><nobr>`'quarterly',`</nobr></li><li><nobr>`'yearly',`</nobr></li><li><nobr>`'monthly'`</nobr></li></ul> | Yes |
|
||||
| <nobr>`debit_iban`</nobr> | String | The IBAN for SEPA Direct Debit payments | A valid ISO 13616-1:2007 IBAN | Yes, if `payment_method` is `debit_manual` and CiviSEPA is used |
|
||||
| <nobr>`debit_bic`</nobr> | String | The BIC for SEPA Direct Debit payments | A valid ISO 9362 BIC | Yes, if `payment_method` is `debit_manual` and CiviSEPA is used |
|
||||
| <nobr>`debit_mandate_reference`</nobr> | String | The mandate reference for SEPA Direct Debit payments | | |
|
||||
| <nobr>`debit_account_holder`</nobr> | String | The account holder for SEPA Direct Debit payments | | |
|
||||
| <nobr>`is_anonymous`</nobr> | Boolean | Whether the donation is submitted anonymously | | |
|
||||
| <nobr>`user_gender`</nobr> | String | The gender of the contact | | |
|
||||
| <nobr>`user_birthdate`</nobr> | String | The date of birth of the contact | A string representing a date in the format `Ymd` | |
|
||||
| <nobr>`user_title`</nobr> | String | The formal title of the contact | | |
|
||||
| <nobr>`user_email`</nobr> | String | The e-mail address of the contact | A valid e-mail address | |
|
||||
| <nobr>`user_firstname`</nobr> | String | The first name of the contact | | |
|
||||
| <nobr>`user_lastname`</nobr> | String | The last name of the contact | | |
|
||||
| <nobr>`user_street`</nobr> | String | The street address of the contact | | |
|
||||
| <nobr>`user_postal_code`</nobr> | String | The postal code of the contact | | |
|
||||
| <nobr>`user_city`</nobr> | String | The city of the contact | | |
|
||||
| <nobr>`user_country`</nobr> | String | The country of the contact | A [ISO 3166-1 Alpha-2 country code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements) | |
|
||||
| <nobr>`user_telephone`</nobr> | String | The telephone number of the contact | | |
|
||||
| <nobr>`user_company`</nobr> | String | The company of the contact | | |
|
||||
| <nobr>`user_extrafield`</nobr> | String | Additional information of the contact | | |
|
||||
| <nobr>`user_language`</nobr> | String | The preferred language of the contact. | A [ISO-639-1 2-digit language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) | |
|
||||
| <nobr>`campaign_id`</nobr> | Integer | The CiviCRM ID of a campaign to assign the contribution | A valid CiviCRM Campaign ID. This overrides the campaign ID configured within the profile. | |
|
||||
|
||||
You may also refer to
|
||||
[the code](https://github.com/systopia/de.systopia.twingle/blob/master/api/v3/TwingleDonation/Submit.php)
|
||||
for more insight into this API action.
|
||||
|
||||
### End recurring donation
|
||||
|
||||
- Entity: `TwingleDonation`
|
||||
- Action: `Endrecurring`
|
||||
|
||||
The action accepts the following parameters:
|
||||
|
||||
| Parameter | Type | Description | Values/Format | Required |
|
||||
| ------------------------- | ------- | ---------------------------------------------- | --------------------------------------------------- | -------- |
|
||||
| <nobr>`project_id`</nobr> | String | The Twingle project ID | | Yes |
|
||||
| <nobr>`trx_id`</nobr> | String | The unique transaction ID of the donation | A unique transaction ID for the donation. | Yes |
|
||||
| <nobr>`ended_at`</nobr> | Integer | The date when the recurring donation was ended | A string representing a date in the format `YmdHis` | Yes |
|
||||
|
||||
You may also refer to
|
||||
[the code](https://github.com/systopia/de.systopia.twingle/blob/master/api/v3/TwingleDonation/Endrecurring.php)
|
||||
for more insight into this API action.
|
||||
|
||||
### Cancel donation
|
||||
|
||||
- Entity: `TwingleDonation`
|
||||
- Action: `Cancel`
|
||||
|
||||
The action accepts the following parameters:
|
||||
|
||||
| Parameter | Type | Description | Values/Format | Required |
|
||||
| ---------------------------- | ------ | -------------------------------------------------- | --------------------------------------------------- | -------- |
|
||||
| <nobr>`project_id`</nobr> | String | The Twingle project ID | | Yes |
|
||||
| <nobr>`trx_id`</nobr> | String | The unique transaction ID of the donation | A unique transaction ID for the donation. | Yes |
|
||||
| <nobr>`cancelled_at`</nobr> | String | The date when the recurring donation was cancelled | A string representing a date in the format `YmdHis` | Yes |
|
||||
| <nobr>`cancel_reason`</nobr> | String | The reason for the donation being cancelled | | Yes |
|
||||
|
||||
You may also refer to
|
||||
[the code](https://github.com/systopia/de.systopia.twingle/blob/master/api/v3/TwingleDonation/Cancel.php)
|
||||
for more insight into this API action.
|
27
docs/configuration/account.md
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Twingle account settings on the Twingle website
|
||||
|
||||
The use of the Twingle API extension requires that you already have a Twingle
|
||||
account and that you make some settings in this account for the connection.
|
||||
|
||||
You will have to provide the following settings in your Twingle account settings
|
||||
in order to send donations to CiviCRM:
|
||||
|
||||
1. API key from your Twingle user
|
||||
2. Site key
|
||||
3. URL
|
||||
|
||||
Important: The URL must always be the complete URL to the CiviCRM REST API
|
||||
endpoint.
|
||||
Examples:
|
||||
|
||||
- Drupal (with the *AuthX* extension): https://example.org/civicrm/ajax/rest
|
||||
- Drupal (legacy
|
||||
method): https://example.org/sites/all/modules/civicrm/extern/rest.php
|
||||
- Wordpress (with CiviCRM <
|
||||
5.25): https://example.org/wp-content/plugins/civicrm/civicrm/extern/rest.php
|
||||
- Wordpress (with CiviCRM
|
||||
5.25+): https://example.org/wp-json/civicrm/v3/rest
|
||||
|
||||
For detailled information, please see
|
||||
the [Twingle documentation](https://support.twingle.de/faq/de-de/9-anbindung-externer-systeme/46-wie-kann-ich-civicrm-mit-twingle-nutzen) (
|
||||
in German language only).
|
32
docs/configuration/civisepa.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Activating CiviSEPA integration
|
||||
|
||||
The Twingle API extension provides integration with the [
|
||||
*CiviSEPA*](https://civicrm.org/extensions/civisepa-sepa-direct-debit-extension)
|
||||
extension. This allows for managing SEPA mandates and collections with
|
||||
*CiviSEPA* for donations being initiated via a *Twingle* form.
|
||||
|
||||
1. In CiviCRM, go to **Administer**.
|
||||
2. Choose **Twingle API configuration**.
|
||||

|
||||
|
||||
3. Then click on **Configure extension settings**.
|
||||

|
||||
|
||||
4. Tick the boxes **Use CiviSEPA** and **Use CiviSEPA generated reference**.
|
||||
These options can only be activated if CiviSEPA is installed and used. If it
|
||||
is not activated, the administration of SEPA mandates will have to take place
|
||||
in Twingle, which is subject to configuration of your available payment
|
||||
methods.
|
||||
5. Write **TW-** in the **Twingle ID Prefix** field.
|
||||
To avoid overlaps when assigning CiviCRM IDs and Twingle transaction IDs, a
|
||||
prefix should be assigned here, e.g. "TWNGL" or "Twingle" or similar.
|
||||
Attention: The prefix should not be changed later, otherwise problems may
|
||||
occur.
|
||||
6. In the **Protect Recurring Contributions** field select **No**.
|
||||
If you choose Yes, all recurring donations created by Twingle can no longer
|
||||
be changed in CiviCRM, but must then be changed accordingly in Twingle. If no
|
||||
recurring payments are processed via Twingle, but only one-off donations,
|
||||
then this does not need to be activated. Otherwise, we strongly recommend
|
||||
setting the button here to **Yes** so that there are no discrepancies between
|
||||
CiviCRM and Twingle.
|
||||

|
30
docs/configuration/profiles.md
Normal file
|
@ -0,0 +1,30 @@
|
|||
# Configuring the Twingle Profile in CiviCRM
|
||||
|
||||
The Twingle API extension is being configured through configuration profiles.
|
||||
This allows you to have different sets of configuration, as you might want to
|
||||
handle donation submissions differently, depending on which form was used.
|
||||
Each profile can react to one ore more form IDs being submitted along with
|
||||
donation data.
|
||||
|
||||
1. In CiviCRM, go to **Administer**.
|
||||
2. Choose **Twingle API configuration**.
|
||||

|
||||
|
||||
3. Then click on **Configure profiles**.
|
||||

|
||||
|
||||
4. The Twingle configuration is always done with the help of a profile. Please
|
||||
use the Twingle default profile and click on **Edit**.
|
||||

|
||||
|
||||
5. Then you will identify the Twingle API profile window. Start by entering the
|
||||
corresponding information in the **General settings** section.
|
||||

|
||||
|
||||
6. Define the different payment methods in the Payments section.
|
||||

|
||||
|
||||
7. Make the settings for the groups.
|
||||

|
||||
|
||||
8. When you have made all the settings, please press the **Save** button.
|
82
docs/configuration/user_permissions.md
Normal file
|
@ -0,0 +1,82 @@
|
|||
# User, permissions, and API authentication
|
||||
|
||||
After the installation of the Twingle API extension, various configuration steps
|
||||
must be carried out so that the connection functions smoothly. Among other
|
||||
things, certain configurations must be made on the CMS platform CiviCRM is
|
||||
implemented on.
|
||||
|
||||
Connecting to the Twingle API via CiviCRM's REST interface requires a user with
|
||||
appropriate permissions in your CMS-system.
|
||||
|
||||
You might want to create a specific user role to assign permissions necessary
|
||||
for calling the Twingle API only. This section describes how to accomplish this
|
||||
in *Drupal* and *Wordpress*:
|
||||
|
||||
## New User Role in Drupal
|
||||
|
||||
1. In Drupal, go to **Administration/People/Permissions/Roles**.
|
||||
2. Type Twingle API in the text box and select **Add role**. To the right of
|
||||
your role there will be a *edit role* function and an *edit permissions*
|
||||
button. The *edit permissions* selection will show only the permission
|
||||
selections for the individual role.
|
||||
3. As Permission you only have to select the following entry: **Twingle API:
|
||||
Access Twingle API**.
|
||||
|
||||
## New User Role in Wordpress
|
||||
|
||||
1. In CiviCRM, go to **Administer/User and Permissions (Access Control)**.
|
||||
2. Then select the **WordPress Access Control** link.
|
||||
Here you can adjust the CiviCRM settings for each of the predefined User
|
||||
Roles from WordPress.
|
||||
3. Scroll down. As Permission you only have to select the following entry: *
|
||||
*Twingle API: Access Twingle API**.
|
||||
|
||||

|
||||
|
||||
## New User in Drupal
|
||||
|
||||
1. In Drupal, go to **Administration/People**.
|
||||
2. Then select **Add user**.
|
||||
3. In user name field enter something like **Twingle API**
|
||||
5. In Roles select **Twingle API**.
|
||||
|
||||
## Take over user
|
||||
|
||||
The Twingle API only works correctly if a contact connected to the permissioned
|
||||
user exists in CiviCRM.
|
||||
|
||||
Here, the corresponding steps are described by way of example when using Drupal.
|
||||
|
||||
1. In CiviCRM, go to **Administer**.
|
||||
2. In the **Users and Permissions** section, choose **Synchronize Users to
|
||||
Contacts**.
|
||||
|
||||

|
||||
|
||||
This function checks each user record in Drupal for a contact record in CiviCRM.
|
||||
If there is no corresponding contact record for a user, a new one will be
|
||||
generated. Check this in your CiviCRM contact management.
|
||||
|
||||

|
||||
|
||||
## Assign API key for the Twingle API user
|
||||
|
||||
The Twingle API contact in CiviCRM needs their own API key for authenticating
|
||||
against CiviCRM's REST API endpoint. The API key is assigned with the help of
|
||||
the API Explorer in CiviCRM.
|
||||
|
||||
1. Select the Twingle API contact in CiviCRM.
|
||||
2. Look for the corresponding **CiviCRM ID** and remember the ID.
|
||||
3. Go to **Support/Developper/API Explorer v4**.
|
||||
4. Enter **Contact** in the entity field, **create** in action field and the
|
||||
**ID** of the Twingle User in the **index** field.
|
||||
5. In the values field, select **api_key**.
|
||||
6. Enter the API key for the Twingle API user in the **add value** field.
|
||||
7. Click on **Execute**.
|
||||
|
||||

|
||||
|
||||
!!!note
|
||||
You can also create API keys for contacts by using the [*API
|
||||
Key*](https://civicrm.org/extensions/api-key) extension or with administrator
|
||||
tools like *cv* or *drush*.
|
75
docs/configuration/xcm.md
Normal file
|
@ -0,0 +1,75 @@
|
|||
# Configuring the Extended Contact Manager extension (XCM)
|
||||
|
||||
After the installation of the Twingle API extension, various configuration steps
|
||||
must be carried out so that the connection functions smoothly. Twingle API
|
||||
depends on the *Extended Contact Manager (XCM)* extension.
|
||||
|
||||
Taking over contact data using the Twingle API means that they may produce
|
||||
duplicates in your CiviCRM contact management. Before contacts are added or
|
||||
updated in CiviCRM a data check should take place to avoid this problem. This
|
||||
data check is handled by the *Extended Contact Manager (XCM)* extension. This
|
||||
extension must be configured accordingly for use with Twingle by defining a
|
||||
corresponding profile.
|
||||
|
||||
## Creating an XCM Profile
|
||||
|
||||
Your first task regarding the Extended Contact Manager extension (XCM)
|
||||
configuration will be to create an XCM profile to be used for the Twingle API.
|
||||
This works best if you copy the *Default* profile.
|
||||
|
||||
1. In CiviCRM, go to **Administer**.
|
||||
|
||||
2. Select **Xtended Contact Matcher (XCM) Configuration** in the **System
|
||||
Settings** section.
|
||||
|
||||

|
||||
|
||||
3. Click on **Copy** in the **Default** profile.
|
||||
|
||||
4. Rename the new profile with **Twingle** in the **Profile name** field.
|
||||
|
||||

|
||||
|
||||
5. Click **Save** at the bottom of this window. In the Profiles overview you can
|
||||
find your new Twingle profile.
|
||||
|
||||

|
||||
|
||||
## Set up the Extended Contact Manager extension
|
||||
|
||||
After you have created the XCM profile, you must enter the configuration
|
||||
settings for the Twingle connection to CiviCRM in this profile. Generally, you
|
||||
will find a description of all the settings in
|
||||
the [Extended Contact Manager (XCM) documentation](https://docs.civicrm.org/xcm/en/latest/configuration/).
|
||||
|
||||
Here you will find as support screenshots of the various sections of the
|
||||
Extended Contact Manager extension (XCM). The settings are only an example.
|
||||
Please adapt the settings to your individual requirements or environnement.
|
||||
|
||||
#### General section
|
||||
|
||||

|
||||
|
||||
#### Update section
|
||||
|
||||

|
||||
|
||||
#### Assignment rules section
|
||||
|
||||

|
||||
|
||||
#### Identified contacts section
|
||||
|
||||

|
||||
|
||||
#### New contact section
|
||||
|
||||

|
||||
|
||||
#### Duplicate section
|
||||
|
||||

|
||||
|
||||
#### Difference Handling section
|
||||
|
||||

|
BIN
docs/img/GenSet.jpg
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
docs/img/Kon_syn.jpg
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
docs/img/Konso.jpg
Normal file
After Width: | Height: | Size: 95 KiB |
BIN
docs/img/NewUser_Tw.jpg
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
docs/img/ProNam.jpg
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
docs/img/Prof.jpg
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
docs/img/Role_Twingle.jpg
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
docs/img/Sepa.jpg
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
docs/img/SepaKon.jpg
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
docs/img/Twgrou.jpg
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
docs/img/Twin_per.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
docs/img/XCMAdmin.jpg
Normal file
After Width: | Height: | Size: 279 KiB |
BIN
docs/img/XCMAkt.jpg
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
docs/img/XCMDup.jpg
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
docs/img/XCMGen.jpg
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
docs/img/XCMIde.jpg
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
docs/img/XCMNeu.jpg
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
docs/img/XCMPro.jpg
Normal file
After Width: | Height: | Size: 122 KiB |
BIN
docs/img/XCMReg.jpg
Normal file
After Width: | Height: | Size: 76 KiB |
BIN
docs/img/XCMUpda.jpg
Normal file
After Width: | Height: | Size: 157 KiB |
BIN
docs/img/XCM_Profile.jpg
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
docs/img/apikey.jpg
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
docs/img/civiuser_tw.jpg
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
docs/img/twpay.jpg
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
docs/img/xcmdif.jpg
Normal file
After Width: | Height: | Size: 7.8 KiB |
|
@ -1 +0,0 @@
|
|||
../README.md
|
15
docs/index.md
Normal file
|
@ -0,0 +1,15 @@
|
|||
# Twingle API
|
||||
Twingle is a payment service provider that makes it possible to create donation forms with various payment options and embed them on websites or integrate them into your homepage. Interested parties can donate via known payment options ( e.g. credit card, PayPal). The procedure is also set up and optimised for mobile devices. If you want to use the Twingle fundraising service, you have to set up a corresponding online account.
|
||||
|
||||
For further information about Twingle fundraising see the [Twingle](https://www.twingle.de) website.
|
||||
|
||||
Twingle as fundraising service can be connected to CiviCRM via its API with the extension Twingle API.
|
||||
|
||||
## Features
|
||||
* Donations from Twingle can be automatically created as contributions in CiviCRM and assigned to existing or new contacts and administered in CiviCRM.
|
||||
* Supporters and contacts of donations can be managed in CiviCRM.
|
||||
* Donations can be submitted with different payment statuses depending on the payment type
|
||||
* SEPA mandates can be created for one-off and recurring payments.
|
||||
* Donors can be added to groups for receiving newsletters, mailings and donation receipts.
|
||||
* A membership can be set up for a donor.
|
||||
* Data can be entered in user-defined fields
|
19
docs/installation.md
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Installation
|
||||
|
||||
You can find an official release archive from
|
||||
the [release page](https://github.com/systopia/de.systopia.twingle).
|
||||
|
||||
1. First, download and then unpack the archive and move the directory into your
|
||||
CiviCRM extensions folder (e.g.,`.../civicrm/ext/`.
|
||||
If you don't know where your extensions folder is, just have a look in your
|
||||
CiviCRM settings ( **Administer**/**System Settings**/**Directories**)).
|
||||
2. Next, open the extensions page in the CiviCRM settings (**Administer**/*
|
||||
*System Settings**/**Extensions**).
|
||||
3. Find the extension Twingle API in the*Extensions*tab and click on**Install**.
|
||||
The extension will be set up.
|
||||
|
||||
## Extended Contact Matcher (XCM)
|
||||
|
||||
Please note that for the correct working of Twingle API you still need to
|
||||
install the extension Extended Contact Matcher (XCM), see
|
||||
the [documentation](https://docs.civicrm.org/xcm).
|
20
info.xml
|
@ -10,21 +10,33 @@
|
|||
</maintainer>
|
||||
<urls>
|
||||
<url desc="Main Extension Page">https://github.com/systopia/de.systopia.twingle</url>
|
||||
<url desc="Documentation">https://github.com/systopia/de.systopia.twingle/blob/master/README.md</url>
|
||||
<url desc="Documentation">https://docs.civicrm.org/twingle/en/latest</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>
|
||||
</urls>
|
||||
<releaseDate>2019-01-23</releaseDate>
|
||||
<version>0.9</version>
|
||||
<releaseDate></releaseDate>
|
||||
<version>1.6-dev</version>
|
||||
<develStage>dev</develStage>
|
||||
<compatibility>
|
||||
<ver>4.7</ver>
|
||||
<ver>5.58</ver>
|
||||
</compatibility>
|
||||
<comments></comments>
|
||||
<classloader>
|
||||
<psr4 prefix="Civi\" path="Civi"/>
|
||||
<psr0 prefix="CRM_" path="."/>
|
||||
</classloader>
|
||||
<requires>
|
||||
<ext>de.systopia.xcm</ext>
|
||||
</requires>
|
||||
<civix>
|
||||
<namespace>CRM/Twingle</namespace>
|
||||
<format>23.02.1</format>
|
||||
</civix>
|
||||
<mixins>
|
||||
<mixin>menu-xml@1.0.0</mixin>
|
||||
<mixin>mgd-php@1.0.0</mixin>
|
||||
<mixin>smarty-v2@1.0.1</mixin>
|
||||
<mixin>entity-types-php@1.0.0</mixin>
|
||||
</mixins>
|
||||
<upgrader>CRM_Twingle_Upgrader</upgrader>
|
||||
</extension>
|
||||
|
|
744
js/twingle_shop.js
Normal 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 = '<' + ts('select financial type', []) + '>';
|
||||
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
|
@ -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 ""
|
||||
|
93
managed/Navigation__twingle_configuration.mgd.php
Normal file
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
/*
|
||||
* Copyright (C) 2023 SYSTOPIA GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation in version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use CRM_Twingle_ExtensionUtil as E;
|
||||
|
||||
return [
|
||||
[
|
||||
'name' => 'Navigation__twingle_configuration',
|
||||
'entity' => 'Navigation',
|
||||
'cleanup' => 'unused',
|
||||
'update' => 'unmodified',
|
||||
'params' => [
|
||||
'version' => 4,
|
||||
'values' => [
|
||||
'domain_id' => 'current_domain',
|
||||
'label' => E::ts('Twingle API Configuration'),
|
||||
'name' => 'twingle_configuration',
|
||||
'url' => 'civicrm/admin/settings/twingle',
|
||||
'icon' => NULL,
|
||||
'permission' => [
|
||||
'administer CiviCRM',
|
||||
],
|
||||
'permission_operator' => 'OR',
|
||||
'parent_id.name' => 'CiviContribute',
|
||||
'is_active' => TRUE,
|
||||
'has_separator' => 0,
|
||||
],
|
||||
'match' => ['name', 'parent_id'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'Navigation__twingle_settings',
|
||||
'entity' => 'Navigation',
|
||||
'cleanup' => 'unused',
|
||||
'update' => 'unmodified',
|
||||
'params' => [
|
||||
'version' => 4,
|
||||
'values' => [
|
||||
'domain_id' => 'current_domain',
|
||||
'label' => E::ts('Twingle API Settings'),
|
||||
'name' => 'twingle_settings',
|
||||
'url' => 'civicrm/admin/settings/twingle/settings',
|
||||
'icon' => NULL,
|
||||
'permission' => [
|
||||
'administer CiviCRM',
|
||||
],
|
||||
'permission_operator' => 'OR',
|
||||
'parent_id.name' => 'twingle_configuration',
|
||||
'is_active' => TRUE,
|
||||
'has_separator' => 0,
|
||||
],
|
||||
'match' => ['name', 'parent_id'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'Navigation__twingle_profiles',
|
||||
'entity' => 'Navigation',
|
||||
'cleanup' => 'unused',
|
||||
'update' => 'unmodified',
|
||||
'params' => [
|
||||
'version' => 4,
|
||||
'values' => [
|
||||
'domain_id' => 'current_domain',
|
||||
'label' => E::ts('Twingle API Profiles'),
|
||||
'name' => 'twingle_profiles',
|
||||
'url' => 'civicrm/admin/settings/twingle/profiles',
|
||||
'icon' => NULL,
|
||||
'permission' => [
|
||||
'administer CiviCRM',
|
||||
],
|
||||
'permission_operator' => 'OR',
|
||||
'parent_id.name' => 'twingle_configuration',
|
||||
'is_active' => TRUE,
|
||||
'has_separator' => 0,
|
||||
],
|
||||
'match' => ['name', 'parent_id'],
|
||||
],
|
||||
],
|
||||
];
|
51
mixin/smarty-v2@1.0.1.mixin.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Auto-register "templates/" folder.
|
||||
*
|
||||
* @mixinName smarty-v2
|
||||
* @mixinVersion 1.0.1
|
||||
* @since 5.59
|
||||
*
|
||||
* @param CRM_Extension_MixInfo $mixInfo
|
||||
* On newer deployments, this will be an instance of MixInfo. On older deployments, Civix may polyfill with a work-a-like.
|
||||
* @param \CRM_Extension_BootCache $bootCache
|
||||
* On newer deployments, this will be an instance of MixInfo. On older deployments, Civix may polyfill with a work-a-like.
|
||||
*/
|
||||
return function ($mixInfo, $bootCache) {
|
||||
$dir = $mixInfo->getPath('templates');
|
||||
if (!file_exists($dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$register = function() use ($dir) {
|
||||
// This implementation has a theoretical edge-case bug on older versions of CiviCRM where a template could
|
||||
// be registered more than once.
|
||||
CRM_Core_Smarty::singleton()->addTemplateDir($dir);
|
||||
};
|
||||
|
||||
// Let's figure out what environment we're in -- so that we know the best way to call $register().
|
||||
|
||||
if (!empty($GLOBALS['_CIVIX_MIXIN_POLYFILL'])) {
|
||||
// Polyfill Loader (v<=5.45): We're already in the middle of firing `hook_config`.
|
||||
if ($mixInfo->isActive()) {
|
||||
$register();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (CRM_Extension_System::singleton()->getManager()->extensionIsBeingInstalledOrEnabled($mixInfo->longName)) {
|
||||
// New Install, Standard Loader: The extension has just been enabled, and we're now setting it up.
|
||||
// System has already booted. New templates may be needed for upcoming installation steps.
|
||||
$register();
|
||||
return;
|
||||
}
|
||||
|
||||
// Typical Pageview, Standard Loader: Defer the actual registration for a moment -- to ensure that Smarty is online.
|
||||
\Civi::dispatcher()->addListener('hook_civicrm_config', function() use ($mixInfo, $register) {
|
||||
if ($mixInfo->isActive()) {
|
||||
$register();
|
||||
}
|
||||
});
|
||||
|
||||
};
|
22
mkdocs.yml
|
@ -1,15 +1,25 @@
|
|||
site_name: Twingle API
|
||||
repo_url: https://github.com/systopia/de.systopia.twingle
|
||||
theme: material
|
||||
theme:
|
||||
name: material
|
||||
|
||||
pages:
|
||||
- 'Home': index.md
|
||||
nav:
|
||||
- Introduction: index.md
|
||||
- Installation: installation.md
|
||||
- Configuration:
|
||||
- User, permissions and API authentication: configuration/user_permissions.md
|
||||
- Extended Contact Manager (XCM) extension: configuration/xcm.md
|
||||
- CiviSEPA integration: configuration/civisepa.md
|
||||
- Twingle Profiles: configuration/profiles.md
|
||||
- Configuring the Twingle Account on the Twingle website: configuration/account.md
|
||||
- API documentation: api.md
|
||||
|
||||
markdown_extensions:
|
||||
- attr_list
|
||||
- admonition
|
||||
- def_list
|
||||
- codehilite
|
||||
- pymdownx.highlight:
|
||||
guess_lang: false
|
||||
- toc:
|
||||
permalink: true
|
||||
- pymdownx.superfences
|
||||
|
@ -17,3 +27,7 @@ markdown_extensions:
|
|||
- pymdownx.tilde
|
||||
- pymdownx.betterem
|
||||
- pymdownx.mark
|
||||
|
||||
plugins:
|
||||
- search:
|
||||
lang: en
|
83
phpcs.xml.dist
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?xml version="1.0"?>
|
||||
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="CiviCRM - Modified"
|
||||
xsi:noNamespaceSchemaLocation="tools/phpcs/vendor/squizlabs/php_codesniffer/phpcs.xsd">
|
||||
<description>CiviCRM coding standard with some additional changes</description>
|
||||
|
||||
<file>api</file>
|
||||
<file>Civi</file>
|
||||
<file>CRM</file>
|
||||
<file>tests</file>
|
||||
|
||||
<exclude-pattern>/CRM/[^/]+/DAO/.*\.php$</exclude-pattern>
|
||||
|
||||
<arg name="extensions" value="php"/>
|
||||
<arg name="cache" value=".phpcs.cache"/>
|
||||
<arg name="colors"/>
|
||||
<arg value="sp"/>
|
||||
|
||||
<!-- Exit with code 0 if warnings, but no error occurred -->
|
||||
<config name="ignore_warnings_on_exit" value="true"/>
|
||||
|
||||
<rule ref="tools/phpcs/vendor/drupal/coder/coder_sniffer/Drupal">
|
||||
<!-- Conflicts with PHPStan type hints -->
|
||||
<exclude name="Drupal.Commenting.VariableComment.IncorrectVarType"/>
|
||||
<exclude name="Drupal.Commenting.FunctionComment.ParamTypeSpaces"/>
|
||||
<exclude name="Drupal.Commenting.FunctionComment.ReturnTypeSpaces"/>
|
||||
<exclude name="Drupal.Commenting.VariableComment.MissingVar"/>
|
||||
|
||||
<!-- Don't enforce phpdoc type hint because it (might) only duplicate a PHP type hint -->
|
||||
<exclude name="Drupal.Commenting.VariableComment.MissingVar"/>
|
||||
|
||||
<!-- Don't enforce phpdoc type hint because it (might) only duplicate a PHP type hint -->
|
||||
<exclude name="Drupal.Commenting.FunctionComment.ParamMissingDefinition"/>
|
||||
|
||||
<!-- False positive when license header is set and variable has no comment -->
|
||||
<exclude name="Drupal.Commenting.VariableComment.WrongStyle"/>
|
||||
</rule>
|
||||
|
||||
<rule ref="Generic.Arrays.DisallowLongArraySyntax"/>
|
||||
<rule ref="Generic.CodeAnalysis.EmptyStatement">
|
||||
<exclude name="Generic.CodeAnalysis.EmptyStatement.DetectedCatch"/>
|
||||
</rule>
|
||||
<rule ref="Generic.CodeAnalysis.ForLoopWithTestFunctionCall"/>
|
||||
<rule ref="Generic.Files.OneClassPerFile"/>
|
||||
<rule ref="Generic.Files.OneInterfacePerFile"/>
|
||||
<rule ref="Generic.Files.OneObjectStructurePerFile"/>
|
||||
<rule ref="Generic.Files.OneTraitPerFile"/>
|
||||
<rule ref="Generic.Formatting.DisallowMultipleStatements"/>
|
||||
<rule ref="Generic.Metrics.CyclomaticComplexity"/>
|
||||
<rule ref="Generic.Metrics.NestingLevel"/>
|
||||
<rule ref="Generic.NamingConventions.AbstractClassNamePrefix"/>
|
||||
<rule ref="Generic.NamingConventions.InterfaceNameSuffix"/>
|
||||
<rule ref="Generic.NamingConventions.TraitNameSuffix"/>
|
||||
<rule ref="Generic.PHP.RequireStrictTypes"/>
|
||||
<rule ref="PSR1.Files.SideEffects"/>
|
||||
<rule ref="PSR12.Classes.ClassInstantiation"/>
|
||||
<rule ref="PSR12.Properties.ConstantVisibility"/>
|
||||
<rule ref="Squiz.PHP.CommentedOutCode"/>
|
||||
<rule ref="Squiz.PHP.GlobalKeyword"/>
|
||||
<rule ref="Squiz.Strings.DoubleQuoteUsage">
|
||||
<exclude name="Squiz.Strings.DoubleQuoteUsage.ContainsVar"/>
|
||||
</rule>
|
||||
|
||||
<!-- Lines can be 120 chars long, but never show errors -->
|
||||
<rule ref="Generic.Files.LineLength">
|
||||
<properties>
|
||||
<property name="lineLimit" value="120"/>
|
||||
<property name="absoluteLineLimit" value="0"/>
|
||||
</properties>
|
||||
</rule>
|
||||
|
||||
<!-- Ban some functions -->
|
||||
<rule ref="Generic.PHP.ForbiddenFunctions">
|
||||
<properties>
|
||||
<property name="forbiddenFunctions" type="array">
|
||||
<element key="sizeof" value="count"/>
|
||||
<element key="delete" value="unset"/>
|
||||
<element key="print" value="echo"/>
|
||||
<element key="is_null" value="null"/>
|
||||
<element key="create_function" value="null"/>
|
||||
</property>
|
||||
</properties>
|
||||
</rule>
|
||||
</ruleset>
|
13
phpstan.ci.neon
Normal file
|
@ -0,0 +1,13 @@
|
|||
includes:
|
||||
- phpstan.neon.dist
|
||||
|
||||
parameters:
|
||||
scanDirectories:
|
||||
- ci/vendor/civicrm/civicrm-core/CRM/
|
||||
bootstrapFiles:
|
||||
- ci/vendor/autoload.php
|
||||
# Because we test with different versions in CI we have unmatched errors
|
||||
reportUnmatchedIgnoredErrors: false
|
||||
ignoreErrors:
|
||||
# Errors we get when using "prefer-lowest"
|
||||
- '#::getSubscribedEvents\(\) return type has no value type specified in iterable type array.$#'
|
42
phpstan.neon.dist
Normal file
|
@ -0,0 +1,42 @@
|
|||
parameters:
|
||||
paths:
|
||||
- api
|
||||
- Civi
|
||||
- CRM
|
||||
- tests
|
||||
excludePaths:
|
||||
analyse:
|
||||
- CRM/*/DAO/*
|
||||
- tests/phpunit/bootstrap.php
|
||||
scanFiles:
|
||||
- twingle.civix.php
|
||||
- tools/phpunit/vendor/bin/.phpunit/phpunit/src/Framework/TestCase.php
|
||||
scanDirectories:
|
||||
- tools/phpunit/vendor/bin/.phpunit/phpunit/src/Framework
|
||||
bootstrapFiles:
|
||||
- tools/phpunit/vendor/bin/.phpunit/phpunit/vendor/autoload.php
|
||||
- vendor/autoload.php
|
||||
- phpstanBootstrap.php
|
||||
level: 9
|
||||
universalObjectCratesClasses:
|
||||
- Civi\Core\Event\GenericHookEvent
|
||||
checkTooWideReturnTypesInProtectedAndPublicMethods: true
|
||||
checkUninitializedProperties: true
|
||||
checkMissingCallableSignature: true
|
||||
treatPhpDocTypesAsCertain: false
|
||||
exceptions:
|
||||
check:
|
||||
missingCheckedExceptionInThrows: true
|
||||
tooWideThrowType: true
|
||||
checkedExceptionClasses:
|
||||
- \Webmozart\Assert\InvalidArgumentException
|
||||
implicitThrows: false
|
||||
ignoreErrors:
|
||||
# Note paths are prefixed with "*/" to work with inspections in PHPStorm because of:
|
||||
# https://youtrack.jetbrains.com/issue/WI-63891/PHPStan-ignoreErrors-configuration-isnt-working-with-inspections
|
||||
|
||||
# Example
|
||||
#- # Accessing results of API requests
|
||||
#message: "#^Offset '[^']+' does not exist on array[^\\|]+\\|null.$#"
|
||||
#path: */tests/phpunit/**/*Test.php
|
||||
tmpDir: .phpstan
|
14
phpstan.neon.template
Normal file
|
@ -0,0 +1,14 @@
|
|||
# Copy this file to phpstan.neon and replace {VENDOR_DIR} with the appropriate
|
||||
# path.
|
||||
|
||||
includes:
|
||||
- phpstan.neon.dist
|
||||
|
||||
parameters:
|
||||
scanDirectories:
|
||||
- {VENDOR_DIR}/civicrm/civicrm-core/Civi/
|
||||
- {VENDOR_DIR}/civicrm/civicrm-core/CRM/
|
||||
- {VENDOR_DIR}/civicrm/civicrm-core/api/
|
||||
- {VENDOR_DIR}/civicrm/civicrm-packages/
|
||||
bootstrapFiles:
|
||||
- {VENDOR_DIR}/autoload.php
|
43
phpstanBootstrap.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
/*
|
||||
* Copyright (C) 2022 SYSTOPIA GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation in version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
// phpcs:disable Drupal.Commenting.DocComment.ContentAfterOpen
|
||||
/** @var \PHPStan\DependencyInjection\Container $container */
|
||||
/** @phpstan-var array<string> $bootstrapFiles */
|
||||
$bootstrapFiles = $container->getParameter('bootstrapFiles');
|
||||
foreach ($bootstrapFiles as $bootstrapFile) {
|
||||
if (str_ends_with($bootstrapFile, 'vendor/autoload.php')) {
|
||||
$vendorDir = dirname($bootstrapFile);
|
||||
$civiCrmVendorDir = $vendorDir . '/civicrm';
|
||||
$civiCrmCoreDir = $civiCrmVendorDir . '/civicrm-core';
|
||||
if (file_exists($civiCrmCoreDir)) {
|
||||
set_include_path(get_include_path()
|
||||
. PATH_SEPARATOR . $civiCrmCoreDir
|
||||
. PATH_SEPARATOR . $civiCrmVendorDir . '/civicrm-packages'
|
||||
);
|
||||
// $bootstrapFile might not be included, yet. It is required for the
|
||||
// following require_once, though.
|
||||
require_once $bootstrapFile;
|
||||
// Prevent error "Class 'CRM_Core_Exception' not found in file".
|
||||
require_once $civiCrmCoreDir . '/CRM/Core/Exception.php';
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
35
phpunit.xml.dist
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.5/phpunit.xsd"
|
||||
colors="true"
|
||||
failOnRisky="true"
|
||||
failOnWarning="true"
|
||||
forceCoversAnnotation="true"
|
||||
bootstrap="tests/phpunit/bootstrap.php">
|
||||
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1" />
|
||||
<env name="SYMFONY_DEPRECATIONS_HELPER" value="max[direct]=0&baselineFile=./tests/ignored-deprecations.json"/>
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Extension Test Suite">
|
||||
<directory>./tests/phpunit</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory suffix=".php">api</directory>
|
||||
<directory suffix=".php">CRM</directory>
|
||||
<directory suffix=".php">Civi</directory>
|
||||
<exclude>
|
||||
<directory>CRM/*/DAO</directory>
|
||||
</exclude>
|
||||
</whitelist>
|
||||
</filter>
|
||||
<listeners>
|
||||
<listener class="Civi\Test\CiviTestListener">
|
||||
<arguments/>
|
||||
</listener>
|
||||
</listeners>
|
||||
</phpunit>
|
|
@ -1,34 +0,0 @@
|
|||
<?php
|
||||
/*------------------------------------------------------------+
|
||||
| SYSTOPIA Twingle Integration |
|
||||
| Copyright (C) 2018 SYSTOPIA |
|
||||
| Author: J. Schuppe (schuppe@systopia.de) |
|
||||
+-------------------------------------------------------------+
|
||||
| This program is released as free software under the |
|
||||
| Affero GPL license. You can redistribute it and/or |
|
||||
| modify it under the terms of this license which you |
|
||||
| can read by viewing the included agpl.txt or online |
|
||||
| at www.gnu.org/licenses/agpl.html. Removal of this |
|
||||
| copyright header is strictly prohibited without |
|
||||
| written permission from the original author(s). |
|
||||
+-------------------------------------------------------------*/
|
||||
|
||||
/*
|
||||
* Settings metadata file
|
||||
*/
|
||||
return array(
|
||||
'twingle_use_sepa' => array(
|
||||
'group_name' => 'de.systopia.twingle',
|
||||
'group' => 'de.systopia.twingle',
|
||||
'name' => 'twingle_use_sepa',
|
||||
'type' => 'Boolean',
|
||||
'quick_form_type' => 'YesNo',
|
||||
'html_type' => 'radio',
|
||||
'title' => 'Use CiviSEPA',
|
||||
'default' => 0,
|
||||
'add' => '4.6',
|
||||
'is_domain' => 1,
|
||||
'is_contact' => 0,
|
||||
'description' => 'Whether to provide CiviSEPA functionality for manual debit payment method. This requires the CiviSEPA (org.project60.sepa) extension be installed.',
|
||||
),
|
||||
);
|
21
sql/auto_uninstall.sql
Normal 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;
|
16
sql/civicrm_twingle_profile.sql
Normal file
|
@ -0,0 +1,16 @@
|
|||
-- /*******************************************************
|
||||
-- ** civicrm_twingle_profile
|
||||
-- **
|
||||
-- ** stores twingle profile data v1.4+
|
||||
-- ********************************************************/
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `civicrm_twingle_profile`(
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`name` varchar(255) COMMENT 'configuration name, i.e. internal ID',
|
||||
`config` text COMMENT 'JSON encoded configuration',
|
||||
`last_access` datetime COMMENT 'timestamp of the last access (through the api)',
|
||||
`access_counter` int unsigned COMMENT 'number of accesses (through the api)',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE INDEX (`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;
|
||||
|
66
sql/civicrm_twingle_shop.sql
Normal 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;
|