coati-payroll 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of coati-payroll might be problematic. Click here for more details.

Files changed (243) hide show
  1. coati_payroll/__init__.py +415 -0
  2. coati_payroll/app.py +95 -0
  3. coati_payroll/audit_helpers.py +904 -0
  4. coati_payroll/auth.py +123 -0
  5. coati_payroll/cli.py +1318 -0
  6. coati_payroll/config.py +219 -0
  7. coati_payroll/demo_data.py +813 -0
  8. coati_payroll/enums.py +278 -0
  9. coati_payroll/forms.py +1769 -0
  10. coati_payroll/formula_engine/__init__.py +81 -0
  11. coati_payroll/formula_engine/ast/__init__.py +110 -0
  12. coati_payroll/formula_engine/ast/ast_visitor.py +259 -0
  13. coati_payroll/formula_engine/ast/expression_evaluator.py +228 -0
  14. coati_payroll/formula_engine/ast/safe_operators.py +131 -0
  15. coati_payroll/formula_engine/ast/type_converter.py +172 -0
  16. coati_payroll/formula_engine/data_sources.py +752 -0
  17. coati_payroll/formula_engine/engine.py +247 -0
  18. coati_payroll/formula_engine/exceptions.py +52 -0
  19. coati_payroll/formula_engine/execution/__init__.py +24 -0
  20. coati_payroll/formula_engine/execution/execution_context.py +52 -0
  21. coati_payroll/formula_engine/execution/step_executor.py +62 -0
  22. coati_payroll/formula_engine/execution/variable_store.py +59 -0
  23. coati_payroll/formula_engine/novelty_codes.py +206 -0
  24. coati_payroll/formula_engine/results/__init__.py +20 -0
  25. coati_payroll/formula_engine/results/execution_result.py +59 -0
  26. coati_payroll/formula_engine/steps/__init__.py +30 -0
  27. coati_payroll/formula_engine/steps/assignment_step.py +71 -0
  28. coati_payroll/formula_engine/steps/base_step.py +48 -0
  29. coati_payroll/formula_engine/steps/calculation_step.py +42 -0
  30. coati_payroll/formula_engine/steps/conditional_step.py +122 -0
  31. coati_payroll/formula_engine/steps/step_factory.py +58 -0
  32. coati_payroll/formula_engine/steps/tax_lookup_step.py +45 -0
  33. coati_payroll/formula_engine/tables/__init__.py +24 -0
  34. coati_payroll/formula_engine/tables/bracket_calculator.py +51 -0
  35. coati_payroll/formula_engine/tables/table_lookup.py +161 -0
  36. coati_payroll/formula_engine/tables/tax_table.py +32 -0
  37. coati_payroll/formula_engine/validation/__init__.py +24 -0
  38. coati_payroll/formula_engine/validation/schema_validator.py +37 -0
  39. coati_payroll/formula_engine/validation/security_validator.py +52 -0
  40. coati_payroll/formula_engine/validation/tax_table_validator.py +205 -0
  41. coati_payroll/formula_engine_examples.py +153 -0
  42. coati_payroll/i18n.py +54 -0
  43. coati_payroll/initial_data.py +613 -0
  44. coati_payroll/interes_engine.py +450 -0
  45. coati_payroll/liquidacion_engine/__init__.py +25 -0
  46. coati_payroll/liquidacion_engine/engine.py +267 -0
  47. coati_payroll/locale_config.py +165 -0
  48. coati_payroll/log.py +138 -0
  49. coati_payroll/model.py +2410 -0
  50. coati_payroll/nomina_engine/__init__.py +87 -0
  51. coati_payroll/nomina_engine/calculators/__init__.py +30 -0
  52. coati_payroll/nomina_engine/calculators/benefit_calculator.py +79 -0
  53. coati_payroll/nomina_engine/calculators/concept_calculator.py +254 -0
  54. coati_payroll/nomina_engine/calculators/deduction_calculator.py +105 -0
  55. coati_payroll/nomina_engine/calculators/exchange_rate_calculator.py +51 -0
  56. coati_payroll/nomina_engine/calculators/perception_calculator.py +75 -0
  57. coati_payroll/nomina_engine/calculators/salary_calculator.py +86 -0
  58. coati_payroll/nomina_engine/domain/__init__.py +27 -0
  59. coati_payroll/nomina_engine/domain/calculation_items.py +52 -0
  60. coati_payroll/nomina_engine/domain/employee_calculation.py +53 -0
  61. coati_payroll/nomina_engine/domain/payroll_context.py +44 -0
  62. coati_payroll/nomina_engine/engine.py +188 -0
  63. coati_payroll/nomina_engine/processors/__init__.py +28 -0
  64. coati_payroll/nomina_engine/processors/accounting_processor.py +171 -0
  65. coati_payroll/nomina_engine/processors/accumulation_processor.py +90 -0
  66. coati_payroll/nomina_engine/processors/loan_processor.py +227 -0
  67. coati_payroll/nomina_engine/processors/novelty_processor.py +42 -0
  68. coati_payroll/nomina_engine/processors/vacation_processor.py +67 -0
  69. coati_payroll/nomina_engine/repositories/__init__.py +32 -0
  70. coati_payroll/nomina_engine/repositories/acumulado_repository.py +83 -0
  71. coati_payroll/nomina_engine/repositories/base_repository.py +40 -0
  72. coati_payroll/nomina_engine/repositories/config_repository.py +102 -0
  73. coati_payroll/nomina_engine/repositories/employee_repository.py +34 -0
  74. coati_payroll/nomina_engine/repositories/exchange_rate_repository.py +58 -0
  75. coati_payroll/nomina_engine/repositories/novelty_repository.py +54 -0
  76. coati_payroll/nomina_engine/repositories/planilla_repository.py +52 -0
  77. coati_payroll/nomina_engine/results/__init__.py +24 -0
  78. coati_payroll/nomina_engine/results/error_result.py +28 -0
  79. coati_payroll/nomina_engine/results/payroll_result.py +53 -0
  80. coati_payroll/nomina_engine/results/validation_result.py +39 -0
  81. coati_payroll/nomina_engine/services/__init__.py +22 -0
  82. coati_payroll/nomina_engine/services/accounting_voucher_service.py +708 -0
  83. coati_payroll/nomina_engine/services/employee_processing_service.py +173 -0
  84. coati_payroll/nomina_engine/services/payroll_execution_service.py +374 -0
  85. coati_payroll/nomina_engine/services/snapshot_service.py +295 -0
  86. coati_payroll/nomina_engine/validators/__init__.py +31 -0
  87. coati_payroll/nomina_engine/validators/base_validator.py +48 -0
  88. coati_payroll/nomina_engine/validators/currency_validator.py +50 -0
  89. coati_payroll/nomina_engine/validators/employee_validator.py +87 -0
  90. coati_payroll/nomina_engine/validators/period_validator.py +44 -0
  91. coati_payroll/nomina_engine/validators/planilla_validator.py +136 -0
  92. coati_payroll/plugin_manager.py +176 -0
  93. coati_payroll/queue/__init__.py +33 -0
  94. coati_payroll/queue/driver.py +127 -0
  95. coati_payroll/queue/drivers/__init__.py +22 -0
  96. coati_payroll/queue/drivers/dramatiq_driver.py +268 -0
  97. coati_payroll/queue/drivers/huey_driver.py +390 -0
  98. coati_payroll/queue/drivers/noop_driver.py +54 -0
  99. coati_payroll/queue/selector.py +121 -0
  100. coati_payroll/queue/tasks.py +764 -0
  101. coati_payroll/rate_limiting.py +83 -0
  102. coati_payroll/rbac.py +183 -0
  103. coati_payroll/report_engine.py +512 -0
  104. coati_payroll/report_export.py +208 -0
  105. coati_payroll/schema_validator.py +167 -0
  106. coati_payroll/security.py +77 -0
  107. coati_payroll/static/styles.css +1044 -0
  108. coati_payroll/system_reports.py +573 -0
  109. coati_payroll/templates/auth/login.html +189 -0
  110. coati_payroll/templates/base.html +283 -0
  111. coati_payroll/templates/index.html +227 -0
  112. coati_payroll/templates/macros.html +146 -0
  113. coati_payroll/templates/modules/calculation_rule/form.html +78 -0
  114. coati_payroll/templates/modules/calculation_rule/index.html +102 -0
  115. coati_payroll/templates/modules/calculation_rule/schema_editor.html +1159 -0
  116. coati_payroll/templates/modules/carga_inicial_prestacion/form.html +170 -0
  117. coati_payroll/templates/modules/carga_inicial_prestacion/index.html +170 -0
  118. coati_payroll/templates/modules/carga_inicial_prestacion/reporte.html +193 -0
  119. coati_payroll/templates/modules/config_calculos/index.html +44 -0
  120. coati_payroll/templates/modules/configuracion/index.html +90 -0
  121. coati_payroll/templates/modules/currency/form.html +47 -0
  122. coati_payroll/templates/modules/currency/index.html +64 -0
  123. coati_payroll/templates/modules/custom_field/form.html +62 -0
  124. coati_payroll/templates/modules/custom_field/index.html +78 -0
  125. coati_payroll/templates/modules/deduccion/form.html +1 -0
  126. coati_payroll/templates/modules/deduccion/index.html +1 -0
  127. coati_payroll/templates/modules/employee/form.html +254 -0
  128. coati_payroll/templates/modules/employee/index.html +76 -0
  129. coati_payroll/templates/modules/empresa/form.html +74 -0
  130. coati_payroll/templates/modules/empresa/index.html +71 -0
  131. coati_payroll/templates/modules/exchange_rate/form.html +47 -0
  132. coati_payroll/templates/modules/exchange_rate/import.html +93 -0
  133. coati_payroll/templates/modules/exchange_rate/index.html +114 -0
  134. coati_payroll/templates/modules/liquidacion/index.html +58 -0
  135. coati_payroll/templates/modules/liquidacion/nueva.html +51 -0
  136. coati_payroll/templates/modules/liquidacion/ver.html +91 -0
  137. coati_payroll/templates/modules/payroll_concepts/audit_log.html +146 -0
  138. coati_payroll/templates/modules/percepcion/form.html +1 -0
  139. coati_payroll/templates/modules/percepcion/index.html +1 -0
  140. coati_payroll/templates/modules/planilla/config.html +190 -0
  141. coati_payroll/templates/modules/planilla/config_deducciones.html +129 -0
  142. coati_payroll/templates/modules/planilla/config_empleados.html +116 -0
  143. coati_payroll/templates/modules/planilla/config_percepciones.html +113 -0
  144. coati_payroll/templates/modules/planilla/config_prestaciones.html +118 -0
  145. coati_payroll/templates/modules/planilla/config_reglas.html +120 -0
  146. coati_payroll/templates/modules/planilla/ejecutar_nomina.html +106 -0
  147. coati_payroll/templates/modules/planilla/form.html +197 -0
  148. coati_payroll/templates/modules/planilla/index.html +144 -0
  149. coati_payroll/templates/modules/planilla/listar_nominas.html +91 -0
  150. coati_payroll/templates/modules/planilla/log_nomina.html +135 -0
  151. coati_payroll/templates/modules/planilla/novedades/form.html +177 -0
  152. coati_payroll/templates/modules/planilla/novedades/index.html +170 -0
  153. coati_payroll/templates/modules/planilla/ver_nomina.html +477 -0
  154. coati_payroll/templates/modules/planilla/ver_nomina_empleado.html +231 -0
  155. coati_payroll/templates/modules/plugins/index.html +71 -0
  156. coati_payroll/templates/modules/prestacion/form.html +1 -0
  157. coati_payroll/templates/modules/prestacion/index.html +1 -0
  158. coati_payroll/templates/modules/prestacion_management/dashboard.html +150 -0
  159. coati_payroll/templates/modules/prestacion_management/initial_balance_bulk.html +195 -0
  160. coati_payroll/templates/modules/prestamo/approve.html +156 -0
  161. coati_payroll/templates/modules/prestamo/condonacion.html +249 -0
  162. coati_payroll/templates/modules/prestamo/detail.html +443 -0
  163. coati_payroll/templates/modules/prestamo/form.html +203 -0
  164. coati_payroll/templates/modules/prestamo/index.html +150 -0
  165. coati_payroll/templates/modules/prestamo/pago_extraordinario.html +211 -0
  166. coati_payroll/templates/modules/prestamo/tabla_pago_pdf.html +181 -0
  167. coati_payroll/templates/modules/report/admin_index.html +125 -0
  168. coati_payroll/templates/modules/report/detail.html +129 -0
  169. coati_payroll/templates/modules/report/execute.html +266 -0
  170. coati_payroll/templates/modules/report/index.html +95 -0
  171. coati_payroll/templates/modules/report/permissions.html +64 -0
  172. coati_payroll/templates/modules/settings/index.html +274 -0
  173. coati_payroll/templates/modules/shared/concept_form.html +201 -0
  174. coati_payroll/templates/modules/shared/concept_index.html +145 -0
  175. coati_payroll/templates/modules/tipo_planilla/form.html +70 -0
  176. coati_payroll/templates/modules/tipo_planilla/index.html +68 -0
  177. coati_payroll/templates/modules/user/form.html +65 -0
  178. coati_payroll/templates/modules/user/index.html +76 -0
  179. coati_payroll/templates/modules/user/profile.html +81 -0
  180. coati_payroll/templates/modules/vacation/account_detail.html +149 -0
  181. coati_payroll/templates/modules/vacation/account_form.html +52 -0
  182. coati_payroll/templates/modules/vacation/account_index.html +68 -0
  183. coati_payroll/templates/modules/vacation/dashboard.html +156 -0
  184. coati_payroll/templates/modules/vacation/initial_balance_bulk.html +149 -0
  185. coati_payroll/templates/modules/vacation/initial_balance_form.html +93 -0
  186. coati_payroll/templates/modules/vacation/leave_request_detail.html +158 -0
  187. coati_payroll/templates/modules/vacation/leave_request_form.html +61 -0
  188. coati_payroll/templates/modules/vacation/leave_request_index.html +98 -0
  189. coati_payroll/templates/modules/vacation/policy_detail.html +176 -0
  190. coati_payroll/templates/modules/vacation/policy_form.html +152 -0
  191. coati_payroll/templates/modules/vacation/policy_index.html +79 -0
  192. coati_payroll/templates/modules/vacation/register_taken_form.html +178 -0
  193. coati_payroll/translations/en/LC_MESSAGES/messages.mo +0 -0
  194. coati_payroll/translations/en/LC_MESSAGES/messages.po +7283 -0
  195. coati_payroll/translations/es/LC_MESSAGES/messages.mo +0 -0
  196. coati_payroll/translations/es/LC_MESSAGES/messages.po +7374 -0
  197. coati_payroll/vacation_service.py +451 -0
  198. coati_payroll/version.py +18 -0
  199. coati_payroll/vistas/__init__.py +64 -0
  200. coati_payroll/vistas/calculation_rule.py +307 -0
  201. coati_payroll/vistas/carga_inicial_prestacion.py +423 -0
  202. coati_payroll/vistas/config_calculos.py +72 -0
  203. coati_payroll/vistas/configuracion.py +87 -0
  204. coati_payroll/vistas/constants.py +17 -0
  205. coati_payroll/vistas/currency.py +112 -0
  206. coati_payroll/vistas/custom_field.py +120 -0
  207. coati_payroll/vistas/employee.py +305 -0
  208. coati_payroll/vistas/empresa.py +153 -0
  209. coati_payroll/vistas/exchange_rate.py +341 -0
  210. coati_payroll/vistas/liquidacion.py +205 -0
  211. coati_payroll/vistas/payroll_concepts.py +580 -0
  212. coati_payroll/vistas/planilla/__init__.py +38 -0
  213. coati_payroll/vistas/planilla/association_routes.py +238 -0
  214. coati_payroll/vistas/planilla/config_routes.py +158 -0
  215. coati_payroll/vistas/planilla/export_routes.py +175 -0
  216. coati_payroll/vistas/planilla/helpers/__init__.py +34 -0
  217. coati_payroll/vistas/planilla/helpers/association_helpers.py +161 -0
  218. coati_payroll/vistas/planilla/helpers/excel_helpers.py +29 -0
  219. coati_payroll/vistas/planilla/helpers/form_helpers.py +97 -0
  220. coati_payroll/vistas/planilla/nomina_routes.py +488 -0
  221. coati_payroll/vistas/planilla/novedad_routes.py +227 -0
  222. coati_payroll/vistas/planilla/routes.py +145 -0
  223. coati_payroll/vistas/planilla/services/__init__.py +26 -0
  224. coati_payroll/vistas/planilla/services/export_service.py +687 -0
  225. coati_payroll/vistas/planilla/services/nomina_service.py +233 -0
  226. coati_payroll/vistas/planilla/services/novedad_service.py +126 -0
  227. coati_payroll/vistas/planilla/services/planilla_service.py +34 -0
  228. coati_payroll/vistas/planilla/validators/__init__.py +18 -0
  229. coati_payroll/vistas/planilla/validators/planilla_validators.py +40 -0
  230. coati_payroll/vistas/plugins.py +45 -0
  231. coati_payroll/vistas/prestacion.py +272 -0
  232. coati_payroll/vistas/prestamo.py +808 -0
  233. coati_payroll/vistas/report.py +432 -0
  234. coati_payroll/vistas/settings.py +29 -0
  235. coati_payroll/vistas/tipo_planilla.py +134 -0
  236. coati_payroll/vistas/user.py +172 -0
  237. coati_payroll/vistas/vacation.py +1045 -0
  238. coati_payroll-0.0.2.dist-info/LICENSE +201 -0
  239. coati_payroll-0.0.2.dist-info/METADATA +581 -0
  240. coati_payroll-0.0.2.dist-info/RECORD +243 -0
  241. coati_payroll-0.0.2.dist-info/WHEEL +5 -0
  242. coati_payroll-0.0.2.dist-info/entry_points.txt +2 -0
  243. coati_payroll-0.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,81 @@
1
+ # Copyright 2025 BMO Soluciones, S.A.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Formula engine package.
15
+
16
+ This package contains the refactored formula engine with modular architecture:
17
+ - AST parsing and evaluation using Visitor pattern
18
+ - Step execution using Strategy pattern
19
+ - Validation, tables, execution, and results modules
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ # Import from formula_engine_examples
25
+ # EXAMPLE_PROGRESSIVE_TAX_SCHEMA is the new generic name
26
+ # EXAMPLE_IR_NICARAGUA_SCHEMA is kept for backward compatibility (deprecated)
27
+ from coati_payroll.formula_engine_examples import (
28
+ EXAMPLE_PROGRESSIVE_TAX_SCHEMA,
29
+ EXAMPLE_IR_NICARAGUA_SCHEMA,
30
+ )
31
+
32
+ # Import main engine and functions
33
+ from .engine import FormulaEngine, calculate_with_rule, get_available_sources_for_ui
34
+
35
+ # Import exceptions
36
+ from .exceptions import CalculationError, FormulaEngineError, TaxEngineError, ValidationError
37
+
38
+ # Import utilities for backward compatibility
39
+ from .ast.type_converter import safe_divide, to_decimal
40
+
41
+ # Import submodules
42
+ from .ast import (
43
+ ALLOWED_AST_TYPES,
44
+ ASTVisitor,
45
+ COMPARISON_OPERATORS,
46
+ ExpressionEvaluator,
47
+ SAFE_FUNCTIONS,
48
+ SAFE_OPERATORS,
49
+ SafeASTVisitor,
50
+ )
51
+ from .data_sources import AVAILABLE_DATA_SOURCES
52
+ from .novelty_codes import NOVELTY_CODES
53
+
54
+ __all__ = [
55
+ # Main engine
56
+ "FormulaEngine",
57
+ "calculate_with_rule",
58
+ "get_available_sources_for_ui",
59
+ # Exceptions
60
+ "FormulaEngineError",
61
+ "TaxEngineError",
62
+ "ValidationError",
63
+ "CalculationError",
64
+ # Examples
65
+ "EXAMPLE_PROGRESSIVE_TAX_SCHEMA",
66
+ "EXAMPLE_IR_NICARAGUA_SCHEMA", # Deprecated alias for backward compatibility
67
+ # Utilities
68
+ "to_decimal",
69
+ "safe_divide",
70
+ # AST modules
71
+ "ASTVisitor",
72
+ "SafeASTVisitor",
73
+ "ExpressionEvaluator",
74
+ "SAFE_OPERATORS",
75
+ "COMPARISON_OPERATORS",
76
+ "SAFE_FUNCTIONS",
77
+ "ALLOWED_AST_TYPES",
78
+ # Data sources
79
+ "AVAILABLE_DATA_SOURCES",
80
+ "NOVELTY_CODES",
81
+ ]
@@ -0,0 +1,110 @@
1
+ # Copyright 2025 BMO Soluciones, S.A.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """AST parsing and evaluation modules.
15
+
16
+ This package provides secure, enterprise-grade evaluation of mathematical
17
+ expressions for payroll formulas. It implements a whitelist-based security
18
+ model that prevents arbitrary code execution while maintaining financial precision.
19
+
20
+ Security Architecture:
21
+
22
+ 1. **Whitelist-Based Validation**: Only explicitly approved AST node types
23
+ and functions are allowed. Any attempt to use unapproved operations is
24
+ rejected with a clear security violation message.
25
+
26
+ 2. **No Dynamic Code Execution**: The system does NOT use eval(), exec(),
27
+ compile(), or any dynamic method resolution (getattr). All code paths
28
+ are explicit and auditable.
29
+
30
+ 3. **DoS Prevention**: Expression length and AST depth are bounded to prevent
31
+ denial-of-service attacks via extremely long or deeply nested expressions.
32
+
33
+ 4. **Financial Precision**: All calculations use Python's Decimal type to
34
+ maintain precision required for payroll calculations.
35
+
36
+ 5. **Immutable Context**: Variable contexts are read-only during evaluation,
37
+ preventing side effects and ensuring deterministic results.
38
+
39
+ Allowed Operations:
40
+ - Arithmetic: +, -, *, /, //, %, **
41
+ - Functions: min, max, abs, round
42
+ - Variables: Pre-defined in execution context
43
+ - Constants: Numeric literals only
44
+
45
+ Prohibited Operations:
46
+ - File I/O, network access, system calls
47
+ - Import statements, attribute access
48
+ - Lambda functions, list comprehensions
49
+ - Any Python builtin not explicitly whitelisted
50
+
51
+ Usage Example:
52
+ ```python
53
+ from coati_payroll.formula_engine.ast import ExpressionEvaluator
54
+ from decimal import Decimal
55
+
56
+ variables = {
57
+ 'salario_base': Decimal('5000'),
58
+ 'bono': Decimal('1000')
59
+ }
60
+
61
+ evaluator = ExpressionEvaluator(variables)
62
+ result = evaluator.evaluate('salario_base * 1.15 + bono')
63
+ # result = Decimal('6750')
64
+ ```
65
+
66
+ Security Notes:
67
+ - This module is designed for use with untrusted input (JSON rules)
68
+ - All security validations are fail-safe (reject by default)
69
+ - Adding new functions or operators requires security review
70
+ - Regular security audits are recommended
71
+ """
72
+
73
+ from .ast_visitor import ASTVisitor, SafeASTVisitor
74
+ from .expression_evaluator import ExpressionEvaluator
75
+ from .safe_operators import (
76
+ SAFE_OPERATORS,
77
+ COMPARISON_OPERATORS,
78
+ SAFE_FUNCTIONS,
79
+ ALLOWED_AST_TYPES,
80
+ MAX_EXPRESSION_LENGTH,
81
+ MAX_AST_DEPTH,
82
+ MAX_FUNCTION_ARGS,
83
+ validate_safe_function_call,
84
+ )
85
+ from .type_converter import (
86
+ to_decimal,
87
+ safe_divide,
88
+ MAX_DECIMAL_DIGITS,
89
+ MAX_DECIMAL_VALUE,
90
+ MIN_DECIMAL_VALUE,
91
+ )
92
+
93
+ __all__ = [
94
+ "ASTVisitor",
95
+ "SafeASTVisitor",
96
+ "ExpressionEvaluator",
97
+ "SAFE_OPERATORS",
98
+ "COMPARISON_OPERATORS",
99
+ "SAFE_FUNCTIONS",
100
+ "ALLOWED_AST_TYPES",
101
+ "MAX_EXPRESSION_LENGTH",
102
+ "MAX_AST_DEPTH",
103
+ "MAX_FUNCTION_ARGS",
104
+ "validate_safe_function_call",
105
+ "to_decimal",
106
+ "safe_divide",
107
+ "MAX_DECIMAL_DIGITS",
108
+ "MAX_DECIMAL_VALUE",
109
+ "MIN_DECIMAL_VALUE",
110
+ ]
@@ -0,0 +1,259 @@
1
+ # Copyright 2025 BMO Soluciones, S.A.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """AST Visitor pattern for safe expression evaluation.
15
+
16
+ This module implements a secure AST visitor that evaluates mathematical expressions
17
+ without using dynamic method dispatch (getattr). All node types are explicitly
18
+ handled to prevent any possibility of code injection or unexpected behavior.
19
+
20
+ Security Features:
21
+ - Explicit visitor methods for each allowed node type
22
+ - No dynamic method resolution (no getattr/setattr)
23
+ - Whitelist-based approach - unknown nodes are rejected
24
+ - All operations maintain Decimal precision
25
+ - Division by zero is handled safely
26
+
27
+ The visitor pattern ensures that only pre-approved AST node types can be processed,
28
+ and each type has an explicit, auditable handler method.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ from abc import ABC, abstractmethod
34
+ from decimal import Decimal
35
+
36
+ import ast
37
+
38
+ from ..exceptions import CalculationError
39
+ from .safe_operators import SAFE_FUNCTIONS, SAFE_OPERATORS
40
+ from .type_converter import to_decimal
41
+
42
+
43
+ class ASTVisitor(ABC):
44
+ """Visitor pattern base class for AST traversal.
45
+
46
+ This abstract base class defines the interface for AST visitors.
47
+ Implementations must provide a visit() method that safely evaluates
48
+ AST nodes and returns Decimal results.
49
+ """
50
+
51
+ @abstractmethod
52
+ def visit(self, node: ast.AST) -> Decimal:
53
+ """Visit and evaluate an AST node.
54
+
55
+ Args:
56
+ node: The AST node to visit and evaluate
57
+
58
+ Returns:
59
+ The evaluated result as a Decimal
60
+
61
+ Raises:
62
+ CalculationError: If the node cannot be safely evaluated
63
+ """
64
+ pass
65
+
66
+
67
+ class SafeASTVisitor(ASTVisitor):
68
+ """Safe visitor for evaluating mathematical expressions.
69
+
70
+ This visitor implements a secure evaluation strategy:
71
+ 1. Explicit dispatch - no dynamic method resolution
72
+ 2. Whitelist approach - only known node types are processed
73
+ 3. Immutable context - variables are read-only during evaluation
74
+ 4. Decimal precision - all numeric operations maintain precision
75
+ 5. Safe error handling - division by zero returns 0
76
+
77
+ Security Note:
78
+ This class does NOT use getattr() or any dynamic dispatch mechanism.
79
+ All node types are handled by explicit if/elif chains to prevent
80
+ any possibility of method injection or unexpected behavior.
81
+ """
82
+
83
+ def __init__(self, variables: dict[str, Decimal]):
84
+ """Initialize visitor with variable context.
85
+
86
+ Args:
87
+ variables: Dictionary of variable names to Decimal values.
88
+ This dictionary is not modified during evaluation.
89
+ """
90
+ self.variables = variables
91
+
92
+ def visit(self, node: ast.AST) -> Decimal:
93
+ """Visit and evaluate an AST node using explicit dispatch.
94
+
95
+ This method uses explicit type checking with match/case instead of
96
+ dynamic method resolution (getattr) for security and maintainability.
97
+ Each node type is explicitly handled, making the code easier to audit
98
+ and preventing any possibility of method injection.
99
+
100
+ Args:
101
+ node: AST node to evaluate
102
+
103
+ Returns:
104
+ Decimal result of evaluation
105
+
106
+ Raises:
107
+ CalculationError: If the node type is not supported or evaluation fails
108
+ """
109
+ match node:
110
+ case ast.Constant():
111
+ return self.visit_constant(node)
112
+ case ast.Name():
113
+ return self.visit_name(node)
114
+ case ast.BinOp():
115
+ return self.visit_binop(node)
116
+ case ast.UnaryOp():
117
+ return self.visit_unaryop(node)
118
+ case ast.Call():
119
+ return self.visit_call(node)
120
+ case _:
121
+ raise CalculationError(
122
+ f"Unsupported AST node type: {type(node).__name__}. "
123
+ "Only Constant, Name, BinOp, UnaryOp, and Call nodes are allowed."
124
+ )
125
+
126
+ def visit_constant(self, node: ast.Constant) -> Decimal:
127
+ """Visit a constant node (numeric literal).
128
+
129
+ Args:
130
+ node: AST Constant node containing a numeric value
131
+
132
+ Returns:
133
+ The constant value as a Decimal
134
+
135
+ Raises:
136
+ CalculationError: If the constant cannot be converted to Decimal
137
+ """
138
+ return to_decimal(node.value)
139
+
140
+ def visit_name(self, node: ast.Name) -> Decimal:
141
+ """Visit a variable name node.
142
+
143
+ Args:
144
+ node: AST Name node representing a variable reference
145
+
146
+ Returns:
147
+ The value of the variable from the context
148
+
149
+ Raises:
150
+ CalculationError: If the variable is not defined in the context
151
+ """
152
+ if node.id not in self.variables:
153
+ raise CalculationError(
154
+ f"Undefined variable: '{node.id}'. " f"Available variables: {', '.join(sorted(self.variables.keys()))}"
155
+ )
156
+ return self.variables[node.id]
157
+
158
+ def visit_binop(self, node: ast.BinOp) -> Decimal:
159
+ """Visit a binary operation node (e.g., a + b, x * y).
160
+
161
+ Args:
162
+ node: AST BinOp node representing a binary operation
163
+
164
+ Returns:
165
+ The result of the binary operation as a Decimal
166
+
167
+ Raises:
168
+ CalculationError: If the operator is not whitelisted or operation fails
169
+ """
170
+ left = self.visit(node.left)
171
+ right = self.visit(node.right)
172
+
173
+ op_type = type(node.op)
174
+ op_func = SAFE_OPERATORS.get(op_type)
175
+ if not op_func:
176
+ raise CalculationError(
177
+ f"Operator '{op_type.__name__}' is not allowed. " f"Allowed operators: +, -, *, /, //, %, **"
178
+ )
179
+
180
+ if op_type in (ast.Div, ast.FloorDiv, ast.Mod) and right == 0:
181
+ return Decimal("0")
182
+
183
+ try:
184
+ result = op_func(left, right)
185
+ return to_decimal(result)
186
+ except (OverflowError, ValueError) as e:
187
+ raise CalculationError(f"Arithmetic error in operation '{left} {op_type.__name__} {right}': {e}") from e
188
+ except Exception as e:
189
+ raise CalculationError(f"Unexpected error in binary operation: {e}") from e
190
+
191
+ def visit_unaryop(self, node: ast.UnaryOp) -> Decimal:
192
+ """Visit a unary operation node (e.g., -x, +y).
193
+
194
+ Args:
195
+ node: AST UnaryOp node representing a unary operation
196
+
197
+ Returns:
198
+ The result of the unary operation as a Decimal
199
+
200
+ Raises:
201
+ CalculationError: If the unary operator is not allowed
202
+ """
203
+ operand = self.visit(node.operand)
204
+
205
+ op_type = type(node.op)
206
+ if op_type == ast.UAdd:
207
+ return to_decimal(+operand)
208
+ elif op_type == ast.USub:
209
+ return to_decimal(-operand)
210
+ else:
211
+ raise CalculationError(
212
+ f"Unary operator '{op_type.__name__}' is not allowed. " "Only unary + and - are permitted."
213
+ )
214
+
215
+ def visit_call(self, node: ast.Call) -> Decimal:
216
+ """Visit a function call node (e.g., max(a, b), round(x, 2)).
217
+
218
+ Args:
219
+ node: AST Call node representing a function call
220
+
221
+ Returns:
222
+ The result of the function call as a Decimal
223
+
224
+ Raises:
225
+ CalculationError: If the function is not whitelisted or call fails
226
+ """
227
+ if not isinstance(node.func, ast.Name):
228
+ raise CalculationError(
229
+ "Only direct named function calls are allowed. " "Attribute access and lambda functions are prohibited."
230
+ )
231
+
232
+ func_name = node.func.id
233
+ if func_name not in SAFE_FUNCTIONS:
234
+ raise CalculationError(
235
+ f"Function '{func_name}' is not in the whitelist. "
236
+ f"Allowed functions: {', '.join(sorted(SAFE_FUNCTIONS.keys()))}"
237
+ )
238
+
239
+ if node.keywords:
240
+ raise CalculationError(
241
+ f"Keyword arguments are not allowed in function '{func_name}'. " "Use positional arguments only."
242
+ )
243
+
244
+ args = [self.visit(arg) for arg in node.args]
245
+
246
+ try:
247
+ if func_name == "round" and len(args) > 1:
248
+ if args[1] < 0 or args[1] > 10:
249
+ raise CalculationError(f"round() precision must be between 0 and 10, got {args[1]}")
250
+ result = SAFE_FUNCTIONS[func_name](args[0], int(args[1]))
251
+ else:
252
+ result = SAFE_FUNCTIONS[func_name](*args)
253
+ return to_decimal(result)
254
+ except TypeError as e:
255
+ raise CalculationError(f"Invalid arguments for function '{func_name}': {e}") from e
256
+ except ValueError as e:
257
+ raise CalculationError(f"Invalid value in function '{func_name}': {e}") from e
258
+ except Exception as e:
259
+ raise CalculationError(f"Error calling function '{func_name}': {e}") from e
@@ -0,0 +1,228 @@
1
+ # Copyright 2025 BMO Soluciones, S.A.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Expression evaluator using AST visitor pattern.
15
+
16
+ This module provides secure evaluation of mathematical expressions from JSON rules.
17
+ It implements multiple layers of security:
18
+
19
+ 1. Expression Length Validation: Prevents DoS attacks via extremely long expressions
20
+ 2. AST Depth Validation: Prevents stack overflow from deeply nested expressions
21
+ 3. Whitelist-based AST Validation: Only approved node types are allowed
22
+ 4. Safe Visitor Pattern: No dynamic code execution or attribute access
23
+ 5. Decimal Precision: All calculations maintain financial precision
24
+
25
+ Security Model:
26
+ - Input expressions are parsed into Abstract Syntax Trees (AST)
27
+ - AST is validated against a whitelist of allowed node types
28
+ - AST depth is checked to prevent stack overflow
29
+ - Evaluation uses explicit visitor pattern (no eval/exec/compile)
30
+ - All operations are deterministic and side-effect free
31
+
32
+ Example Safe Expression:
33
+ 'salario_base * 1.15 + max(bono, 1000)'
34
+
35
+ Example Unsafe Expression (rejected):
36
+ '__import__("os").system("rm -rf /")'
37
+ 'open("/etc/passwd").read()'
38
+ '[x for x in range(1000000)]'
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import ast
44
+ from decimal import Decimal
45
+ from typing import Callable
46
+
47
+ from coati_payroll.i18n import _
48
+ from coati_payroll.log import TRACE_LEVEL_NUM, is_trace_enabled, log
49
+
50
+ from ..exceptions import CalculationError
51
+ from .ast_visitor import SafeASTVisitor
52
+ from .safe_operators import ALLOWED_AST_TYPES, MAX_EXPRESSION_LENGTH, MAX_AST_DEPTH
53
+ from .type_converter import to_decimal
54
+
55
+
56
+ class ExpressionEvaluator:
57
+ """Evaluates mathematical expressions safely using AST.
58
+
59
+ This class provides enterprise-grade secure expression evaluation for
60
+ payroll formulas. It prevents code injection, DoS attacks, and ensures
61
+ all calculations are deterministic and auditable.
62
+
63
+ Security Features:
64
+ - Expression length limits (prevents DoS)
65
+ - AST depth limits (prevents stack overflow)
66
+ - Whitelist-based validation (prevents code injection)
67
+ - No dynamic code execution (no eval/exec/compile)
68
+ - Immutable variable context (prevents side effects)
69
+ - Comprehensive error messages for debugging
70
+
71
+ Thread Safety:
72
+ This class is thread-safe as long as the variables dictionary is not
73
+ modified during evaluation. Each evaluation creates a new visitor instance.
74
+ """
75
+
76
+ def __init__(self, variables: dict[str, Decimal], trace_callback: Callable[[str], None] | None = None):
77
+ """Initialize expression evaluator.
78
+
79
+ Args:
80
+ variables: Dictionary of variable names to Decimal values.
81
+ This dictionary is not modified during evaluation.
82
+ trace_callback: Optional callback for trace logging.
83
+ Should be thread-safe if used in concurrent contexts.
84
+ """
85
+ self.variables = variables
86
+ self.trace_callback = trace_callback or self._default_trace
87
+
88
+ def _default_trace(self, message: str) -> None:
89
+ """Default trace callback."""
90
+ if is_trace_enabled():
91
+ try:
92
+ log.log(TRACE_LEVEL_NUM, message)
93
+ except Exception:
94
+ pass
95
+
96
+ def evaluate(self, expression: str) -> Decimal:
97
+ """Safely evaluate a mathematical expression using AST.
98
+
99
+ This method implements multiple layers of security validation:
100
+ 1. Input validation (type, length)
101
+ 2. Syntax validation (AST parsing)
102
+ 3. Security validation (whitelist checking)
103
+ 4. Depth validation (stack overflow prevention)
104
+ 5. Safe evaluation (visitor pattern)
105
+
106
+ Args:
107
+ expression: Mathematical expression string (e.g., 'a + b * 2')
108
+
109
+ Returns:
110
+ Result of the expression as Decimal
111
+
112
+ Raises:
113
+ CalculationError: If expression is invalid, unsafe, or evaluation fails
114
+
115
+ Examples:
116
+ >>> evaluator = ExpressionEvaluator({'x': Decimal('10'), 'y': Decimal('5')})
117
+ >>> evaluator.evaluate('x + y')
118
+ Decimal('15')
119
+ >>> evaluator.evaluate('max(x, y) * 2')
120
+ Decimal('20')
121
+ """
122
+ if not expression or not isinstance(expression, str):
123
+ return Decimal("0")
124
+
125
+ expression = expression.strip()
126
+ if not expression:
127
+ return Decimal("0")
128
+
129
+ if len(expression) > MAX_EXPRESSION_LENGTH:
130
+ raise CalculationError(
131
+ f"Expression too long ({len(expression)} characters). "
132
+ f"Maximum allowed: {MAX_EXPRESSION_LENGTH} characters. "
133
+ "This limit prevents denial-of-service attacks."
134
+ )
135
+
136
+ self.trace_callback(_("Evaluando expresión: '%(expr)s'") % {"expr": expression})
137
+
138
+ try:
139
+ tree = ast.parse(expression, mode="eval")
140
+
141
+ self._validate_ast_security(tree.body)
142
+ self._validate_ast_depth(tree.body)
143
+
144
+ visitor = SafeASTVisitor(self.variables)
145
+ result = visitor.visit(tree.body)
146
+ final_result = to_decimal(result)
147
+
148
+ self.trace_callback(
149
+ _("Resultado expresión '%(expr)s' => %(res)s") % {"expr": expression, "res": final_result}
150
+ )
151
+ return final_result
152
+ except SyntaxError as e:
153
+ raise CalculationError(
154
+ f"Invalid expression syntax in '{expression}': {e}. "
155
+ "Check for unmatched parentheses, invalid operators, or typos."
156
+ ) from e
157
+ except ZeroDivisionError:
158
+ return Decimal("0")
159
+ except CalculationError:
160
+ raise
161
+ except Exception as e:
162
+ raise CalculationError(f"Unexpected error evaluating expression '{expression}': {e}") from e
163
+
164
+ def _validate_ast_security(self, node: ast.AST) -> None:
165
+ """Validate that an AST node only contains safe operations.
166
+
167
+ This method implements a whitelist-based security model. It walks the
168
+ entire AST and verifies that every node is in the allowed list. This
169
+ prevents code injection attacks like:
170
+ - Import statements: __import__('os').system('rm -rf /')
171
+ - Attribute access: obj.__class__.__bases__[0].__subclasses__()
172
+ - List comprehensions: [x for x in range(999999999)]
173
+ - Lambda functions: (lambda: exec('malicious code'))()
174
+
175
+ Args:
176
+ node: AST node to validate (typically the root of the expression tree)
177
+
178
+ Raises:
179
+ CalculationError: If any unsafe operation is detected
180
+ """
181
+ for child in ast.walk(node):
182
+ if not isinstance(child, ALLOWED_AST_TYPES):
183
+ raise CalculationError(
184
+ f"Security violation: AST node type '{child.__class__.__name__}' is not allowed. "
185
+ "Only basic arithmetic operations and whitelisted functions are permitted. "
186
+ "This restriction prevents code injection and arbitrary code execution."
187
+ )
188
+
189
+ if isinstance(child, ast.Call):
190
+ if not isinstance(child.func, ast.Name):
191
+ raise CalculationError(
192
+ "Security violation: Only direct named function calls are allowed. "
193
+ "Attribute access (e.g., obj.method()) and lambda functions are prohibited."
194
+ )
195
+ from .safe_operators import SAFE_FUNCTIONS
196
+
197
+ if child.func.id not in SAFE_FUNCTIONS:
198
+ raise CalculationError(
199
+ f"Security violation: Function '{child.func.id}' is not in the whitelist. "
200
+ f"Allowed functions: {', '.join(sorted(SAFE_FUNCTIONS.keys()))}. "
201
+ "This restriction prevents execution of arbitrary Python functions."
202
+ )
203
+
204
+ def _validate_ast_depth(self, node: ast.AST, current_depth: int = 0) -> None:
205
+ """Validate that AST depth does not exceed maximum to prevent stack overflow.
206
+
207
+ Deeply nested expressions can cause stack overflow during evaluation:
208
+ - Example: ((((((((((x + 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1)
209
+
210
+ This validation prevents denial-of-service attacks via deeply nested
211
+ expressions that could exhaust the call stack.
212
+
213
+ Args:
214
+ node: AST node to validate
215
+ current_depth: Current depth in the tree (used for recursion)
216
+
217
+ Raises:
218
+ CalculationError: If AST depth exceeds maximum allowed
219
+ """
220
+ if current_depth > MAX_AST_DEPTH:
221
+ raise CalculationError(
222
+ f"Expression too complex: AST depth ({current_depth}) exceeds maximum ({MAX_AST_DEPTH}). "
223
+ "Simplify the expression by breaking it into multiple steps. "
224
+ "This limit prevents stack overflow attacks."
225
+ )
226
+
227
+ for child in ast.iter_child_nodes(node):
228
+ self._validate_ast_depth(child, current_depth + 1)