Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5064656
Fallback to 'receivedByName' if Fedex's 'signedByName' is missing whe…
ChrisNolan Sep 5, 2024
2248e6d
Update my tracking re estimated delivery dates to try and avoid merge…
ChrisNolan Sep 5, 2024
aedfc93
Trying to prevent merge conflicts
ChrisNolan Sep 5, 2024
00443bc
Merge branch 'fedex_tracker_updates' of https://github.com/saunders-b…
ChrisNolan Sep 11, 2024
8b28f8b
Correct typo "Delet" -> "Delete" and add tracking number to the dialog
ChrisNolan Sep 18, 2024
e7e8a97
If the tracker has been Delivered, include the signed_by as part of t…
ChrisNolan Sep 18, 2024
eeead1e
Show error directly (in case of no API server running for example), a…
ChrisNolan Sep 18, 2024
a92bfb1
Display available 'info' fields in the tracker preview header
ChrisNolan Sep 18, 2024
3a01862
Refactor tracking-preview and tracking-page so they don't repeat them…
ChrisNolan Sep 18, 2024
09d8fb9
Fix Fedex's SPOD (Signature Proof of Delivery) logic
ChrisNolan Sep 19, 2024
9f9a056
More effort to try and get the '404' page to actually show... ugly bu…
ChrisNolan Sep 19, 2024
9f47df0
minor clean-up
ChrisNolan Oct 1, 2024
84049ce
Typescript issues etc
ChrisNolan Oct 1, 2024
7293190
remove a bunch of unnecessary debug logging
ChrisNolan Oct 1, 2024
a648a0d
remove unnecessary logging
ChrisNolan Oct 1, 2024
dc0c442
Fallback to 'receivedByName' if Fedex's 'signedByName' is missing whe…
ChrisNolan Sep 5, 2024
223727b
Trying to prevent merge conflicts
ChrisNolan Sep 5, 2024
ca23bec
Correct typo "Delet" -> "Delete" and add tracking number to the dialog
ChrisNolan Sep 18, 2024
5a1a784
If the tracker has been Delivered, include the signed_by as part of t…
ChrisNolan Sep 18, 2024
2ea146f
Display available 'info' fields in the tracker preview header
ChrisNolan Sep 18, 2024
a8ed7cc
Refactor tracking-preview and tracking-page so they don't repeat them…
ChrisNolan Sep 18, 2024
f32c7cd
Fix Fedex's SPOD (Signature Proof of Delivery) logic
ChrisNolan Sep 19, 2024
f523562
minor clean-up
ChrisNolan Oct 1, 2024
19062e6
Typescript issues etc
ChrisNolan Oct 1, 2024
b8c9ed9
remove a bunch of unnecessary debug logging
ChrisNolan Oct 1, 2024
0331242
remove unnecessary logging
ChrisNolan Oct 1, 2024
d1dff9e
merge after rebase?
ChrisNolan Oct 2, 2024
46bb061
Merge branch 'fedex_tracker_updates' of https://github.com/saunders-b…
ChrisNolan Oct 2, 2024
14af461
address codacy issues
ChrisNolan Oct 2, 2024
e231052
refactor 'Additional Info' in TrackingHeader to be 'prettier' and col…
ChrisNolan Oct 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions modules/connectors/fedex/karrio/mappers/fedex/proxy.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
20 changes: 18 additions & 2 deletions modules/connectors/fedex/karrio/providers/fedex/tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
39 changes: 2 additions & 37 deletions modules/connectors/fedex/karrio/providers/fedex/utils.py
Original file line number Diff line number Diff line change
@@ -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."""
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion modules/connectors/fedex/tests/fedex/test_tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
58 changes: 58 additions & 0 deletions packages/core/components/TrackingEvents.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="my-6">
<aside className="menu">
<ul className="menu-list mb-5" style={{ maxWidth: "28rem" }}>
{Object.entries(eventsByDay).map(([day, events], index) => (
<li key={index}>
<p className="menu-label is-size-6 is-capitalized">{day}</p>
{events.map((event, index) => (
<ul key={index}>
<li className="my-2">
<code>{event.time}</code>
<span className="is-subtitle is-size-7 my-1 has-text-weight-semibold">
{event.location}
</span>
</li>
<li className="my-2">
<span className="is-subtitle is-size-7 my-1 has-text-weight-semibold has-text-grey">
{event.description}
</span>
</li>
</ul>
))}
</li>
))}
</ul>
</aside>
</div>
);
};

export default TrackingEvents;
142 changes: 142 additions & 0 deletions packages/core/components/TrackingHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// components/TrackingHeader.tsx
"use client";

import { useState } from "react";
import { TrackerStatusEnum, TrackerType } from "@karrio/types";
import { formatDayDate, formatRef, isNone } from "@karrio/lib";
import { CarrierImage } from "@karrio/ui/components/carrier-image";

const TrackingHeader: React.FC<{ tracker: TrackerType }> = ({ tracker }) => {
const [isExpanded, setIsExpanded] = useState(false); // State to control expansion

const computeColor = (tracker: TrackerType) => {
if (tracker?.delivered) return "has-background-success";
else if (tracker?.status === TrackerStatusEnum.pending.toString())
return "has-background-grey-dark";
else if (
[
TrackerStatusEnum.on_hold.toString(),
TrackerStatusEnum.delivery_delayed.toString(),
].includes(tracker?.status as string)
)
return "has-background-warning";
else if (
[TrackerStatusEnum.unknown.toString()].includes(
tracker?.status as string,
)
)
return "has-background-grey";
else if (
[TrackerStatusEnum.delivery_failed.toString()].includes(
tracker?.status as string,
)
)
return "has-background-danger";
else return "has-background-info";
};

const computeStatus = (tracker: TrackerType) => {
if (tracker?.delivered) return "Delivered";
else if (tracker?.status === TrackerStatusEnum.pending.toString())
return "Pending";
else if (
[
TrackerStatusEnum.on_hold.toString(),
TrackerStatusEnum.delivery_delayed.toString(),
TrackerStatusEnum.ready_for_pickup.toString(),
TrackerStatusEnum.unknown.toString(),
TrackerStatusEnum.delivery_failed.toString(),
].includes(tracker?.status as string)
)
return formatRef(tracker!.status as string);
else return "In-Transit";
};

const prettifyKey = (key: string) => {
// Convert snake_case to "Pretty Case"
return key
.replace(/_/g, " ") // Replace underscores with spaces
.replace(/\b\w/g, (char) => char.toUpperCase()); // Capitalize each word
};

const customRender = (key: string, value: any) => {
switch (key) {
case "carrier_tracking_link":
return (
<a href={String(value)} target="_blank" rel="noopener noreferrer">
{String(value)}
</a>
);
// Add more cases as needed for custom rendering of specific fields
default:
return <span>{String(value)}</span>;
}
};

return (
<>
<div className="pb-4 is-flex is-justify-content-center">
<CarrierImage
carrier_name={tracker.carrier_name}
width={60}
height={60}
/>
</div>

<p className="subtitle has-text-centered is-6 my-3">
<span>Tracking ID</span> <strong>{tracker.tracking_number}</strong>
</p>

{!isNone(tracker?.estimated_delivery) && (
<p className="subtitle has-text-centered is-6 mb-3">
<span>{tracker?.delivered ? "Delivered" : "Estimated Delivery"}</span>{" "}
<strong>
{formatDayDate(tracker!.estimated_delivery as string)}
</strong>
</p>
)}

{!isNone(tracker?.info) && (
<>
<hr />
{/* Accessible button to toggle section */}
<button
onClick={() => setIsExpanded(!isExpanded)}
aria-expanded={isExpanded} // Accessibility: indicate expanded state
aria-controls="additional-info" // Accessibility: points to content
className="has-text-weight-bold my-4 is-size-6 is-flex is-align-items-center"
style={{ cursor: "pointer", background: "none", border: "none" }}
>
<span>{isExpanded ? "-" : "+"}</span>
<span className="ml-2">Additional Information</span>
</button>

{/* Conditionally render additional info */}
{isExpanded && (
<div id="additional-info" className="columns is-multiline">
{Object.entries(tracker.info || {})
.filter(([_, value]) => value != null) // Exclude null or undefined values
.map(([key, value], index) => (
<div key={index} className="column is-6">
<strong>{prettifyKey(key)}: </strong>{" "}
{customRender(key, value)}
</div>
))}
</div>
)}
</>
)}

<p
className={
computeColor(tracker as TrackerType) +
" block has-text-centered has-text-white is-size-4 py-3"
}
>
{computeStatus(tracker as TrackerType)}
</p>
</>
);
};

export default TrackingHeader;
Loading