From 388405d603aa91531c6c38afb866490875dee0f2 Mon Sep 17 00:00:00 2001 From: Daniel K Date: Mon, 30 Sep 2024 07:19:18 -0700 Subject: [PATCH] feat(WIP): Introduce shipping_date for datetime format for carrier expecting ship date with time --- .../providers/eshipper/shipment/create.py | 12 +- .../karrio/providers/eshipper/units.py | 12 +- .../eshipper/tests/eshipper/test_shipment.py | 5 +- .../core/karrio/server/core/serializers.py | 12 +- modules/core/karrio/server/core/validators.py | 15 +- .../server/manager/serializers/shipment.py | 12 +- .../server/manager/tests/test_shipments.py | 10 +- .../server/proxy/tests/test_shipping.py | 2 +- modules/sdk/karrio/core/units.py | 9 +- .../core/modules/Labels/create_labels.tsx | 13 +- packages/core/modules/Orders/create_label.tsx | 12 +- .../core/modules/Shipments/create_label.tsx | 15 +- packages/hooks/label-data.ts | 2 +- packages/ui/forms/shipment-options.tsx | 536 +++++++++++------- 14 files changed, 394 insertions(+), 273 deletions(-) diff --git a/modules/connectors/eshipper/karrio/providers/eshipper/shipment/create.py b/modules/connectors/eshipper/karrio/providers/eshipper/shipment/create.py index dce1e95495..e64bb0a0ee 100644 --- a/modules/connectors/eshipper/karrio/providers/eshipper/shipment/create.py +++ b/modules/connectors/eshipper/karrio/providers/eshipper/shipment/create.py @@ -1,4 +1,3 @@ -import time import karrio.schemas.eshipper.shipping_request as eshipper import karrio.schemas.eshipper.shipping_response as shipping import typing @@ -133,15 +132,14 @@ def shipment_request( else None ), ) - now = datetime.datetime.now() + datetime.timedelta(minutes=5) - shipping_time = lib.ftime(options.shipping_time.state or now, "%H:%M") - shipping_date = lib.fdate(options.shipping_date.state or now) request = eshipper.ShippingRequestType( scheduledShipDate=lib.fdatetime( - f"{shipping_date} {shipping_time}", - current_format="%Y-%m-%d %H:%M", - output_format="%Y-%m-%d %H:%M", + lib.to_next_business_datetime( + options.shipping_date.state or datetime.datetime.now(), + current_format="%Y-%m-%dT%H:%M", + ), + output_format="%Y-%m-%dT%H:%M:%S.%fZ", # 2024-09-30T09:10:29.195Z ), shippingrequestfrom=eshipper.FromType( attention=shipper.contact, diff --git a/modules/connectors/eshipper/karrio/providers/eshipper/units.py b/modules/connectors/eshipper/karrio/providers/eshipper/units.py index 2869383deb..12b69f0ce7 100644 --- a/modules/connectors/eshipper/karrio/providers/eshipper/units.py +++ b/modules/connectors/eshipper/karrio/providers/eshipper/units.py @@ -113,7 +113,7 @@ def to_service_code(service: typing.Dict[str, str]) -> str: parts = list( dict.fromkeys( [ - to_carrier_code(service.get("carrierDTO")), + to_carrier_code(service.get("carrierDTO")), # type: ignore *[ _.lower() for _ in ( @@ -132,7 +132,7 @@ def to_service_code(service: typing.Dict[str, str]) -> str: return output -def get_service(search: str, test_mode: bool = False, service_id: str = None) -> str: +def get_service(search: str, test_mode: bool = False, service_id: str = None): prod_metadata = METADATA_JSON["PROD_SERVICES"] test_metadata = METADATA_JSON["DEV_SERVICES"] metadata = lib.identity( @@ -152,7 +152,7 @@ def get_service(search: str, test_mode: bool = False, service_id: str = None) -> ) -def get_service_id(search: str, test_mode: bool = False, service_id: str = None) -> str: +def get_service_id(search: str, test_mode: bool = False, service_id: str = None): return ( get_service(search, test_mode=test_mode, service_id=service_id).get("id") or service_id @@ -177,7 +177,7 @@ def get_carrier( test_mode: bool = False, service_search: str = None, service_id: str = None, -) -> str: +): id_key = "test_id" if test_mode else "prod_id" alternate_key = "prod_id" if not test_mode else "test_id" service = get_service(service_search, test_mode=test_mode, service_id=service_id) @@ -204,7 +204,7 @@ def get_carrier_id( test_mode: bool = False, service_search: str = None, service_id: str = None, -) -> str: +): return get_carrier( search, test_mode=test_mode, @@ -218,7 +218,7 @@ def find_rate_provider( test_mode: bool = False, service_search: str = None, service_id: str = None, -) -> str: +): if RateProvider.map(lib.to_snake_case(search)).name: return RateProvider.map(lib.to_snake_case(search)) diff --git a/modules/connectors/eshipper/tests/eshipper/test_shipment.py b/modules/connectors/eshipper/tests/eshipper/test_shipment.py index c34f1efe5a..dfbb8ab76a 100644 --- a/modules/connectors/eshipper/tests/eshipper/test_shipment.py +++ b/modules/connectors/eshipper/tests/eshipper/test_shipment.py @@ -113,8 +113,7 @@ def test_parse_cancel_shipment_response(self): }, ], "options": { - "shipping_date": "2024-07-16", - "shipping_time": "10:30", + "shipping_date": "2024-07-16T10:30", }, } @@ -179,7 +178,7 @@ def test_parse_cancel_shipment_response(self): "type": "Package", }, "packagingUnit": "Metric", - "scheduledShipDate": "2024-07-16 10:30", + "scheduledShipDate": "2024-07-16T10:30:00.000000Z", "serviceId": 4500, "from": { "address1": "9, Van Der Graaf Court", diff --git a/modules/core/karrio/server/core/serializers.py b/modules/core/karrio/server/core/serializers.py index 73c4604927..79cdd1ec0d 100644 --- a/modules/core/karrio/server/core/serializers.py +++ b/modules/core/karrio/server/core/serializers.py @@ -652,8 +652,8 @@ class RateRequest(validators.OptionDefaultSerializer): "hold_at_location": true, "paperless_trade": true, "preferred_service": "fedex_express_saver", - "shipment_date": "2020-01-01", - "shipping_date": "2020-01-01T00:00:00", + "shipment_date": "2020-01-01", # TODO: deprecate + "shipping_date": "2020-01-01T00:00", "shipment_note": "This is a shipment note", "signature_confirmation": true, "saturday_delivery": true, @@ -1253,8 +1253,8 @@ class ShippingData(validators.OptionDefaultSerializer): "hold_at_location": true, "paperless_trade": true, "preferred_service": "fedex_express_saver", - "shipment_date": "2020-01-01", - "shipping_date": "2020-01-01T00:00:00", + "shipment_date": "2020-01-01", # TODO: deprecate + "shipping_date": "2020-01-01T00:00", "shipment_note": "This is a shipment note", "signature_confirmation": true, "saturday_delivery": true, @@ -1476,8 +1476,8 @@ class ShipmentContent(serializers.Serializer): "hold_at_location": true, "paperless_trade": true, "preferred_service": "fedex_express_saver", - "shipment_date": "2020-01-01", - "shipping_date": "2020-01-01T00:00:00", + "shipment_date": "2020-01-01", # TODO: deprecate + "shipping_date": "2020-01-01T00:00", "shipment_note": "This is a shipment note", "signature_confirmation": true, "saturday_delivery": true, diff --git a/modules/core/karrio/server/core/validators.py b/modules/core/karrio/server/core/validators.py index 28f561a09d..923112c292 100644 --- a/modules/core/karrio/server/core/validators.py +++ b/modules/core/karrio/server/core/validators.py @@ -110,23 +110,28 @@ def __init__(self, instance=None, **kwargs): data = kwargs.get("data", {}) if data: options = (data or {}).get("options") or {} + + # TODO: remove this when we have a standard shipping date field shipment_date = lib.to_date( options.get("shipment_date") or (getattr(instance, "options", None) or {}).get("shipment_date") ) shipping_date = lib.to_date( options.get("shipping_date") - or (getattr(instance, "options", None) or {}).get("shipping_date") + or (getattr(instance, "options", None) or {}).get("shipping_date"), + current_format="%Y-%m-%dT%H:%M", ) - if shipment_date is None or shipment_date.date() < datetime.now().date(): + # TODO: remove this when we have a standard shipping date field + if ( + shipment_date is not None + and shipment_date.date() < datetime.now().date() + ): options.update(shipment_date=datetime.now().strftime("%Y-%m-%d")) kwargs["data"].update(dict(options=options)) if shipping_date is None or shipping_date.date() < datetime.now().date(): - options.update( - shipping_date=datetime.now().strftime("%Y-%m-%dT%H:%M:%S") - ) + options.update(shipping_date=datetime.now().strftime("%Y-%m-%dT%H:%M")) kwargs["data"].update(dict(options=options)) super().__init__(instance, **kwargs) diff --git a/modules/manager/karrio/server/manager/serializers/shipment.py b/modules/manager/karrio/server/manager/serializers/shipment.py index 64e97eac83..99a4deb0ba 100644 --- a/modules/manager/karrio/server/manager/serializers/shipment.py +++ b/modules/manager/karrio/server/manager/serializers/shipment.py @@ -334,8 +334,8 @@ class ShipmentUpdateData(validators.OptionDefaultSerializer): "hold_at_location": true, "paperless_trade": true, "preferred_service": "fedex_express_saver", - "shipment_date": "2020-01-01", - "shipping_date": "2020-01-01T00:00:00", + "shipment_date": "2020-01-01", # TODO: deprecate + "shipping_date": "2020-01-01T00:00", "shipment_note": "This is a shipment note", "signature_confirmation": true, "saturday_delivery": true, @@ -404,8 +404,8 @@ class ShipmentRateData(validators.OptionDefaultSerializer): "hold_at_location": true, "paperless_trade": true, "preferred_service": "fedex_express_saver", - "shipment_date": "2020-01-01", - "shipping_date": "2020-01-01T00:00:00", + "shipment_date": "2020-01-01", # TODO: deprecate + "shipping_date": "2020-01-01T00:00", "shipment_note": "This is a shipment note", "signature_confirmation": true, "saturday_delivery": true, @@ -461,8 +461,8 @@ class ShipmentPurchaseSerializer(Shipment): "hold_at_location": true, "paperless_trade": true, "preferred_service": "fedex_express_saver", - "shipment_date": "2020-01-01", - "shipping_date": "2020-01-01T00:00:00", + "shipment_date": "2020-01-01", # TODO: deprecate + "shipping_date": "2020-01-01T00:00", "shipment_note": "This is a shipment note", "signature_confirmation": true, "saturday_delivery": true, diff --git a/modules/manager/karrio/server/manager/tests/test_shipments.py b/modules/manager/karrio/server/manager/tests/test_shipments.py index 880c7c37f4..c41fe190de 100644 --- a/modules/manager/karrio/server/manager/tests/test_shipments.py +++ b/modules/manager/karrio/server/manager/tests/test_shipments.py @@ -361,7 +361,7 @@ def test_cancel_purchased_shipment(self): "return_address": None, "billing_address": None, "services": [], - "options": {"shipment_date": ANY, "shipping_date": ANY}, + "options": {"shipping_date": ANY}, "customs": None, "reference": None, "carrier_ids": ["canadapost"], @@ -372,7 +372,11 @@ def test_cancel_purchased_shipment(self): } SHIPMENT_OPTIONS = { - "options": {"insurance": 54, "currency": "CAD", "shipment_date": "2050-01-01"}, + "options": { + "insurance": 54, + "currency": "CAD", + "shipping_date": "2050-01-01T10:30", + }, } RETURNED_RATES_VALUE = [ @@ -509,7 +513,7 @@ def test_cancel_purchased_shipment(self): } ], "services": [], - "options": {"shipment_date": ANY}, + "options": {"shipping_date": ANY}, "payment": {"paid_by": "sender", "currency": "CAD", "account_number": None}, "return_address": None, "billing_address": None, diff --git a/modules/proxy/karrio/server/proxy/tests/test_shipping.py b/modules/proxy/karrio/server/proxy/tests/test_shipping.py index a630aa10dd..e17c4090ff 100644 --- a/modules/proxy/karrio/server/proxy/tests/test_shipping.py +++ b/modules/proxy/karrio/server/proxy/tests/test_shipping.py @@ -228,7 +228,7 @@ def test_shipping_failed_cancel(self): } ], "services": [], - "options": {"shipment_date": ANY}, + "options": {"shipment_date": ANY, "shipping_date": ANY}, "payment": {"paid_by": "sender", "currency": "CAD", "account_number": None}, "billing_address": None, "customs": None, diff --git a/modules/sdk/karrio/core/units.py b/modules/sdk/karrio/core/units.py index b619eef6ff..35550ae17a 100644 --- a/modules/sdk/karrio/core/units.py +++ b/modules/sdk/karrio/core/units.py @@ -1123,8 +1123,7 @@ class ShippingOption(utils.Enum): shipment_date = utils.OptionEnum("shipment_date") """TODO: standardize to these""" - shipping_date = utils.OptionEnum("shipping_date") # TODO: change format to datetime - shipping_time = utils.OptionEnum("shipping_time") + shipping_date = utils.OptionEnum("shipping_date") # format: %Y-%m-%dT%H:%M class ShippingOptions(Options): @@ -1174,11 +1173,7 @@ def shipment_date(self) -> utils.OptionEnum: @property def shipping_date(self) -> utils.OptionEnum: - return self[ShippingOption.shipping_date.name] or self.shipment_date - - @property - def shipping_time(self) -> utils.OptionEnum: - return self[ShippingOption.shipping_time.name] + return self[ShippingOption.shipping_date.name] @property def signature_confirmation(self) -> utils.OptionEnum: diff --git a/packages/core/modules/Labels/create_labels.tsx b/packages/core/modules/Labels/create_labels.tsx index 235d5247df..04913d94c0 100644 --- a/packages/core/modules/Labels/create_labels.tsx +++ b/packages/core/modules/Labels/create_labels.tsx @@ -73,6 +73,7 @@ import { useAppMode } from "@karrio/hooks/app-mode"; import { useSearchParams } from "next/navigation"; import { useOrders } from "@karrio/hooks/order"; import Image from "next/legacy/image"; +import moment from "moment"; import React from "react"; export const generateMetadata = dynamicMetadata("Create labels"); @@ -1404,21 +1405,21 @@ export default function Page(pageProps: any) { />
- {/* shipment date */} + {/* shipping date */} onChange(shipment_index, shipment, { options: { ...shipment.options, - shipment_date: e.target.value, + shipping_date: e.target.value, }, }) } diff --git a/packages/core/modules/Orders/create_label.tsx b/packages/core/modules/Orders/create_label.tsx index 7bbff47c23..328fe37e25 100644 --- a/packages/core/modules/Orders/create_label.tsx +++ b/packages/core/modules/Orders/create_label.tsx @@ -776,20 +776,20 @@ export default function Page(pageProps: any) {
- {/* shipment date */} + {/* shipping date */} onChange({ options: { ...shipment.options, - shipment_date: e.target.value, + shipping_date: e.target.value, }, }) } diff --git a/packages/core/modules/Shipments/create_label.tsx b/packages/core/modules/Shipments/create_label.tsx index ec7a6e2b3f..67691611e9 100644 --- a/packages/core/modules/Shipments/create_label.tsx +++ b/packages/core/modules/Shipments/create_label.tsx @@ -65,9 +65,10 @@ import { bundleContexts } from "@karrio/hooks/utils"; import { useLocation } from "@karrio/hooks/location"; import { useAppMode } from "@karrio/hooks/app-mode"; import React, { useEffect, useState } from "react"; +import { useSearchParams } from "next/navigation"; import { useOrders } from "@karrio/hooks/order"; import { Disclosure } from "@headlessui/react"; -import { useSearchParams } from "next/navigation"; +import moment from "moment"; export const generateMetadata = dynamicMetadata("Create Label"); const ContextProviders = bundleContexts([ @@ -870,20 +871,20 @@ export default function CreateLabelPage(pageProps: any) {
- {/* shipment date */} + {/* shipping date */} onChange({ options: { ...shipment.options, - shipment_date: e.target.value, + shipping_date: e.target.value, }, }) } diff --git a/packages/hooks/label-data.ts b/packages/hooks/label-data.ts index 84061bb63c..ad481813dd 100644 --- a/packages/hooks/label-data.ts +++ b/packages/hooks/label-data.ts @@ -40,7 +40,7 @@ const DEFAULT_SHIPMENT_DATA = { shipper: {} as AddressType, recipient: {} as AddressType, parcels: [] as ParcelType[], - options: { shipment_date: moment().format("YYYY-MM-DD") }, + options: { shipping_date: moment().format("YYYY-MM-DDTHH:mm") }, payment: { paid_by: PaidByEnum.sender }, label_type: LabelTypeEnum.PDF, } as ShipmentType; diff --git a/packages/ui/forms/shipment-options.tsx b/packages/ui/forms/shipment-options.tsx index 09dabffc91..54c61e8bef 100644 --- a/packages/ui/forms/shipment-options.tsx +++ b/packages/ui/forms/shipment-options.tsx @@ -1,209 +1,327 @@ -import { CURRENCY_OPTIONS, NotificationType, ShipmentType } from '@karrio/types'; -import { MetadataEditor, MetadataEditorContext } from './metadata-editor'; -import React, { FormEvent, useContext, useReducer, useState } from 'react'; -import { CheckBoxField } from '../components/checkbox-field'; -import { cleanDict, isEqual, isNone } from '@karrio/lib'; -import { ButtonField } from '../components/button-field'; -import { SelectField } from '../components/select-field'; -import { MetadataObjectTypeEnum } from '@karrio/types'; -import { InputField } from '../components/input-field'; -import { Notify } from '../components/notifier'; -import { Loading } from '../components/loader'; - -interface ShipmentOptionsComponent { - shipment: ShipmentType; - onSubmit: (changes: Partial) => Promise; -} - -function reducer(state: any, { name, value }: { name: string, value: string | boolean }) { - switch (name) { - case 'addCOD': - return cleanDict({ ...state, cash_on_delivery: value === true ? "" : undefined }); - case 'addInsurance': - return cleanDict({ ...state, insurance: value === true ? "" : undefined }); - case 'addDeclaredValue': - return cleanDict({ ...state, declared_value: value === true ? "" : undefined }); - default: - return cleanDict({ ...state, [name]: value || undefined }); - }; -} - -export const ShipmentOptions: React.FC = ({ shipment, onSubmit }) => { - const { notify } = useContext(Notify); - const { loading, setLoading } = useContext(Loading); - const [options, dispatch] = useReducer(reducer, shipment?.options, () => shipment?.options); - const [metadata, setMetadata] = useState(shipment?.metadata); - const [reference, setReference] = useState(shipment?.reference); - - const computeDisable = (shipment: ShipmentType, options: any, metadata: any, reference?: string | null) => { - return ( - (isEqual(shipment.options, options) || (options === ({} as any) && shipment.options === ({} as any))) - && (isEqual(shipment.metadata, metadata) || (metadata === ({} as any) && shipment.metadata === ({} as any))) - && shipment.reference === reference - ) - } - const handleChange = (event: React.ChangeEvent) => { - const target = event.target; - const name: string = target.name; - const value = target.type === 'checkbox' ? target.checked : target.value; - - dispatch({ name, value }); - }; - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - try { - if (shipment.id !== undefined) { - setLoading(true); - await onSubmit({ options, metadata, reference }); - notify({ type: NotificationType.success, message: 'Shipment options successfully updated!' }); - } else { - await onSubmit({ options, metadata, reference }); - } - } catch (message: any) { - notify({ type: NotificationType.error, message }); - } - setLoading(false); - }; - - return ( -
- -
- - setReference(e.target.value || null)} - placeholder="shipment reference" - className="is-small" - autoComplete="off" /> - -
- -
- -
- - - - - Add signature confirmation - - -
- - -
- - - Add insurance - - -
- - - - - - {options?.currency} - - -
-
- -
- - - - - {CURRENCY_OPTIONS.map(unit => )} - - -
- -
- - - Collect On Delivery - - -
- - - - - - {options?.currency} - - -
- -
- - -
- - - Add Total Value - - -
- - - - - - {options?.currency} - - -
-
- -
- - - {({ isEditing, editMetadata }) => (<> - -
-

Metadata

- - -
- -
- - )}
-
- -
- - Save - - -
- ) -}; +import { + CURRENCY_OPTIONS, + NotificationType, + ShipmentType, +} from "@karrio/types"; +import { MetadataEditor, MetadataEditorContext } from "./metadata-editor"; +import React, { FormEvent, useContext, useReducer, useState } from "react"; +import { CheckBoxField } from "../components/checkbox-field"; +import { cleanDict, isEqual, isNone } from "@karrio/lib"; +import { ButtonField } from "../components/button-field"; +import { SelectField } from "../components/select-field"; +import { MetadataObjectTypeEnum } from "@karrio/types"; +import { InputField } from "../components/input-field"; +import { Notify } from "../components/notifier"; +import { Loading } from "../components/loader"; +import moment from "moment"; + +interface ShipmentOptionsComponent { + shipment: ShipmentType; + onSubmit: (changes: Partial) => Promise; +} + +function reducer( + state: any, + { name, value }: { name: string; value: string | boolean }, +) { + switch (name) { + case "addCOD": + return cleanDict({ + ...state, + cash_on_delivery: value === true ? "" : undefined, + }); + case "addInsurance": + return cleanDict({ + ...state, + insurance: value === true ? "" : undefined, + }); + case "addDeclaredValue": + return cleanDict({ + ...state, + declared_value: value === true ? "" : undefined, + }); + default: + return cleanDict({ ...state, [name]: value || undefined }); + } +} + +export const ShipmentOptions: React.FC = ({ + shipment, + onSubmit, +}) => { + const { notify } = useContext(Notify); + const { loading, setLoading } = useContext(Loading); + const [options, dispatch] = useReducer( + reducer, + shipment?.options, + () => shipment?.options, + ); + const [metadata, setMetadata] = useState(shipment?.metadata); + const [reference, setReference] = useState(shipment?.reference); + + const computeDisable = ( + shipment: ShipmentType, + options: any, + metadata: any, + reference?: string | null, + ) => { + return ( + (isEqual(shipment.options, options) || + (options === ({} as any) && shipment.options === ({} as any))) && + (isEqual(shipment.metadata, metadata) || + (metadata === ({} as any) && shipment.metadata === ({} as any))) && + shipment.reference === reference + ); + }; + const handleChange = (event: React.ChangeEvent) => { + const target = event.target; + const name: string = target.name; + const value = target.type === "checkbox" ? target.checked : target.value; + + dispatch({ name, value }); + }; + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + try { + if (shipment.id !== undefined) { + setLoading(true); + await onSubmit({ options, metadata, reference }); + notify({ + type: NotificationType.success, + message: "Shipment options successfully updated!", + }); + } else { + await onSubmit({ options, metadata, reference }); + } + } catch (message: any) { + notify({ type: NotificationType.error, message }); + } + setLoading(false); + }; + + return ( +
+
+ setReference(e.target.value || null)} + placeholder="shipment reference" + className="is-small" + autoComplete="off" + /> +
+ +
+ +
+ + dispatch({ + name: "shipping_date", + value: moment(e.target.value).format("YYYY-MM-DDTHH:mm:ssZ"), + }) + } + label="shipping date" + name="shipping_date" + type="datetime-local" + className="is-small" + fieldClass="column mb-0 is-5 px-2 py-2" + /> + + + Add signature confirmation + +
+ +
+ + Add insurance + + +
+ + + + + {options?.currency} + +
+
+ +
+ + + + {CURRENCY_OPTIONS.map((unit) => ( + + ))} + +
+ +
+ + Collect On Delivery + + +
+ + + + + {options?.currency} + +
+
+ +
+ + Add Total Value + + +
+ + + + + {options?.currency} + +
+
+ +
+ + + + {({ isEditing, editMetadata }) => ( + <> +
+

Metadata

+ + +
+ +
+ + )} +
+
+ +
+ + Save + +
+ ); +};