brynq-sdk-alight 1.0.0__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.
Files changed (44) hide show
  1. brynq_sdk_alight/__init__.py +1019 -0
  2. brynq_sdk_alight/address.py +72 -0
  3. brynq_sdk_alight/archive/flat_wrapper.py +139 -0
  4. brynq_sdk_alight/archive/hrxml_generator.py +280 -0
  5. brynq_sdk_alight/archive/managers.py +132 -0
  6. brynq_sdk_alight/archive/managers_generic.py +114 -0
  7. brynq_sdk_alight/archive/managers_old_complex.py +294 -0
  8. brynq_sdk_alight/archive/managers_simple.py +229 -0
  9. brynq_sdk_alight/employee.py +81 -0
  10. brynq_sdk_alight/job.py +89 -0
  11. brynq_sdk_alight/leave.py +97 -0
  12. brynq_sdk_alight/pay_elements.py +97 -0
  13. brynq_sdk_alight/salary.py +89 -0
  14. brynq_sdk_alight/schemas/__init__.py +26 -0
  15. brynq_sdk_alight/schemas/absence.py +83 -0
  16. brynq_sdk_alight/schemas/address.py +113 -0
  17. brynq_sdk_alight/schemas/employee.py +641 -0
  18. brynq_sdk_alight/schemas/generated_envelope_xsd_schema/__init__.py +38683 -0
  19. brynq_sdk_alight/schemas/generated_envelope_xsd_schema/process_pay_serv_emp.py +622264 -0
  20. brynq_sdk_alight/schemas/generated_xsd_schemas/__init__.py +10965 -0
  21. brynq_sdk_alight/schemas/generated_xsd_schemas/csec_person.py +39808 -0
  22. brynq_sdk_alight/schemas/generated_xsd_schemas/hrxml_indicative_data.py +90318 -0
  23. brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_bod.py +33869 -0
  24. brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_code_list_currency_code_iso_7_04.py +365 -0
  25. brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_code_list_language_code_iso_7_04.py +16 -0
  26. brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_code_list_mimemedia_type_code_iana_7_04.py +16 -0
  27. brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_code_list_unit_code_unece_7_04.py +14 -0
  28. brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_code_lists.py +535 -0
  29. brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_qualified_data_types.py +84 -0
  30. brynq_sdk_alight/schemas/generated_xsd_schemas/openapplications_unqualified_data_types.py +1449 -0
  31. brynq_sdk_alight/schemas/job.py +129 -0
  32. brynq_sdk_alight/schemas/leave.py +58 -0
  33. brynq_sdk_alight/schemas/payments.py +207 -0
  34. brynq_sdk_alight/schemas/salary.py +67 -0
  35. brynq_sdk_alight/schemas/termination.py +48 -0
  36. brynq_sdk_alight/schemas/timequota.py +66 -0
  37. brynq_sdk_alight/schemas/utils.py +452 -0
  38. brynq_sdk_alight/termination.py +103 -0
  39. brynq_sdk_alight/time_elements.py +121 -0
  40. brynq_sdk_alight/time_quotas.py +114 -0
  41. brynq_sdk_alight-1.0.0.dist-info/METADATA +20 -0
  42. brynq_sdk_alight-1.0.0.dist-info/RECORD +44 -0
  43. brynq_sdk_alight-1.0.0.dist-info/WHEEL +5 -0
  44. brynq_sdk_alight-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,114 @@
1
+ """
2
+ Fully generic manager for the Alight SDK.
3
+ Pure XSD schema-driven transformations - zero hardcoded mappings.
4
+ """
5
+
6
+ from typing import Dict, Any, Optional, TYPE_CHECKING, List, get_origin, get_args
7
+ from pydantic import BaseModel
8
+
9
+ from .schemas.hrxml_indicative_data import IndicativeDataType
10
+
11
+ if TYPE_CHECKING:
12
+ from . import Alight
13
+
14
+
15
+ class EmployeeManager:
16
+ """Fully generic employee manager - pure XSD schema-driven conversion."""
17
+
18
+ def __init__(self, alight_sdk: 'Alight'):
19
+ self.alight_sdk = alight_sdk
20
+
21
+ def create_newhire_simple(self, flat_data: Dict[str, Any],
22
+ logical_id: Optional[str] = None,
23
+ pretty_print: bool = True,
24
+ save_to_file: bool = False,
25
+ filename: Optional[str] = None) -> str:
26
+ """
27
+ Create NewHire XML from flat dictionary using pure XSD schema conversion.
28
+ """
29
+ nested_data = self._flat_to_nested(flat_data)
30
+ return self.alight_sdk.generate_newhire_xml(
31
+ person_data=nested_data,
32
+ logical_id=logical_id,
33
+ pretty_print=pretty_print
34
+ )
35
+
36
+ def create_employee_change_simple(self, flat_data: Dict[str, Any],
37
+ logical_id: Optional[str] = None,
38
+ pretty_print: bool = True,
39
+ save_to_file: bool = False,
40
+ filename: Optional[str] = None) -> str:
41
+ """
42
+ Create Employee Change XML from flat dictionary using pure XSD schema conversion.
43
+ """
44
+ nested_data = self._flat_to_nested(flat_data)
45
+ return self.alight_sdk.generate_employee_change_xml(
46
+ person_data=nested_data,
47
+ logical_id=logical_id,
48
+ pretty_print=pretty_print
49
+ )
50
+
51
+ def _flat_to_nested(self, flat_data: Dict[str, Any]) -> Dict[str, Any]:
52
+ """
53
+ Convert flat data to nested using PURE XSD schema inspection.
54
+ Zero hardcoded mappings - everything is generic and schema-driven.
55
+ """
56
+ # Use the generic XSD-aware conversion directly on the main schema
57
+ return self._xsd_flat_to_nested(flat_data, IndicativeDataType)
58
+
59
+ def _xsd_flat_to_nested(self, flat_dict: Dict[str, Any], model: BaseModel) -> Dict[str, Any]:
60
+ """
61
+ Generic XSD-aware version exactly like your Functions.flat_dict_to_nested_dict.
62
+ Recursively processes the schema and handles XSD {"value": ...} format automatically.
63
+ """
64
+ nested = {}
65
+
66
+ for name, field in model.model_fields.items():
67
+ key_in_input = name # Original model field name as key in flat_dict
68
+ alias = field.alias or name
69
+
70
+ # Handle nested BaseModel fields (recursively) - exact same logic as your function
71
+ if isinstance(field.annotation, type) and issubclass(field.annotation, BaseModel):
72
+ nested[alias] = self._xsd_flat_to_nested(flat_dict, field.annotation)
73
+
74
+ # Handle Union types with BaseModel (like Optional[SomeModel]) - exact same logic
75
+ elif any(isinstance(item, type) and issubclass(item, BaseModel) for item in get_args(field.annotation)):
76
+ # Get the BaseModel class from the Union
77
+ nested_model = next(item for item in get_args(field.annotation)
78
+ if isinstance(item, type) and issubclass(item, BaseModel))
79
+ nested[alias] = self._xsd_flat_to_nested(flat_dict, nested_model)
80
+
81
+ # Handle primitive fields - your logic + XSD value wrapping
82
+ else:
83
+ if key_in_input in flat_dict:
84
+ value = flat_dict[key_in_input]
85
+
86
+ # XSD enhancement: Check if this field type needs {"value": ...} wrapping
87
+ field_type = self._unwrap_optional_and_list(field.annotation)
88
+ if (hasattr(field_type, 'model_fields') and
89
+ 'value' in getattr(field_type, 'model_fields', {})):
90
+ nested[alias] = {"value": value}
91
+ else:
92
+ nested[alias] = value
93
+
94
+ return nested
95
+
96
+ def _unwrap_optional_and_list(self, field_type):
97
+ """Helper to unwrap Optional and List types to get the core type."""
98
+ # Handle Optional[T] (Union[T, None])
99
+ origin = get_origin(field_type)
100
+ if origin is type(Optional[str]) or str(origin) == 'typing.Union':
101
+ args = get_args(field_type)
102
+ if args:
103
+ for arg in args:
104
+ if arg is not type(None):
105
+ field_type = arg
106
+ break
107
+
108
+ # Handle List[T]
109
+ if get_origin(field_type) is list:
110
+ args = get_args(field_type)
111
+ if args:
112
+ field_type = args[0]
113
+
114
+ return field_type
@@ -0,0 +1,294 @@
1
+ """
2
+ Simplified manager for the Alight SDK.
3
+ Pure dictionary transformations - no unnecessary classes or complexity.
4
+ """
5
+
6
+ from typing import Dict, Any, Optional, TYPE_CHECKING, List, get_origin, get_args
7
+ from pydantic import BaseModel
8
+
9
+ from .schemas.hrxml_indicative_data import IndicativeDataType, IndicativePerson
10
+
11
+ if TYPE_CHECKING:
12
+ from . import Alight
13
+
14
+
15
+ class EmployeeManager:
16
+ """Simple employee manager - just flat-to-nested dictionary conversion."""
17
+
18
+ def __init__(self, alight_sdk: 'Alight'):
19
+ self.alight_sdk = alight_sdk
20
+
21
+ # Simple field categorization
22
+ self.field_categories = {
23
+ 'extension': ['iban', 'bic', 'account_holder', 'notes'],
24
+ 'salary': ['base_salary', 'bonus_amount', 'salary_currency', 'pay_element_code'],
25
+ 'job': ['job_title', 'weekly_hours', 'country'],
26
+ }
27
+
28
+ def create_newhire_simple(self, flat_data: Dict[str, Any],
29
+ logical_id: Optional[str] = None,
30
+ pretty_print: bool = True,
31
+ save_to_file: bool = False,
32
+ filename: Optional[str] = None) -> str:
33
+ """
34
+ Create NewHire XML from flat dictionary - that's it!
35
+
36
+ Args:
37
+ flat_data: Simple flat dictionary with snake_case keys
38
+ logical_id: Optional logical ID
39
+ pretty_print: Format XML nicely
40
+ save_to_file: Save to file
41
+ filename: Optional filename
42
+
43
+ Returns:
44
+ str: HR-XML ready for Alight
45
+ """
46
+ nested_data = self._flat_to_nested(flat_data)
47
+ return self.alight_sdk.generate_newhire_xml(
48
+ person_data=nested_data.get('person_data'),
49
+ extension_data=nested_data.get('extension_data'),
50
+ pay_elements_data=nested_data.get('salary_data'),
51
+ logical_id=logical_id,
52
+ pretty_print=pretty_print
53
+ )
54
+
55
+ def create_employee_change_simple(self, flat_data: Dict[str, Any],
56
+ logical_id: Optional[str] = None,
57
+ pretty_print: bool = True,
58
+ save_to_file: bool = False,
59
+ filename: Optional[str] = None) -> str:
60
+ """
61
+ Create Employee Change XML from flat dictionary - that's it!
62
+ """
63
+ nested_data = self._flat_to_nested(flat_data)
64
+ return self.alight_sdk.generate_employee_change_xml(
65
+ person_data=nested_data.get('person_data'),
66
+ extension_data=nested_data.get('extension_data'),
67
+ pay_elements_data=nested_data.get('salary_data'),
68
+ logical_id=logical_id,
69
+ pretty_print=pretty_print
70
+ )
71
+
72
+ def _flat_to_nested(self, flat_data: Dict[str, Any]) -> Dict[str, Any]:
73
+ """
74
+ Convert flat data to nested using PURE XSD schema inspection.
75
+ No hardcoded field mappings - everything is generic and schema-driven.
76
+ """
77
+ result = {}
78
+
79
+ # Use the generic XSD-aware conversion directly on the main schema
80
+ nested_data = self._xsd_flat_to_nested(flat_data, IndicativeDataType)
81
+
82
+ # The result should have the indicative_person_dossier structure
83
+ if 'indicative_person_dossier' in nested_data:
84
+ # Extract the person dossier data
85
+ person_dossier = nested_data['indicative_person_dossier']
86
+
87
+ # Map the XSD structure to our expected output format
88
+ if person_dossier:
89
+ result['person_data'] = person_dossier
90
+
91
+ return result
92
+
93
+ def _xsd_flat_to_nested(self, flat_dict: Dict[str, Any], model: BaseModel) -> Dict[str, Any]:
94
+ """
95
+ Generic XSD-aware version of flat_dict_to_nested_dict.
96
+ Based on your Functions.flat_dict_to_nested_dict but handles XSD {"value": ...} format.
97
+ """
98
+ nested = {}
99
+
100
+ for name, field in model.model_fields.items():
101
+ key_in_input = name # Original model field name as key in flat_dict
102
+ alias = field.alias or name
103
+
104
+ # Handle nested BaseModel fields (recursively)
105
+ if isinstance(field.annotation, type) and issubclass(field.annotation, BaseModel):
106
+ nested[alias] = self._xsd_flat_to_nested(flat_dict, field.annotation)
107
+
108
+ # Handle Union types with BaseModel (like Optional[SomeModel])
109
+ elif any(isinstance(item, type) and issubclass(item, BaseModel) for item in get_args(field.annotation)):
110
+ # Get the BaseModel class from the Union
111
+ nested_model = next(item for item in get_args(field.annotation)
112
+ if isinstance(item, type) and issubclass(item, BaseModel))
113
+ nested[alias] = self._xsd_flat_to_nested(flat_dict, nested_model)
114
+
115
+ # Handle List types with BaseModel elements
116
+ elif get_origin(field.annotation) is list:
117
+ args = get_args(field.annotation)
118
+ if args and isinstance(args[0], type) and issubclass(args[0], BaseModel):
119
+ # List of BaseModel - create single item with nested structure
120
+ nested_item = self._xsd_flat_to_nested(flat_dict, args[0])
121
+ if nested_item: # Only add if there's actual data
122
+ nested[alias] = [nested_item]
123
+ else:
124
+ # List of primitives - just wrap the value if present
125
+ if key_in_input in flat_dict:
126
+ nested[alias] = [flat_dict[key_in_input]]
127
+
128
+ # Handle primitive fields (including XSD value wrappers)
129
+ else:
130
+ if key_in_input in flat_dict:
131
+ value = flat_dict[key_in_input]
132
+
133
+ # Check if this is an XSD type that needs {"value": ...} wrapping
134
+ field_type = self._unwrap_optional_and_list(field.annotation)
135
+ if (hasattr(field_type, 'model_fields') and
136
+ isinstance(field_type.model_fields.get('value'), type)):
137
+ nested[alias] = {"value": value}
138
+ else:
139
+ nested[alias] = value
140
+
141
+ return nested
142
+
143
+ def _unwrap_optional_and_list(self, field_type):
144
+ """Helper to unwrap Optional and List types to get the core type."""
145
+ # Handle Optional[T] (Union[T, None])
146
+ origin = get_origin(field_type)
147
+ if origin is type(Optional[str]) or str(origin) == 'typing.Union':
148
+ args = get_args(field_type)
149
+ if args:
150
+ for arg in args:
151
+ if arg is not type(None):
152
+ field_type = arg
153
+ break
154
+
155
+ # Handle List[T]
156
+ if get_origin(field_type) is list:
157
+ args = get_args(field_type)
158
+ if args:
159
+ field_type = args[0]
160
+
161
+ return field_type
162
+
163
+ def _build_person_data(self, fields: Dict[str, Any]) -> Dict[str, Any]:
164
+ """Build person data structure using XSD-based automatic wrapping."""
165
+ person_data = {}
166
+ person_section = {}
167
+ employment_section = {}
168
+
169
+ # XSD-based automatic field processing
170
+ # These fields map directly to IndicativeDataType schema fields
171
+ direct_person_fields = ['person_id', 'birth_date']
172
+ direct_employment_fields = ['employee_id']
173
+
174
+ # Process direct person fields using XSD schema inspection
175
+ for field_name in direct_person_fields:
176
+ if fields.get(field_name):
177
+ wrapped_value = self._auto_wrap_field(field_name, fields[field_name], IndicativePerson)
178
+ person_section[field_name] = wrapped_value
179
+
180
+ # Process direct employment fields
181
+ for field_name in direct_employment_fields:
182
+ if fields.get(field_name):
183
+ wrapped_value = self._auto_wrap_field(field_name, fields[field_name], IndicativeDataType)
184
+ employment_section[field_name] = wrapped_value
185
+
186
+ # Handle structured fields that don't map directly to schema
187
+ # Names (complex nested structure)
188
+ if fields.get('first_name') or fields.get('last_name'):
189
+ person_section['person_name'] = [{}]
190
+ if fields.get('first_name'):
191
+ # Use XSD inspection for given_name wrapping
192
+ person_section['person_name'][0]['given_name'] = self._auto_wrap_field('given_name', fields['first_name'], IndicativeDataType)
193
+ if fields.get('last_name'):
194
+ # Use XSD inspection for family_name wrapping
195
+ person_section['person_name'][0]['family_name'] = self._auto_wrap_field('family_name', fields['last_name'], IndicativeDataType)
196
+
197
+ # Communication (structured)
198
+ communication = []
199
+ if fields.get('email'):
200
+ communication.append({
201
+ "type": "Email",
202
+ "uri": self._auto_wrap_field('uri', fields['email'], IndicativeDataType)
203
+ })
204
+ if fields.get('phone'):
205
+ communication.append({
206
+ "type": "Phone",
207
+ "dial_number": self._auto_wrap_field('dial_number', fields['phone'], IndicativeDataType)
208
+ })
209
+ if communication:
210
+ person_section['communication'] = communication
211
+
212
+ # Employment dates
213
+ hire_date = fields.get('hire_date')
214
+ effective_date = fields.get('effective_date')
215
+ actual_date = hire_date or effective_date
216
+
217
+ if actual_date:
218
+ employment_section['employment_lifecycle'] = [{
219
+ "valid_from": actual_date,
220
+ "hire": {
221
+ "hire_date": self._auto_wrap_field('hire_date', actual_date, IndicativeDataType),
222
+ "original_hire_date": self._auto_wrap_field('original_hire_date', actual_date, IndicativeDataType)
223
+ }
224
+ }]
225
+
226
+ # Assemble
227
+ if person_section:
228
+ person_data['person'] = person_section
229
+ if employment_section:
230
+ person_data['employment'] = employment_section
231
+
232
+ return person_data
233
+
234
+ def _build_extension_data(self, fields: Dict[str, Any]) -> Dict[str, Any]:
235
+ """Build extension data (payment info, etc.)."""
236
+ extension_data = {}
237
+
238
+ # Payment instruction
239
+ if any(fields.get(k) for k in ['iban', 'bic', 'account_holder']):
240
+ extension_data['payment_instruction'] = {}
241
+ if fields.get('iban'):
242
+ extension_data['payment_instruction']['iban'] = fields['iban']
243
+ if fields.get('bic'):
244
+ extension_data['payment_instruction']['bic'] = fields['bic']
245
+ if fields.get('account_holder'):
246
+ extension_data['payment_instruction']['holder_name'] = fields['account_holder']
247
+
248
+ if fields.get('notes'):
249
+ extension_data['note'] = fields['notes']
250
+
251
+ return extension_data
252
+
253
+ def _build_salary_data(self, fields: Dict[str, Any]) -> Dict[str, Any]:
254
+ """Build salary data."""
255
+ salary_data = {}
256
+ pay_elements = []
257
+
258
+ if fields.get('base_salary'):
259
+ pay_elements.append({
260
+ "code": "BASE",
261
+ "amount": fields['base_salary'],
262
+ "valid_from": fields.get('effective_date') or fields.get('hire_date')
263
+ })
264
+
265
+ if fields.get('bonus_amount'):
266
+ pay_elements.append({
267
+ "code": "BONUS",
268
+ "amount": fields['bonus_amount'],
269
+ "valid_from": fields.get('effective_date') or fields.get('hire_date')
270
+ })
271
+
272
+ if pay_elements:
273
+ salary_data['pay_element'] = pay_elements
274
+
275
+ return salary_data
276
+
277
+ def _build_job_data(self, job_fields: Dict[str, Any], all_fields: Dict[str, Any]) -> Dict[str, Any]:
278
+ """Build job/deployment data."""
279
+ job_data = {}
280
+
281
+ actual_date = all_fields.get('hire_date') or all_fields.get('effective_date')
282
+ if actual_date:
283
+ job_data['valid_from'] = actual_date
284
+
285
+ if job_fields.get('job_title'):
286
+ job_data['job'] = {"title": job_fields['job_title']}
287
+
288
+ if job_fields.get('weekly_hours'):
289
+ job_data['schedule'] = {"weekly_hours": job_fields['weekly_hours']}
290
+
291
+ if job_fields.get('country'):
292
+ job_data['work_location'] = {"country": job_fields['country']}
293
+
294
+ return job_data
@@ -0,0 +1,229 @@
1
+ """
2
+ Simplified manager for the Alight SDK.
3
+ Pure dictionary transformations - no unnecessary classes or complexity.
4
+ """
5
+
6
+ from typing import Dict, Any, Optional, TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from . import Alight
10
+
11
+
12
+ class EmployeeManager:
13
+ """Simple employee manager - just flat-to-nested dictionary conversion."""
14
+
15
+ def __init__(self, alight_sdk: 'Alight'):
16
+ self.alight_sdk = alight_sdk
17
+
18
+ # Simple field categorization
19
+ self.field_categories = {
20
+ 'extension': ['iban', 'bic', 'account_holder', 'notes'],
21
+ 'salary': ['base_salary', 'bonus_amount', 'salary_currency', 'pay_element_code'],
22
+ 'job': ['job_title', 'weekly_hours', 'country'],
23
+ }
24
+
25
+ def create_newhire_simple(self, flat_data: Dict[str, Any],
26
+ logical_id: Optional[str] = None,
27
+ pretty_print: bool = True,
28
+ save_to_file: bool = False,
29
+ filename: Optional[str] = None) -> str:
30
+ """
31
+ Create NewHire XML from flat dictionary - that's it!
32
+
33
+ Args:
34
+ flat_data: Simple flat dictionary with snake_case keys
35
+ logical_id: Optional logical ID
36
+ pretty_print: Format XML nicely
37
+ save_to_file: Save to file
38
+ filename: Optional filename
39
+
40
+ Returns:
41
+ str: HR-XML ready for Alight
42
+ """
43
+ nested_data = self._flat_to_nested(flat_data)
44
+ return self.alight_sdk.generate_newhire_xml(
45
+ person_data=nested_data.get('person_data'),
46
+ extension_data=nested_data.get('extension_data'),
47
+ pay_elements_data=nested_data.get('salary_data'),
48
+ logical_id=logical_id,
49
+ pretty_print=pretty_print
50
+ )
51
+
52
+ def create_employee_change_simple(self, flat_data: Dict[str, Any],
53
+ logical_id: Optional[str] = None,
54
+ pretty_print: bool = True,
55
+ save_to_file: bool = False,
56
+ filename: Optional[str] = None) -> str:
57
+ """
58
+ Create Employee Change XML from flat dictionary - that's it!
59
+ """
60
+ nested_data = self._flat_to_nested(flat_data)
61
+ return self.alight_sdk.generate_employee_change_xml(
62
+ person_data=nested_data.get('person_data'),
63
+ extension_data=nested_data.get('extension_data'),
64
+ pay_elements_data=nested_data.get('salary_data'),
65
+ logical_id=logical_id,
66
+ pretty_print=pretty_print
67
+ )
68
+
69
+ def _flat_to_nested(self, flat_data: Dict[str, Any]) -> Dict[str, Any]:
70
+ """Convert flat data to nested - the ONLY transformation we need."""
71
+ result = {}
72
+
73
+ # Categorize fields
74
+ person_fields = {}
75
+ extension_fields = {}
76
+ salary_fields = {}
77
+ job_fields = {}
78
+
79
+ for key, value in flat_data.items():
80
+ if key in self.field_categories['extension']:
81
+ extension_fields[key] = value
82
+ elif key in self.field_categories['salary']:
83
+ salary_fields[key] = value
84
+ elif key in self.field_categories['job']:
85
+ job_fields[key] = value
86
+ else:
87
+ person_fields[key] = value
88
+
89
+ # Build structures (simple dictionary operations)
90
+ if person_fields:
91
+ result['person_data'] = self._build_person_data(person_fields)
92
+
93
+ if extension_fields:
94
+ result['extension_data'] = self._build_extension_data(extension_fields)
95
+
96
+ if salary_fields:
97
+ result['salary_data'] = self._build_salary_data(salary_fields)
98
+
99
+ if job_fields:
100
+ # Add job data to person_data (deployment section)
101
+ if 'person_data' not in result:
102
+ result['person_data'] = {}
103
+ result['person_data']['deployment'] = self._build_job_data(job_fields, flat_data)
104
+
105
+ return result
106
+
107
+ def _build_person_data(self, fields: Dict[str, Any]) -> Dict[str, Any]:
108
+ """Build person data structure with automatic {"value": ...} wrapping."""
109
+ person_data = {}
110
+ person_section = {}
111
+ employment_section = {}
112
+
113
+ # Person identification
114
+ if fields.get('person_id'):
115
+ person_section['person_id'] = [{"value": fields['person_id']}]
116
+
117
+ # Names
118
+ if fields.get('first_name') or fields.get('last_name'):
119
+ person_section['person_name'] = [{}]
120
+ if fields.get('first_name'):
121
+ person_section['person_name'][0]['given_name'] = [{"value": fields['first_name']}]
122
+ if fields.get('last_name'):
123
+ person_section['person_name'][0]['family_name'] = [{"value": fields['last_name']}]
124
+
125
+ # Birth date
126
+ if fields.get('birth_date'):
127
+ person_section['birth_date'] = {"value": fields['birth_date']}
128
+
129
+ # Contact info
130
+ if fields.get('email'):
131
+ person_section['communication'] = person_section.get('communication', [])
132
+ person_section['communication'].append({
133
+ "type": "Email",
134
+ "uri": {"value": fields['email']}
135
+ })
136
+
137
+ if fields.get('phone'):
138
+ person_section['communication'] = person_section.get('communication', [])
139
+ person_section['communication'].append({
140
+ "type": "Phone",
141
+ "dial_number": {"value": fields['phone']}
142
+ })
143
+
144
+ # Employment dates
145
+ hire_date = fields.get('hire_date')
146
+ effective_date = fields.get('effective_date')
147
+ actual_date = hire_date or effective_date
148
+
149
+ if actual_date:
150
+ employment_section['employment_lifecycle'] = [{
151
+ "valid_from": actual_date,
152
+ "hire": {
153
+ "hire_date": {"value": actual_date},
154
+ "original_hire_date": {"value": actual_date}
155
+ }
156
+ }]
157
+
158
+ if fields.get('employee_id'):
159
+ employment_section['employee_id'] = [{"value": fields['employee_id']}]
160
+
161
+ # Assemble
162
+ if person_section:
163
+ person_data['person'] = person_section
164
+ if employment_section:
165
+ person_data['employment'] = employment_section
166
+
167
+ return person_data
168
+
169
+ def _build_extension_data(self, fields: Dict[str, Any]) -> Dict[str, Any]:
170
+ """Build extension data (payment info, etc.)."""
171
+ extension_data = {}
172
+
173
+ # Payment instruction
174
+ if any(fields.get(k) for k in ['iban', 'bic', 'account_holder']):
175
+ extension_data['payment_instruction'] = {}
176
+ if fields.get('iban'):
177
+ extension_data['payment_instruction']['iban'] = fields['iban']
178
+ if fields.get('bic'):
179
+ extension_data['payment_instruction']['bic'] = fields['bic']
180
+ if fields.get('account_holder'):
181
+ extension_data['payment_instruction']['holder_name'] = fields['account_holder']
182
+
183
+ if fields.get('notes'):
184
+ extension_data['note'] = fields['notes']
185
+
186
+ return extension_data
187
+
188
+ def _build_salary_data(self, fields: Dict[str, Any]) -> Dict[str, Any]:
189
+ """Build salary data."""
190
+ salary_data = {}
191
+ pay_elements = []
192
+
193
+ if fields.get('base_salary'):
194
+ pay_elements.append({
195
+ "code": "BASE",
196
+ "amount": fields['base_salary'],
197
+ "valid_from": fields.get('effective_date') or fields.get('hire_date')
198
+ })
199
+
200
+ if fields.get('bonus_amount'):
201
+ pay_elements.append({
202
+ "code": "BONUS",
203
+ "amount": fields['bonus_amount'],
204
+ "valid_from": fields.get('effective_date') or fields.get('hire_date')
205
+ })
206
+
207
+ if pay_elements:
208
+ salary_data['pay_element'] = pay_elements
209
+
210
+ return salary_data
211
+
212
+ def _build_job_data(self, job_fields: Dict[str, Any], all_fields: Dict[str, Any]) -> Dict[str, Any]:
213
+ """Build job/deployment data."""
214
+ job_data = {}
215
+
216
+ actual_date = all_fields.get('hire_date') or all_fields.get('effective_date')
217
+ if actual_date:
218
+ job_data['valid_from'] = actual_date
219
+
220
+ if job_fields.get('job_title'):
221
+ job_data['job'] = {"title": job_fields['job_title']}
222
+
223
+ if job_fields.get('weekly_hours'):
224
+ job_data['schedule'] = {"weekly_hours": job_fields['weekly_hours']}
225
+
226
+ if job_fields.get('country'):
227
+ job_data['work_location'] = {"country": job_fields['country']}
228
+
229
+ return job_data