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,72 @@
1
+ from typing import Any, Dict, Optional
2
+
3
+ from .schemas import Address as AddressModel
4
+
5
+
6
+ class Address:
7
+ """
8
+ High-level Address API. Address changes are updates to IndicativeData.
9
+
10
+ Usage:
11
+ alight = Alight()
12
+ xml = alight.address.update(
13
+ data={...},
14
+ person_id="35561",
15
+ employee_id="35561ZZGB",
16
+ )
17
+ """
18
+
19
+ def __init__(self, client: Any):
20
+ self._client = client
21
+
22
+ def update(
23
+ self,
24
+ data: Dict[str, Any],
25
+ *,
26
+ person_id: str,
27
+ employee_id: str,
28
+ logical_id: Optional[str] = None,
29
+ envelope_options: Optional[Dict[str, Any]] = None,
30
+ employee_data: Optional[Dict[str, Any]] = None,
31
+ pretty_print: bool = True,
32
+ ) -> str:
33
+ """
34
+ Update address (actionCode=CHANGE). Returns XML string.
35
+ Requires both person_id and employee_id per integration constraints.
36
+
37
+ Example:
38
+ >>> alight.address.update(
39
+ ... data={
40
+ ... "address_line_1": "10 Downing Street",
41
+ ... "town": "London",
42
+ ... "postal_code": "SW1A 2AA",
43
+ ... "address_valid_from": "2024-01-01",
44
+ ... },
45
+ ... person_id="35561",
46
+ ... employee_id="35561ZZGB",
47
+ ... )
48
+ """
49
+ payload: dict[str, Any] = {
50
+ "person_id": person_id,
51
+ "employee_id": employee_id,
52
+ }
53
+
54
+ # build from flat fields
55
+ address_model = AddressModel(**data)
56
+ payload["address"] = address_model.model_dump(exclude_none=True, by_alias=True)
57
+
58
+ if "address_valid_from" in data:
59
+ payload["address_valid_from"] = data["address_valid_from"]
60
+ if "address_valid_to" in data:
61
+ payload["address_valid_to"] = data["address_valid_to"]
62
+
63
+ if employee_data:
64
+ payload.update(employee_data)
65
+
66
+ return self._client.generate_employee_xml(
67
+ employee=payload,
68
+ action_code="CHANGE",
69
+ logical_id=logical_id,
70
+ envelope_options=envelope_options,
71
+ pretty_print=pretty_print,
72
+ )
@@ -0,0 +1,139 @@
1
+ """
2
+ Flat wrapper for XSD schemas.
3
+ Provides a simple interface that automatically maps flat dictionaries to complex XSD schemas.
4
+ """
5
+
6
+ from typing import Any, Dict, Optional, Type, get_origin, get_args
7
+ from pydantic import BaseModel
8
+ from xsdata.models.datatype import XmlDate
9
+ from xsdata_pydantic.bindings import XmlSerializer
10
+
11
+
12
+ class SchemaWrapper:
13
+ """
14
+ Generic wrapper for any XSD schema.
15
+ Converts flat dictionaries to proper XSD format without field duplication.
16
+
17
+ Usage:
18
+ employee_data = {
19
+ "person_id": "12345",
20
+ "given_name": "John",
21
+ "family_name": "Smith",
22
+ "birth_date": "1990-01-01",
23
+ # ... any other fields matching the schema
24
+ }
25
+
26
+ # Create wrapper with the flat data and schema class
27
+ wrapper = SchemaWrapper(employee_data, IndicativeDataType)
28
+
29
+ # Get XML directly
30
+ xml = wrapper.to_xml()
31
+ """
32
+
33
+ def __init__(self, data: Dict[str, Any], schema_class: Type[BaseModel]):
34
+ """
35
+ Initialize with flat data and target schema class.
36
+
37
+ Args:
38
+ data: Flat dictionary with field names matching schema fields
39
+ schema_class: XSD-generated schema class to use (e.g. IndicativeDataType)
40
+ """
41
+ self.data = data
42
+ self.schema_class = schema_class
43
+
44
+ def _xsd_flat_to_nested(self, flat_dict: Dict[str, Any], model: Type[BaseModel]) -> Dict[str, Any]:
45
+ """
46
+ Convert flat dictionary to nested structure using schema introspection.
47
+ Uses the model's field types to determine how to structure the data.
48
+ """
49
+ nested = {}
50
+
51
+ for name, field in model.model_fields.items():
52
+ key_in_input = name # Original model field name as key in flat_dict
53
+ alias = field.alias or name
54
+
55
+ # Handle nested BaseModel fields (recursively)
56
+ if isinstance(field.annotation, type) and issubclass(field.annotation, BaseModel):
57
+ nested[alias] = self._xsd_flat_to_nested(flat_dict, field.annotation)
58
+
59
+ # Handle Union types with BaseModel (like Optional[SomeModel])
60
+ elif any(isinstance(item, type) and issubclass(item, BaseModel)
61
+ for item in get_args(field.annotation) if isinstance(item, type)):
62
+ # Get the BaseModel class from the Union
63
+ nested_model = next((item for item in get_args(field.annotation)
64
+ if isinstance(item, type) and issubclass(item, BaseModel)), None)
65
+ if nested_model:
66
+ nested[alias] = self._xsd_flat_to_nested(flat_dict, nested_model)
67
+
68
+ # Handle primitive fields - PURE schema introspection, NO hardcoding!
69
+ else:
70
+ if key_in_input in flat_dict:
71
+ value = flat_dict[key_in_input]
72
+
73
+ # XSD enhancement: Check if this field type needs {"value": ...} wrapping
74
+ field_type = self._unwrap_optional_and_list(field.annotation)
75
+ if (hasattr(field_type, 'model_fields') and
76
+ 'value' in getattr(field_type, 'model_fields', {})):
77
+ # Automatic XmlDate conversion for date fields (minimal logic)
78
+ if 'date' in key_in_input.lower() and isinstance(value, str):
79
+ nested[alias] = {"value": XmlDate.from_string(value)}
80
+ else:
81
+ nested[alias] = {"value": value}
82
+ else:
83
+ nested[alias] = value
84
+
85
+ return nested
86
+
87
+ def _unwrap_optional_and_list(self, field_type):
88
+ """Helper to unwrap Optional and List types to get the core type."""
89
+ # Handle Optional[T] (Union[T, None])
90
+ origin = get_origin(field_type)
91
+ if origin is type(None) or str(origin) == 'typing.Union':
92
+ args = get_args(field_type)
93
+ if args:
94
+ for arg in args:
95
+ if arg is not type(None):
96
+ field_type = arg
97
+ break
98
+
99
+ # Handle List[T]
100
+ if get_origin(field_type) is list:
101
+ args = get_args(field_type)
102
+ if args:
103
+ field_type = args[0]
104
+
105
+ return field_type
106
+
107
+ def to_nested_dict(self) -> Dict[str, Any]:
108
+ """Convert flat data to nested dictionary structure using schema introspection."""
109
+ return self._xsd_flat_to_nested(self.data, self.schema_class)
110
+
111
+ def to_model(self) -> BaseModel:
112
+ """Convert flat data to proper schema model instance using introspection."""
113
+ nested_data = self._xsd_flat_to_nested(self.data, self.schema_class)
114
+ return self.schema_class.model_validate(nested_data)
115
+
116
+ def to_xml(self, pretty_print: bool = True) -> str:
117
+ """
118
+ Generate XML output directly from flat data.
119
+
120
+ Args:
121
+ pretty_print: Whether to format the XML with proper indentation
122
+
123
+ Returns:
124
+ XML string representation of the data
125
+ """
126
+ from xml.dom import minidom
127
+
128
+ model = self.to_model()
129
+ serializer = XmlSerializer()
130
+ xml_output = serializer.render(model)
131
+
132
+ # Optional pretty printing
133
+ if pretty_print:
134
+ dom = minidom.parseString(xml_output)
135
+ xml_output = dom.toprettyxml(indent=" ")
136
+ xml_output = '\n'.join([line for line in xml_output.split('\n') if line.strip()])
137
+
138
+ return xml_output
139
+
@@ -0,0 +1,280 @@
1
+ """
2
+ HR-XML Generator for Alight Integration
3
+
4
+ This module provides functions to generate HR-XML documents using XSData models
5
+ with proper Alight envelope structure and namespace handling.
6
+ """
7
+
8
+ import uuid
9
+ from datetime import datetime
10
+ from typing import Optional, Dict, Any
11
+ from xml.dom import minidom
12
+
13
+ from xsdata.models.datatype import XmlDate, XmlDateTime
14
+ from xsdata_pydantic.bindings import XmlSerializer as PydanticXmlSerializer
15
+
16
+ from .schemas.hrxml_indicative_data import IndicativeDataType, IndicativeData
17
+ from .schemas.flattened_main_schemas.process_pay_serv_emp import (
18
+ ProcessPayServEmp, DataArea, PayServEmp, PayServEmpExtension, PayServEmpPayElements
19
+ )
20
+ from .schemas.openapplications_bod import ApplicationArea, Sender
21
+
22
+
23
+ def create_hrxml_from_data(person_data: Dict[str, Any]) -> IndicativeDataType:
24
+ """
25
+ Create HR-XML IndicativeDataType from person data dictionary.
26
+
27
+ Args:
28
+ person_data: Dictionary containing person, employee, employment, deployment, and remuneration data
29
+
30
+ Returns:
31
+ IndicativeDataType: Validated XSData model instance
32
+ """
33
+ try:
34
+ indicative_data = IndicativeDataType.model_validate(person_data)
35
+ return indicative_data
36
+ except Exception as e:
37
+ raise ValueError(f"Failed to create HR-XML from data: {e}")
38
+
39
+
40
+ def create_extension_from_data(extension_data: Dict[str, Any]) -> PayServEmpExtension:
41
+ """
42
+ Create PayServEmpExtension from extension data dictionary.
43
+
44
+ Args:
45
+ extension_data: Dictionary containing payment instructions, cost assignments, etc.
46
+
47
+ Returns:
48
+ PayServEmpExtension: Validated XSData model instance
49
+ """
50
+ try:
51
+ extension = PayServEmpExtension.model_validate(extension_data)
52
+ return extension
53
+ except Exception as e:
54
+ raise ValueError(f"Failed to create extension from data: {e}")
55
+
56
+
57
+ def create_pay_elements_from_data(pay_elements_data: Dict[str, Any]) -> PayServEmpPayElements:
58
+ """
59
+ Create PayServEmpPayElements from pay elements data dictionary.
60
+
61
+ Args:
62
+ pay_elements_data: Dictionary containing pay elements
63
+
64
+ Returns:
65
+ PayServEmpPayElements: Validated XSData model instance
66
+ """
67
+ try:
68
+ pay_elements = PayServEmpPayElements.model_validate(pay_elements_data)
69
+ return pay_elements
70
+ except Exception as e:
71
+ raise ValueError(f"Failed to create pay elements from data: {e}")
72
+
73
+
74
+ def create_complete_alight_envelope(
75
+ indicative_data: IndicativeDataType,
76
+ extension: Optional[PayServEmpExtension] = None,
77
+ pay_elements: Optional[PayServEmpPayElements] = None,
78
+ action_code: str = "ADD",
79
+ logical_id: str = "TST-GB003-1001",
80
+ language_code: str = "en-US",
81
+ system_environment_code: str = "TST NF",
82
+ release_id: str = "DEFAULT"
83
+ ) -> ProcessPayServEmp:
84
+ """
85
+ Create the complete Alight XML envelope using XSData models.
86
+
87
+ Args:
88
+ indicative_data: The HR-XML indicative data
89
+ extension: Optional Alight-specific extensions
90
+ pay_elements: Optional pay elements
91
+ action_code: Action code for the process (default: "ADD")
92
+ logical_id: Logical ID for the sender (default: "TST-GB003-1001")
93
+ language_code: Language code (default: "en-US")
94
+ system_environment_code: System environment (default: "TST NF")
95
+ release_id: Release ID (default: "DEFAULT")
96
+
97
+ Returns:
98
+ ProcessPayServEmp: Complete envelope ready for serialization
99
+ """
100
+ # Create timestamp and BODID
101
+ timestamp = datetime.now()
102
+ bod_id = str(uuid.uuid4()).upper()
103
+
104
+ try:
105
+ # Create Sender
106
+ sender_data = {
107
+ "logical_id": {"value": logical_id},
108
+ "component_id": {"value": "PAYROLL"},
109
+ "reference_id": {"value": "hrisxml"},
110
+ "confirmation_code": {"value": "Always"}
111
+ }
112
+ sender = Sender.model_validate(sender_data)
113
+
114
+ # Create ApplicationArea
115
+ app_area_data = {
116
+ "sender": sender,
117
+ "creation_date_time": {"value": XmlDateTime.from_datetime(timestamp)},
118
+ "bodid": {"value": bod_id}
119
+ }
120
+ application_area = ApplicationArea.model_validate(app_area_data)
121
+
122
+ # Create PayServEmp (contains our IndicativeData + Extensions)
123
+ # Need to create IndicativeData wrapper (not IndicativeDataType)
124
+ indicative_data_wrapper = IndicativeData(
125
+ indicative_person_dossier=indicative_data.indicative_person_dossier
126
+ )
127
+
128
+ pay_serv_emp_data = {
129
+ "indicative_data": indicative_data_wrapper
130
+ }
131
+
132
+ # Add optional components if provided
133
+ if extension:
134
+ pay_serv_emp_data["pay_serv_emp_extension"] = extension
135
+ if pay_elements:
136
+ pay_serv_emp_data["pay_serv_emp_pay_elements"] = pay_elements
137
+
138
+ pay_serv_emp = PayServEmp.model_validate(pay_serv_emp_data)
139
+
140
+ # Create Process with ActionExpression
141
+ process_data = {
142
+ "action_criteria": [{
143
+ "action_expression": [{
144
+ "action_code": action_code
145
+ }]
146
+ }]
147
+ }
148
+
149
+ # Create DataArea
150
+ data_area_data = {
151
+ "process": process_data,
152
+ "pay_serv_emp": [pay_serv_emp]
153
+ }
154
+ data_area = DataArea.model_validate(data_area_data)
155
+
156
+ # Create the complete ProcessPayServEmp envelope
157
+ envelope_data = {
158
+ "application_area": application_area,
159
+ "data_area": data_area,
160
+ "language_code": language_code,
161
+ "system_environment_code": system_environment_code,
162
+ "release_id": release_id
163
+ }
164
+
165
+ complete_envelope = ProcessPayServEmp.model_validate(envelope_data)
166
+ return complete_envelope
167
+
168
+ except Exception as e:
169
+ raise ValueError(f"Failed to create complete Alight envelope: {e}")
170
+
171
+
172
+ def serialize_with_namespaces(
173
+ envelope: ProcessPayServEmp,
174
+ pretty_print: bool = True,
175
+ custom_namespaces: Optional[Dict[str, str]] = None
176
+ ) -> str:
177
+ """
178
+ Serialize ProcessPayServEmp envelope to XML with proper namespace handling.
179
+
180
+ Args:
181
+ envelope: The ProcessPayServEmp envelope to serialize
182
+ pretty_print: Whether to format the XML with indentation (default: True)
183
+ custom_namespaces: Custom namespace mappings (prefix -> namespace URI)
184
+
185
+ Returns:
186
+ str: Serialized XML string
187
+ """
188
+ # Default namespace mappings for Alight standard prefixes
189
+ namespace_map = custom_namespaces or {
190
+ "nga": "http://www.ngahr.com/ngapexxml/1",
191
+ "oa": "http://www.openapplications.org/oagis/9",
192
+ "hr": "http://www.hr-xml.org/3"
193
+ }
194
+
195
+ try:
196
+ # Use ns_map parameter for clean namespace mapping
197
+ pydantic_serializer = PydanticXmlSerializer()
198
+ raw_xml = pydantic_serializer.render(envelope, ns_map=namespace_map)
199
+
200
+ if pretty_print:
201
+ # Pretty print the XML
202
+ dom = minidom.parseString(raw_xml)
203
+ pretty_xml = dom.toprettyxml(indent=" ", encoding=None)
204
+
205
+ # Clean up extra blank lines
206
+ lines = [line for line in pretty_xml.split('\n') if line.strip()]
207
+ return '\n'.join(lines)
208
+ else:
209
+ return raw_xml
210
+
211
+ except Exception as e:
212
+ # Fallback without ns_map if it fails
213
+ try:
214
+ pydantic_serializer = PydanticXmlSerializer()
215
+ raw_xml = pydantic_serializer.render(envelope)
216
+
217
+ if pretty_print:
218
+ dom = minidom.parseString(raw_xml)
219
+ pretty_xml = dom.toprettyxml(indent=" ", encoding=None)
220
+ lines = [line for line in pretty_xml.split('\n') if line.strip()]
221
+ return '\n'.join(lines)
222
+ else:
223
+ return raw_xml
224
+
225
+ except Exception as e2:
226
+ raise ValueError(f"Failed to serialize envelope: {e2}")
227
+
228
+
229
+ def generate_complete_hrxml(
230
+ person_data: Dict[str, Any],
231
+ extension_data: Optional[Dict[str, Any]] = None,
232
+ pay_elements_data: Optional[Dict[str, Any]] = None,
233
+ action_code: str = "ADD",
234
+ logical_id: str = "TST-GB003-1001",
235
+ pretty_print: bool = True
236
+ ) -> str:
237
+ """
238
+ High-level function to generate complete Alight HR-XML from data dictionaries.
239
+
240
+ Args:
241
+ person_data: Dictionary containing person/employee/employment data
242
+ extension_data: Optional dictionary containing Alight extensions
243
+ pay_elements_data: Optional dictionary containing pay elements
244
+ action_code: Action code for the process (default: "ADD")
245
+ logical_id: Logical ID for the sender (default: "TST-GB003-1001")
246
+ pretty_print: Whether to format the XML with indentation (default: True)
247
+
248
+ Returns:
249
+ str: Complete Alight HR-XML string
250
+ """
251
+ try:
252
+ # Create HR-XML content
253
+ indicative_data = create_hrxml_from_data(person_data)
254
+
255
+ # Create optional components
256
+ extension = None
257
+ pay_elements = None
258
+
259
+ if extension_data:
260
+ extension = create_extension_from_data(extension_data)
261
+
262
+ if pay_elements_data:
263
+ pay_elements = create_pay_elements_from_data(pay_elements_data)
264
+
265
+ # Create complete envelope
266
+ envelope = create_complete_alight_envelope(
267
+ indicative_data=indicative_data,
268
+ extension=extension,
269
+ pay_elements=pay_elements,
270
+ action_code=action_code,
271
+ logical_id=logical_id
272
+ )
273
+
274
+ # Serialize to XML
275
+ xml_string = serialize_with_namespaces(envelope, pretty_print=pretty_print)
276
+
277
+ return xml_string
278
+
279
+ except Exception as e:
280
+ raise ValueError(f"Failed to generate complete HR-XML: {e}")
@@ -0,0 +1,132 @@
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
+ # Check if data is already in schema-compliant format (has proper nesting)
57
+ if self._is_already_schema_compliant(flat_data):
58
+ if self.alight_sdk.debug:
59
+ print("✅ Data is already schema-compliant, using directly")
60
+ return flat_data
61
+
62
+ # Use the generic XSD-aware conversion for truly flat data
63
+ return self._xsd_flat_to_nested(flat_data, IndicativeDataType)
64
+
65
+ def _is_already_schema_compliant(self, data: Dict[str, Any]) -> bool:
66
+ """Check if data is already in schema-compliant format."""
67
+ # Look for key indicators that it's already structured correctly
68
+ if "indicative_person_dossier" in data:
69
+ person_dossier = data["indicative_person_dossier"]
70
+ # Check if main sections are lists (as required by schema)
71
+ if (isinstance(person_dossier.get("indicative_person"), list) and
72
+ isinstance(person_dossier.get("indicative_employee"), list) and
73
+ isinstance(person_dossier.get("indicative_employment"), list)):
74
+ return True
75
+ return False
76
+
77
+ def _xsd_flat_to_nested(self, flat_dict: Dict[str, Any], model: BaseModel) -> Dict[str, Any]:
78
+ """
79
+ Generic XSD-aware version exactly like your Functions.flat_dict_to_nested_dict.
80
+ Recursively processes the schema and handles XSD {"value": ...} format automatically.
81
+ """
82
+ nested = {}
83
+
84
+ for name, field in model.model_fields.items():
85
+ key_in_input = name # Original model field name as key in flat_dict
86
+ alias = field.alias or name
87
+
88
+ # Handle nested BaseModel fields (recursively) - exact same logic as your function
89
+ if isinstance(field.annotation, type) and issubclass(field.annotation, BaseModel):
90
+ nested[alias] = self._xsd_flat_to_nested(flat_dict, field.annotation)
91
+
92
+ # Handle Union types with BaseModel (like Optional[SomeModel]) - exact same logic
93
+ elif any(isinstance(item, type) and issubclass(item, BaseModel) for item in get_args(field.annotation)):
94
+ # Get the BaseModel class from the Union
95
+ nested_model = next(item for item in get_args(field.annotation)
96
+ if isinstance(item, type) and issubclass(item, BaseModel))
97
+ nested[alias] = self._xsd_flat_to_nested(flat_dict, nested_model)
98
+
99
+ # Handle primitive fields - your logic + XSD value wrapping
100
+ else:
101
+ if key_in_input in flat_dict:
102
+ value = flat_dict[key_in_input]
103
+
104
+ # XSD enhancement: Check if this field type needs {"value": ...} wrapping
105
+ field_type = self._unwrap_optional_and_list(field.annotation)
106
+ if (hasattr(field_type, 'model_fields') and
107
+ 'value' in getattr(field_type, 'model_fields', {})):
108
+ nested[alias] = {"value": value}
109
+ else:
110
+ nested[alias] = value
111
+
112
+ return nested
113
+
114
+ def _unwrap_optional_and_list(self, field_type):
115
+ """Helper to unwrap Optional and List types to get the core type."""
116
+ # Handle Optional[T] (Union[T, None])
117
+ origin = get_origin(field_type)
118
+ if origin is type(Optional[str]) or str(origin) == 'typing.Union':
119
+ args = get_args(field_type)
120
+ if args:
121
+ for arg in args:
122
+ if arg is not type(None):
123
+ field_type = arg
124
+ break
125
+
126
+ # Handle List[T]
127
+ if get_origin(field_type) is list:
128
+ args = get_args(field_type)
129
+ if args:
130
+ field_type = args[0]
131
+
132
+ return field_type