From 96a14d42366763003178cd2ebe458a2f45072c5f Mon Sep 17 00:00:00 2001 From: Ansh Dev Nagar Date: Wed, 28 Jan 2026 20:57:05 +0530 Subject: [PATCH 1/3] fix: use safe dict access for service_levels in dataunits.py to prevent KeyError when key is missing --- modules/core/karrio/server/core/dataunits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/core/karrio/server/core/dataunits.py b/modules/core/karrio/server/core/dataunits.py index d9513fcdb..0a1d0fe56 100644 --- a/modules/core/karrio/server/core/dataunits.py +++ b/modules/core/karrio/server/core/dataunits.py @@ -154,7 +154,7 @@ def _get_generic_carriers(): for s in c.services or [ lib.to_object(lib.models.ServiceLevel, _) - for _ in references["service_levels"][c.ext] + for _ in references.get("service_levels", {}).get(c.ext, []) ] } for c in custom_carriers From ecac9c02c69f7ee2d3a835a1393add5b84d8943a Mon Sep 17 00:00:00 2001 From: Ansh Dev Nagar Date: Wed, 28 Jan 2026 21:09:18 +0530 Subject: [PATCH 2/3] fix: add custom SOAP serializer with proper namespace prefixes and element names --- .../providers/postat/shipment/cancel.py | 24 ++-- .../providers/postat/shipment/create.py | 119 +++++++++--------- .../postat/karrio/providers/postat/utils.py | 20 ++- 3 files changed, 93 insertions(+), 70 deletions(-) diff --git a/modules/connectors/postat/karrio/providers/postat/shipment/cancel.py b/modules/connectors/postat/karrio/providers/postat/shipment/cancel.py index 583948afa..bfd70bd8d 100644 --- a/modules/connectors/postat/karrio/providers/postat/shipment/cancel.py +++ b/modules/connectors/postat/karrio/providers/postat/shipment/cancel.py @@ -47,19 +47,19 @@ def shipment_cancel_request( ) -> lib.Serializable: """Create a shipment cancellation request for the PostAT SOAP API.""" # Build request using generated schema types - request = lib.Envelope( - Body=lib.Body( - postat_void.VoidShipmentType( - row=[ - postat_void.VoidShipmentRowType( - TrackingNumber=payload.shipment_identifier, - OrgUnitID=settings.org_unit_id, - OrgUnitGuid=settings.org_unit_guid, - ) - ] + void_shipment = postat_void.VoidShipmentType( + row=[ + postat_void.VoidShipmentRowType( + TrackingNumber=payload.shipment_identifier, + OrgUnitID=settings.org_unit_id, + OrgUnitGuid=settings.org_unit_guid, ) - ) + ] ) + # Set proper element name for PostAT API (VoidShipment, not VoidShipmentType) + void_shipment.original_tagname_ = "VoidShipment" + + request = lib.Envelope(Body=lib.Body(void_shipment)) - return lib.Serializable(request, lib.envelope_serializer) + return lib.Serializable(request, provider_utils.standard_request_serializer) diff --git a/modules/connectors/postat/karrio/providers/postat/shipment/create.py b/modules/connectors/postat/karrio/providers/postat/shipment/create.py index 716c145be..7470f5626 100644 --- a/modules/connectors/postat/karrio/providers/postat/shipment/create.py +++ b/modules/connectors/postat/karrio/providers/postat/shipment/create.py @@ -17,9 +17,14 @@ def parse_shipment_response( response = _response.deserialize() messages = error.parse_error_response(response, settings) result = lib.find_element("ImportShipmentResult", response, first=True) + + # Check if we have valid tracking codes before extracting + code_elements = lib.find_element("Code", result) if result is not None else [] + has_tracking = any(code.text for code in (code_elements or [])) + shipment = ( _extract_details(response, settings) - if result is not None and not any(messages) + if result is not None and has_tracking and not any(messages) else None ) @@ -86,62 +91,62 @@ def shipment_request( ) # Build request using generated schema types - request = lib.Envelope( - Body=lib.Body( - postat.ImportShipmentType( - row=[ - postat.ImportShipmentRowType( - ClientID=settings.client_id, - OrgUnitID=settings.org_unit_id, - OrgUnitGuid=settings.org_unit_guid, - DeliveryServiceThirdPartyID=service, - CustomDataBit1=False, - OUShipperReference1=payload.reference, - ColloList=postat.ColloListType( - ColloRow=[ - postat.ColloRowType( - Weight=package.weight.KG, - Length=package.length.CM, - Width=package.width.CM, - Height=package.height.CM, - ) - for package in packages - ] - ), - OURecipientAddress=postat.AddressType( - Name1=recipient.company_name or recipient.person_name, - Name2=( - recipient.person_name - if recipient.company_name - else None - ), - AddressLine1=recipient.street, - AddressLine2=recipient.address_line2, - HouseNumber=recipient.street_number, - PostalCode=recipient.postal_code, - City=recipient.city, - CountryID=recipient.country_code, - Email=recipient.email, - Tel1=recipient.phone_number, - ), - OUShipperAddress=postat.AddressType( - Name1=shipper.company_name or shipper.person_name, - Name2=shipper.person_name if shipper.company_name else None, - AddressLine1=shipper.street, - AddressLine2=shipper.address_line2, - PostalCode=shipper.postal_code, - City=shipper.city, - CountryID=shipper.country_code, - ), - PrinterObject=postat.PrinterObjectType( - LabelFormatID=label_size, - LanguageID=label_format, - PaperLayoutID=paper_layout, - ), - ) - ] + import_shipment = postat.ImportShipmentType( + row=[ + postat.ImportShipmentRowType( + ClientID=settings.client_id, + OrgUnitID=settings.org_unit_id, + OrgUnitGuid=settings.org_unit_guid, + DeliveryServiceThirdPartyID=service, + CustomDataBit1=False, + OUShipperReference1=payload.reference, + ColloList=postat.ColloListType( + ColloRow=[ + postat.ColloRowType( + Weight=package.weight.KG, + Length=package.length.CM, + Width=package.width.CM, + Height=package.height.CM, + ) + for package in packages + ] + ), + OURecipientAddress=postat.AddressType( + Name1=recipient.company_name or recipient.person_name, + Name2=( + recipient.person_name + if recipient.company_name + else None + ), + AddressLine1=recipient.street, + AddressLine2=recipient.address_line2, + HouseNumber=recipient.street_number, + PostalCode=recipient.postal_code, + City=recipient.city, + CountryID=recipient.country_code, + Email=recipient.email, + Tel1=recipient.phone_number, + ), + OUShipperAddress=postat.AddressType( + Name1=shipper.company_name or shipper.person_name, + Name2=shipper.person_name if shipper.company_name else None, + AddressLine1=shipper.street, + AddressLine2=shipper.address_line2, + PostalCode=shipper.postal_code, + City=shipper.city, + CountryID=shipper.country_code, + ), + PrinterObject=postat.PrinterObjectType( + LabelFormatID=label_size, + LanguageID=label_format, + PaperLayoutID=paper_layout, + ), ) - ) + ] ) + # Set proper element name for PostAT API (ImportShipment, not ImportShipmentType) + import_shipment.original_tagname_ = "ImportShipment" + + request = lib.Envelope(Body=lib.Body(import_shipment)) - return lib.Serializable(request, lib.envelope_serializer) + return lib.Serializable(request, provider_utils.standard_request_serializer) diff --git a/modules/connectors/postat/karrio/providers/postat/utils.py b/modules/connectors/postat/karrio/providers/postat/utils.py index edb3f4a37..c855aba1d 100644 --- a/modules/connectors/postat/karrio/providers/postat/utils.py +++ b/modules/connectors/postat/karrio/providers/postat/utils.py @@ -3,6 +3,24 @@ import attr import karrio.lib as lib import karrio.core as core +import karrio.core.utils as utils +from karrio.core.utils.soap import apply_namespaceprefix + + +def standard_request_serializer(envelope: lib.Envelope) -> str: + """Serialize envelope to PostAT SOAP format with proper namespaces.""" + namespace_def = ( + 'xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" ' + 'xmlns:post="http://post.ondot.at"' + ) + + envelope.ns_prefix_ = "soapenv" + envelope.Body.ns_prefix_ = "soapenv" + + for node in envelope.Body.anytypeobjs_: + apply_namespaceprefix(node, "post") + + return utils.XP.export(envelope, namespacedef_=namespace_def) @attr.s(auto_attribs=True) @@ -35,7 +53,7 @@ def server_url(self): """ return ( self.connection_config.server_url.state - or "https://plc.post.at/Post.Webservice/ShippingService.svc" + or "https://plc.post.at/Post.Webservice/ShippingService.svc/secure" ) @property From e6af0373ee5c3122c76075f45e65adee356157a3 Mon Sep 17 00:00:00 2001 From: Ansh Dev Nagar Date: Wed, 28 Jan 2026 21:11:51 +0530 Subject: [PATCH 3/3] fix: improve error parsing, add service_levels metadata, and fix test import --- .../postat/karrio/plugins/postat/__init__.py | 1 + .../postat/karrio/providers/postat/error.py | 36 +++++++++++-------- modules/connectors/postat/tests/__init__.py | 3 +- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/modules/connectors/postat/karrio/plugins/postat/__init__.py b/modules/connectors/postat/karrio/plugins/postat/__init__.py index 388da1157..5478bcaec 100644 --- a/modules/connectors/postat/karrio/plugins/postat/__init__.py +++ b/modules/connectors/postat/karrio/plugins/postat/__init__.py @@ -21,6 +21,7 @@ is_hub=False, options=units.ShippingOption, services=units.ShippingService, + service_levels=units.DEFAULT_SERVICES, connection_configs=units.ConnectionConfig, # Extra info website="https://www.post.at", diff --git a/modules/connectors/postat/karrio/providers/postat/error.py b/modules/connectors/postat/karrio/providers/postat/error.py index 0da7b7287..6a1eabce6 100644 --- a/modules/connectors/postat/karrio/providers/postat/error.py +++ b/modules/connectors/postat/karrio/providers/postat/error.py @@ -13,18 +13,26 @@ def parse_error_response( **kwargs, ) -> typing.List[models.Message]: """Parse error response from PostAT SOAP API.""" - errors = lib.find_element("Error", response) or [] + messages: typing.List[models.Message] = [] - return [ - models.Message( - carrier_id=settings.carrier_id, - carrier_name=settings.carrier_name, - code=error.findtext("Code") or "ERROR", - message=error.findtext("Message") or "", - details={ - **({"description": error.findtext("Description")} if error.findtext("Description") else {}), - **kwargs, - }, - ) - for error in errors - ] + extract_fault(response, settings) + # Extract SOAP faults (standard SOAP error handling) + messages.extend(extract_fault(response, settings)) + + # Parse PostAT errorMessage element (main error field in responses) + error_messages = lib.find_element("errorMessage", response) or [] + for err_elem in error_messages: + if err_elem.text and err_elem.text.strip(): + msg_text = err_elem.text.strip() + # Avoid duplicates + if not any(msg_text in (m.message or "") for m in messages): + messages.append( + models.Message( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + code="POSTAT_ERROR", + message=msg_text, + details=kwargs if kwargs else None, + ) + ) + + return messages diff --git a/modules/connectors/postat/tests/__init__.py b/modules/connectors/postat/tests/__init__.py index a948a4f54..4cbfe59d6 100644 --- a/modules/connectors/postat/tests/__init__.py +++ b/modules/connectors/postat/tests/__init__.py @@ -1,2 +1 @@ - -from postat.test_shipment import * \ No newline at end of file +from tests.postat.test_shipment import * \ No newline at end of file