diff --git a/modules/connectors/fedex/karrio/mappers/fedex/proxy.py b/modules/connectors/fedex/karrio/mappers/fedex/proxy.py index 3176806f32..4d43fb0020 100644 --- a/modules/connectors/fedex/karrio/mappers/fedex/proxy.py +++ b/modules/connectors/fedex/karrio/mappers/fedex/proxy.py @@ -1,9 +1,13 @@ +import typing import urllib.parse import karrio.lib as lib import karrio.api.proxy as proxy import karrio.providers.fedex.utils as provider_utils import karrio.mappers.fedex.settings as provider_settings +import karrio.schemas.fedex.tracking_document_request as fedex +import logging +logger = logging.getLogger(__name__) class Proxy(proxy.Proxy): settings: provider_settings.Settings @@ -142,3 +146,62 @@ def cancel_pickup(self, request: lib.Serializable) -> lib.Deserializable[str]: ) return lib.Deserializable(response, lib.to_dict, request.ctx) + + def get_proof_of_delivery(self, tracking_number: str) -> typing.Optional[str]: + import karrio.providers.fedex.error as error + + # Construct the request + request = fedex.TrackingDocumentRequestType( + trackDocumentSpecification=[ + fedex.TrackDocumentSpecificationType( + trackingNumberInfo=fedex.TrackingNumberInfoType( + trackingNumber=tracking_number + ) + ) + ], + trackDocumentDetail=fedex.TrackDocumentDetailType( + documentType="SIGNATURE_PROOF_OF_DELIVERY", + documentFormat="PNG", + ), + ) + + try: + # Send the request + response = lib.to_dict( + lib.request( + url=f"{self.settings.server_url}/track/v1/trackingdocuments", + data=lib.to_json(request), + method="POST", + headers={ + "x-locale": "en_US", + "content-type": "application/json", + "authorization": f"Bearer {self.settings.track_access_token}", + }, + decoder=provider_utils.parse_response, + on_error=self.log_request_error, # Direct function reference + ) + ) + messages = error.parse_error_response(response, self.settings) + if any(messages): + logger.error(f"FedEx SPOD Error for tracking number {tracking_number}: {messages}") + return None + + # Check if the documents are present in the response + documents = response.get("output", {}).get("documents") + if not documents: + logger.error(f"No POD documents found in the response for tracking number {tracking_number}") + return None + + # Convert documents to base64 + return lib.failsafe(lambda: lib.bundle_base64(documents, format="PNG")) + + except Exception as e: + # Catch any other exceptions and log the details + logger.error(f"An error occurred while fetching POD for tracking number {tracking_number}: {e}") + return None + + def log_request_error(self, response_body): + # Custom handler for logging errors during the request + error_content = response_body.read().decode() + logger.error(f"FedEx API Request Error: {error_content}") + return provider_utils.parse_response(error_content) diff --git a/modules/connectors/fedex/karrio/providers/fedex/tracking.py b/modules/connectors/fedex/karrio/providers/fedex/tracking.py index 10fe5afebb..7da986fb82 100644 --- a/modules/connectors/fedex/karrio/providers/fedex/tracking.py +++ b/modules/connectors/fedex/karrio/providers/fedex/tracking.py @@ -6,6 +6,8 @@ import karrio.providers.fedex.error as provider_error import karrio.providers.fedex.utils as provider_utils import karrio.providers.fedex.units as provider_units +import karrio.mappers.fedex.proxy as proxy + DATETIME_FORMATS = ["%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%dT%H:%M:%S"] @@ -68,13 +70,21 @@ def _extract_details( provider_units.TrackingStatus.in_transit.name, ) delivered = status == "delivered" + # Create a proxy instance with settings + proxy_instance = proxy.Proxy(settings=settings) + img = lib.failsafe( lambda: ( - provider_utils.get_proof_of_delivery(package.trackingNumber, settings) + proxy_instance.get_proof_of_delivery(package.trackingNumber) if delivered else None ) ) + # if img: + # logger.info(f"Successfully retrieved SPOD image for {package.trackingNumber}") + # else: + # logger.warning(f"No SPOD image available for {package.trackingNumber}") + return models.TrackingDetails( carrier_name=settings.carrier_name, @@ -122,7 +132,13 @@ def _extract_details( shipment_origin_country=lib.failsafe( lambda: detail.originLocation.locationContactAndAddress.address.countryCode ), - signed_by=lib.failsafe(lambda: detail.deliveryDetails.signedByName), + signed_by=lib.failsafe( + lambda: ( + detail.deliveryDetails.signedByName + if detail.deliveryDetails.signedByName + else (detail.deliveryDetails.receivedByName) + ) + ), ), images=lib.identity(models.Images(signature_image=img) if img else None), estimated_delivery=estimated_delivery, diff --git a/modules/connectors/fedex/karrio/providers/fedex/utils.py b/modules/connectors/fedex/karrio/providers/fedex/utils.py index b14f9ae85c..9034f22891 100644 --- a/modules/connectors/fedex/karrio/providers/fedex/utils.py +++ b/modules/connectors/fedex/karrio/providers/fedex/utils.py @@ -1,11 +1,12 @@ -import karrio.schemas.fedex.tracking_document_request as fedex import gzip import datetime import urllib.parse import karrio.lib as lib import karrio.core as core import karrio.core.errors as errors +import logging +logger = logging.getLogger(__name__) class Settings(core.Settings): """FedEx connection settings.""" @@ -142,42 +143,6 @@ def login(settings: Settings, client_id: str = None, client_secret: str = None): return {**response, "expiry": lib.fdatetime(expiry)} -def get_proof_of_delivery(tracking_number: str, settings: Settings): - import karrio.providers.fedex.error as error - - request = fedex.TrackingDocumentRequestType( - trackDocumentSpecification=[ - fedex.TrackDocumentSpecificationType( - trackingNumberInfo=fedex.TrackingNumberInfoType( - trackingNumber=tracking_number - ) - ) - ], - trackDocumentDetail=fedex.TrackDocumentDetailType( - documentType="SIGNATURE_PROOF_OF_DELIVERY", - documentFormat="PNG", - ), - ) - response = lib.to_dict( - lib.request( - url=f"{settings.server_url}/track/v1/trackingdocuments", - data=lib.to_json(request), - method="POST", - decoder=parse_response, - on_error=lambda b: parse_response(b.read()), - ) - ) - - messages = error.parse_error_response(response, settings) - - if any(messages): - return None - - return lib.failsafe( - lambda: lib.bundle_base64(response["output"]["documents"], format="PNG") - ) - - def parse_response(binary_string): content = lib.failsafe(lambda: gzip.decompress(binary_string)) or binary_string return lib.decode(content) diff --git a/modules/connectors/fedex/tests/fedex/test_tracking.py b/modules/connectors/fedex/tests/fedex/test_tracking.py index 7a37212e36..65d377a775 100644 --- a/modules/connectors/fedex/tests/fedex/test_tracking.py +++ b/modules/connectors/fedex/tests/fedex/test_tracking.py @@ -107,7 +107,7 @@ def test_parse_inconsistent_datetime_response(self): "shipment_origin_postal_code": "98101", "shipment_origin_country": "US", "shipment_service": "FedEx Freight Economy.", - "signed_by": "Reciever", + "signed_by": "Reciever", # How do I do a test for the 'fall over' when we set signed_by to recievedBy instead of signedBy? }, "status": "in_transit", "tracking_number": "123456789012", diff --git a/packages/core/components/TrackingEvents.tsx b/packages/core/components/TrackingEvents.tsx new file mode 100644 index 0000000000..524be8ccad --- /dev/null +++ b/packages/core/components/TrackingEvents.tsx @@ -0,0 +1,58 @@ +// components/TrackingEvents.tsx +import { TrackingEvent, TrackingStatus } from "@karrio/types/rest/api"; +import { formatDayDate } from "@karrio/lib"; +import { TrackerType } from "@karrio/types"; + +type DayEvents = { [k: string]: TrackingEvent[] }; + +const computeEvents = (tracker: TrackingStatus): DayEvents => { + const days: DayEvents = {}; + + (tracker?.events || []).forEach((event: TrackingEvent) => { + const daydate = formatDayDate(event.date as string); + + if (!days[daydate]) { + days[daydate] = []; + } + + days[daydate].push(event); + }); + + return days; +}; + + +const TrackingEvents: React.FC<{ tracker: TrackerType }> = ({ tracker }) => { + const eventsByDay = computeEvents(tracker as TrackingStatus); + + return ( +
+ Tracking ID {tracker.tracking_number} +
+ + {!isNone(tracker?.estimated_delivery) && ( ++ {tracker?.delivered ? "Delivered" : "Estimated Delivery"}{" "} + + {formatDayDate(tracker!.estimated_delivery as string)} + +
+ )} + + {!isNone(tracker?.info) && ( + <> ++ {computeStatus(tracker as TrackerType)} +
+ > + ); +}; + +export default TrackingHeader; diff --git a/packages/core/components/TrackingMessages.tsx b/packages/core/components/TrackingMessages.tsx new file mode 100644 index 0000000000..a6cc491bfe --- /dev/null +++ b/packages/core/components/TrackingMessages.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { TrackerType } from "@karrio/types"; + +interface TrackingMessagesProps { + messages: (TrackerType["messages"] extends (infer U)[] ? U : never)[]; // Allow flexibility for undefined/null +} + +const TrackingMessages: React.FC+ {msg?.message ?? "No message available"}{" "} + {/* Handle undefined/null message */} +
+ ))} +- Tracking ID{" "} - {tracker?.tracking_number} -
- - {!isNone(tracker?.estimated_delivery) && ( -- - {tracker?.delivered ? "Delivered" : "Estimated Delivery"} - {" "} - - {formatDayDate(tracker!.estimated_delivery as string)} - -
- )} - -- {computeStatus(tracker as TrackerType)} -
+- {(tracker?.messages || [{}])[0].message} -
-@@ -281,7 +284,7 @@ export default function TrackersPage(pageProps: any) { onClick={(e) => { e.stopPropagation(); confirmDeletion({ - label: "Delet Shipment Tracker", + label: "Delete Shipment Tracker", identifier: tracker.id as string, onConfirm: remove(tracker.id), }); @@ -351,8 +354,22 @@ export default function TrackersPage(pageProps: any) { ); } -function formatEventDescription(last_event?: TrackingEvent): string { - return last_event?.description || ""; +function formatEventDescription( + last_event?: TrackingEvent, + tracking_info?: TrackingInfo, +): string { + if (!last_event) { + return ""; + } + + const { description } = last_event; + const { signed_by } = tracking_info || {}; + + if (description === "Delivered" && signed_by) { + return `${description} (Signed by: ${signed_by})`; + } + + return description || ""; } function formatEventDate(last_event?: TrackingEvent): string { diff --git a/packages/core/modules/Trackers/tracking-page.tsx b/packages/core/modules/Trackers/tracking-page.tsx index d607b6ccbd..13adcc88fb 100644 --- a/packages/core/modules/Trackers/tracking-page.tsx +++ b/packages/core/modules/Trackers/tracking-page.tsx @@ -1,16 +1,15 @@ -import { TrackingEvent, TrackingStatus } from "@karrio/types/rest/api"; -import { formatDayDate, isNone, KARRIO_API, url$ } from "@karrio/lib"; +import { isNone, KARRIO_API, url$ } from "@karrio/lib"; import { dynamicMetadata } from "@karrio/core/components/metadata"; -import { CarrierImage } from "@karrio/ui/components/carrier-image"; -import { Collection, KarrioClient } from "@karrio/types"; +import { Collection, KarrioClient, TrackerType } from "@karrio/types"; import { loadMetadata } from "@karrio/core/context/main"; import Link from "next/link"; import React from "react"; +import TrackingHeader from "@karrio/core/components/TrackingHeader"; +import TrackingEvents from "@karrio/core/components/TrackingEvents"; +import TrackingMessages from "@karrio/core/components/TrackingMessages"; export const generateMetadata = dynamicMetadata("Tracking"); -type DayEvents = { [k: string]: TrackingEvent[] }; - export default async function Page({ params }: { params: Collection }) { const id = params?.id as string; const { metadata } = await loadMetadata(); @@ -27,13 +26,15 @@ export default async function Page({ params }: { params: Collection }) { message: `No Tracker ID nor Tracking Number found for ${id}`, }; }); - - const computeEvents = (tracker: TrackingStatus): DayEvents => { - return (tracker?.events || []).reduce((days, event: TrackingEvent) => { - const daydate = formatDayDate(event.date as string); - return { ...days, [daydate]: [...(days[daydate] || []), event] }; - }, {} as DayEvents); - }; + // Normalize messages to replace undefined with null for TypeScript compatibility + const normalizedMessages = (tracker?.messages || []).map((message) => ({ + ...message, + carrier_name: message.carrier_name ?? null, // Convert undefined to null + carrier_id: message.carrier_id ?? null, // Convert undefined to null + message: message.message ?? null, + code: message.code ?? null, + details: message.details ?? null, + })); return ( <> @@ -51,91 +52,20 @@ export default async function Page({ params }: { params: Collection }) { <>
- Tracking ID{" "} - {tracker?.tracking_number} -
- - {!isNone(tracker?.estimated_delivery) && ( -- - {tracker?.delivered - ? "Delivered" - : "Estimated Delivery"} - {" "} - - {formatDayDate(tracker!.estimated_delivery as string)} - -
- )} +