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,131 @@
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
+ """Safe operators and functions for expression evaluation.
15
+
16
+ This module defines the ONLY operators and functions allowed in formula expressions.
17
+ It implements a whitelist-based security model to prevent arbitrary code execution.
18
+
19
+ Security Guarantees:
20
+ - Only mathematical operations are allowed (no I/O, no imports, no system calls)
21
+ - No access to Python builtins beyond explicitly whitelisted functions
22
+ - No attribute access or dynamic code execution
23
+ - All operations are deterministic and side-effect free
24
+ - Expression complexity is bounded to prevent DoS attacks
25
+
26
+ Allowed Operations:
27
+ - Arithmetic: +, -, *, /, //, %, **
28
+ - Functions: min, max, abs, round
29
+ - Variables: Only pre-defined variables from the execution context
30
+ - Constants: Numeric literals only
31
+
32
+ Prohibited Operations:
33
+ - File I/O, network access, system calls
34
+ - Import statements, eval, exec, compile
35
+ - Attribute access (__getattr__, __setattr__, etc.)
36
+ - Lambda functions, list comprehensions
37
+ - Class definitions, decorators
38
+ - Any Python builtin not explicitly whitelisted
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import ast
44
+ import operator
45
+ from typing import Any, Callable
46
+
47
+ MAX_EXPRESSION_LENGTH = 1000
48
+ MAX_AST_DEPTH = 50
49
+ MAX_FUNCTION_ARGS = 20
50
+
51
+
52
+ def validate_safe_function_call(func_name: str, args: list[Any]) -> None:
53
+ """Validate that a function call is safe.
54
+
55
+ Args:
56
+ func_name: Name of the function being called
57
+ args: Arguments passed to the function
58
+
59
+ Raises:
60
+ ValueError: If the function call is unsafe
61
+ """
62
+ if func_name not in SAFE_FUNCTIONS:
63
+ raise ValueError(
64
+ f"Function '{func_name}' is not in the whitelist. " f"Allowed functions: {', '.join(SAFE_FUNCTIONS.keys())}"
65
+ )
66
+
67
+ if len(args) > MAX_FUNCTION_ARGS:
68
+ raise ValueError(
69
+ f"Too many arguments ({len(args)}) for function '{func_name}'. " f"Maximum allowed: {MAX_FUNCTION_ARGS}"
70
+ )
71
+
72
+ if func_name == "round" and len(args) > 2:
73
+ raise ValueError("round() accepts at most 2 arguments")
74
+
75
+ if func_name in ("min", "max") and len(args) < 1:
76
+ raise ValueError(f"{func_name}() requires at least 1 argument")
77
+
78
+
79
+ # Safe operators for expression evaluation
80
+ SAFE_OPERATORS = {
81
+ ast.Add: operator.add,
82
+ ast.Sub: operator.sub,
83
+ ast.Mult: operator.mul,
84
+ ast.Div: operator.truediv,
85
+ ast.FloorDiv: operator.floordiv,
86
+ ast.Mod: operator.mod,
87
+ ast.Pow: operator.pow,
88
+ }
89
+
90
+ # Safe comparison operators for conditional evaluation
91
+ COMPARISON_OPERATORS: dict[str, Callable[[Any, Any], bool]] = {
92
+ ">": operator.gt,
93
+ ">=": operator.ge,
94
+ "<": operator.lt,
95
+ "<=": operator.le,
96
+ "==": operator.eq,
97
+ "!=": operator.ne,
98
+ }
99
+
100
+ # Safe functions for calculations - WHITELIST ONLY
101
+ # These are the ONLY functions allowed in formula expressions.
102
+ # Adding functions here requires security review.
103
+ SAFE_FUNCTIONS: dict[str, Callable[..., Any]] = {
104
+ "min": min,
105
+ "max": max,
106
+ "abs": abs,
107
+ "round": round,
108
+ }
109
+
110
+ # Allowed AST node types for security validation - WHITELIST ONLY
111
+ # These are the ONLY AST node types allowed in parsed expressions.
112
+ # Any other node type will be rejected as a security violation.
113
+ # This prevents code injection, attribute access, imports, etc.
114
+ ALLOWED_AST_TYPES: tuple[type[ast.AST], ...] = (
115
+ ast.Expression,
116
+ ast.Constant,
117
+ ast.Name,
118
+ ast.Load,
119
+ ast.BinOp,
120
+ ast.UnaryOp,
121
+ ast.Call,
122
+ ast.Add,
123
+ ast.Sub,
124
+ ast.Mult,
125
+ ast.Div,
126
+ ast.FloorDiv,
127
+ ast.Mod,
128
+ ast.Pow,
129
+ ast.UAdd,
130
+ ast.USub,
131
+ )
@@ -0,0 +1,172 @@
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
+ """Type conversion utilities for formula engine.
15
+
16
+ This module provides safe type conversion functions that maintain financial
17
+ precision and prevent common security issues:
18
+
19
+ 1. Decimal Precision: All numeric values are converted to Decimal to avoid
20
+ floating-point precision errors in financial calculations.
21
+
22
+ 2. Input Validation: Values are validated to prevent:
23
+ - Infinity and NaN values
24
+ - Extremely large numbers that could cause DoS
25
+ - Invalid type conversions
26
+
27
+ 3. Safe Division: Division by zero is handled gracefully by returning 0
28
+ instead of raising an exception.
29
+
30
+ Security Considerations:
31
+ - All conversions are deterministic and side-effect free
32
+ - No dynamic type inspection or attribute access
33
+ - Bounded numeric ranges prevent resource exhaustion
34
+ - Explicit error messages aid in debugging and auditing
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ from decimal import Decimal, InvalidOperation
40
+ from typing import Any
41
+ import math
42
+
43
+ from ..exceptions import ValidationError
44
+
45
+ MAX_DECIMAL_DIGITS = 28
46
+ MAX_DECIMAL_VALUE = Decimal("9" * MAX_DECIMAL_DIGITS)
47
+ MIN_DECIMAL_VALUE = -MAX_DECIMAL_VALUE
48
+
49
+
50
+ def to_decimal(value: Any) -> Decimal:
51
+ """Safely convert a value to Decimal with validation.
52
+
53
+ This function converts various Python types to Decimal while enforcing
54
+ security constraints:
55
+ - Rejects infinity and NaN values
56
+ - Validates numeric ranges to prevent DoS
57
+ - Maintains financial precision
58
+ - Provides clear error messages
59
+
60
+ Args:
61
+ value: Value to convert. Supported types:
62
+ - None: converts to 0
63
+ - Decimal: returned as-is (after validation)
64
+ - bool: True -> 1, False -> 0
65
+ - int, float, str: converted to Decimal
66
+
67
+ Returns:
68
+ Decimal representation of the value
69
+
70
+ Raises:
71
+ ValidationError: If value cannot be safely converted to Decimal
72
+
73
+ Examples:
74
+ >>> to_decimal(42)
75
+ Decimal('42')
76
+ >>> to_decimal('123.45')
77
+ Decimal('123.45')
78
+ >>> to_decimal(True)
79
+ Decimal('1')
80
+ >>> to_decimal(None)
81
+ Decimal('0')
82
+ """
83
+ if value is None:
84
+ return Decimal("0")
85
+
86
+ if isinstance(value, Decimal):
87
+ _validate_decimal_range(value)
88
+ return value
89
+
90
+ if isinstance(value, bool):
91
+ return Decimal("1") if value else Decimal("0")
92
+
93
+ if isinstance(value, float):
94
+ if math.isnan(value):
95
+ raise ValidationError(
96
+ "Cannot convert NaN (Not a Number) to Decimal. "
97
+ "This may indicate a calculation error in the input data."
98
+ )
99
+ if math.isinf(value):
100
+ raise ValidationError(
101
+ "Cannot convert infinity to Decimal. " "Check for division by zero or overflow in input calculations."
102
+ )
103
+
104
+ try:
105
+ result = Decimal(str(value))
106
+ _validate_decimal_range(result)
107
+ return result
108
+ except (InvalidOperation, ValueError) as e:
109
+ raise ValidationError(
110
+ f"Cannot convert value '{value}' (type: {type(value).__name__}) to Decimal: {e}. "
111
+ "Ensure the value is a valid number."
112
+ ) from e
113
+ except Exception as e:
114
+ raise ValidationError(f"Unexpected error converting '{value}' to Decimal: {e}") from e
115
+
116
+
117
+ def _validate_decimal_range(value: Decimal) -> None:
118
+ """Validate that a Decimal value is within acceptable range.
119
+
120
+ This prevents denial-of-service attacks via extremely large numbers
121
+ that could consume excessive memory or CPU during calculations.
122
+
123
+ Args:
124
+ value: Decimal value to validate
125
+
126
+ Raises:
127
+ ValidationError: If value is outside acceptable range
128
+ """
129
+ if value.is_nan():
130
+ raise ValidationError("Decimal value is NaN (Not a Number). " "This indicates an invalid calculation result.")
131
+
132
+ if value.is_infinite():
133
+ raise ValidationError("Decimal value is infinite. " "This indicates an overflow or division by zero.")
134
+
135
+ if value > MAX_DECIMAL_VALUE or value < MIN_DECIMAL_VALUE:
136
+ raise ValidationError(
137
+ f"Decimal value {value} is outside acceptable range "
138
+ f"[{MIN_DECIMAL_VALUE}, {MAX_DECIMAL_VALUE}]. "
139
+ f"This limit ({MAX_DECIMAL_DIGITS} digits) prevents resource exhaustion attacks."
140
+ )
141
+
142
+
143
+ def safe_divide(numerator: Decimal, denominator: Decimal) -> Decimal:
144
+ """Safely divide two decimals, handling division by zero.
145
+
146
+ This function provides safe division for payroll calculations where
147
+ division by zero should return 0 rather than raising an exception.
148
+ This is common in formulas like: bonus = sales / days_worked
149
+ where days_worked might be 0 for new employees.
150
+
151
+ Args:
152
+ numerator: The dividend (value to be divided)
153
+ denominator: The divisor (value to divide by)
154
+
155
+ Returns:
156
+ Result of division, or Decimal('0') if denominator is 0
157
+
158
+ Examples:
159
+ >>> safe_divide(Decimal('100'), Decimal('5'))
160
+ Decimal('20')
161
+ >>> safe_divide(Decimal('100'), Decimal('0'))
162
+ Decimal('0')
163
+ """
164
+ if denominator == 0:
165
+ return Decimal("0")
166
+
167
+ try:
168
+ result = numerator / denominator
169
+ _validate_decimal_range(result)
170
+ return result
171
+ except (InvalidOperation, ValueError) as e:
172
+ raise ValidationError(f"Error in division {numerator} / {denominator}: {e}") from e