Skip to content

Commit

Permalink
[feat] Add SmartRate endpoints functions (#225)
Browse files Browse the repository at this point in the history
- Add new `EstimateDeliveryDateForZipPair`, `RecommendShipDateForShipment` and `RecommendShipDateForZipPair`
- Add/update unit tests, re-record cassettes as needed
- Consolidate datetime parsing functionality
  • Loading branch information
nwithan8 authored Jul 16, 2024
1 parent 71c14fe commit c251b4b
Show file tree
Hide file tree
Showing 11 changed files with 563 additions and 101 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Next Release

- Adds new `EstimateDeliveryDateForZipPair`, `RecommendShipDateForShipment` and `RecommendShipDateForZipPair`
- New `CreateUpsCarrierAccount` and `UpdateUpsCarrierAccount` methods and associated parameter structs, required to use for UPS accounts due to new `/ups_oauth_registrations` endpoint.
- Starting `2024-08-05`, UPS accounts will require a new payload to register or update. See [UPS OAuth 2.0 Update](https://support.easypost.com/hc/en-us/articles/26635027512717-UPS-OAuth-2-0-Update) for more details.
- Attempting to use the generic `CreateCarrierAccount` and `UpdateCarrierAccount` methods with UPS accounts will throw an `InvalidFunctionError`.
Expand Down
149 changes: 81 additions & 68 deletions datetime.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,75 +16,10 @@ func (dt *DateTime) UnmarshalJSON(b []byte) (err error) {
var t time.Time

// try to parse
// 2006-01-02
t, err = time.Parse(`"2006-01-02"`, string(b))
if err == nil {
*dt = DateTime(t)
return
}

// try to parse
// 2006-01-02T15:04:05Z
t, err = time.Parse(`"2006-01-02T15:04:05Z"`, string(b))
if err == nil {
*dt = DateTime(t)
return
}

// try to parse as RFC3339 (default for time.Time)
// 2006-01-02T15:04:05Z07:00
t, err = time.Parse(`"`+time.RFC3339+`"`, string(b))
if err == nil {
*dt = DateTime(t)
return
}

// try to parse as RFC3339Nano
// 2006-01-02T15:04:05.999999999Z07:00
t, err = time.Parse(`"`+time.RFC3339Nano+`"`, string(b))
if err == nil {
*dt = DateTime(t)
return
}

// try to parse as RFC1123
// Mon, 02 Jan 2006 15:04:05 MST
t, err = time.Parse(`"`+time.RFC1123+`"`, string(b))
if err == nil {
*dt = DateTime(t)
return
}

// try to parse as RFC1123Z
// Mon, 02 Jan 2006 15:04:05 -0700
t, err = time.Parse(`"`+time.RFC1123Z+`"`, string(b))
if err == nil {
*dt = DateTime(t)
return
}

// try to parse as RFC822
// 02 Jan 06 15:04 MST
t, err = time.Parse(`"`+time.RFC822+`"`, string(b))
asDateTime, err := DateTimeFromString(string(b))
if err == nil {
*dt = DateTime(t)
return
}

// try to parse as RFC822Z
// 02 Jan 06 15:04 -0700
t, err = time.Parse(`"`+time.RFC822Z+`"`, string(b))
if err == nil {
*dt = DateTime(t)
return
}

// try to parse as RFC850
// Monday, 02-Jan-06 15:04:05 MST
t, err = time.Parse(`"`+time.RFC850+`"`, string(b))
if err == nil {
*dt = DateTime(t)
return
*dt = asDateTime
return nil
}

// last ditch effort, fallback to whatever the JSON marshaller thinks
Expand Down Expand Up @@ -141,3 +76,81 @@ func NewDateTime(year int, month time.Month, day, hour, min, sec, nsec int, loc
func DateTimeFromTime(t time.Time) DateTime {
return DateTime(t)
}

func DateTimeFromString(s string) (dt DateTime, err error) {
var t time.Time

// try to parse
// 2006-01-02
t, err = time.Parse(`"2006-01-02"`, s)
if err == nil {
dt = DateTime(t)
return
}

// try to parse
// 2006-01-02T15:04:05Z
t, err = time.Parse(`"2006-01-02T15:04:05Z"`, s)
if err == nil {
dt = DateTime(t)
return
}

// try to parse as RFC3339 (default for time.Time)
// 2006-01-02T15:04:05Z07:00
t, err = time.Parse(`"`+time.RFC3339+`"`, s)
if err == nil {
dt = DateTime(t)
return
}

// try to parse as RFC3339Nano
// 2006-01-02T15:04:05.999999999Z07:00
t, err = time.Parse(`"`+time.RFC3339Nano+`"`, s)
if err == nil {
dt = DateTime(t)
return
}

// try to parse as RFC1123
// Mon, 02 Jan 2006 15:04:05 MST
t, err = time.Parse(`"`+time.RFC1123+`"`, s)
if err == nil {
dt = DateTime(t)
return
}

// try to parse as RFC1123Z
// Mon, 02 Jan 2006 15:04:05 -0700
t, err = time.Parse(`"`+time.RFC1123Z+`"`, s)
if err == nil {
dt = DateTime(t)
return
}

// try to parse as RFC822
// 02 Jan 06 15:04 MST
t, err = time.Parse(`"`+time.RFC822+`"`, s)
if err == nil {
dt = DateTime(t)
return
}

// try to parse as RFC822Z
// 02 Jan 06 15:04 -0700
t, err = time.Parse(`"`+time.RFC822Z+`"`, s)
if err == nil {
dt = DateTime(t)
return
}

// try to parse as RFC850
// Monday, 02-Jan-06 15:04:05 MST
t, err = time.Parse(`"`+time.RFC850+`"`, s)
if err == nil {
dt = DateTime(t)
return
}

return
}
28 changes: 24 additions & 4 deletions shipment.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ type EstimatedDeliveryDate struct {
Rate SmartRate `json:"rate,omitempty"`
}

// RecommendShipDateForShipmentResult is the result of the RecommendShipDateForShipment method.
type RecommendShipDateForShipmentResult struct {
Rate *SmartRate `json:"rate,omitempty"`
EasyPostTimeInTransitData *TimeInTransitDetailsForShipDate `json:"easypost_time_in_transit_data,omitempty"`
}

// CreateShipment creates a new Shipment object. The ToAddress, FromAddress and
// Parcel attributes are required. These objects may be fully-specified to
// create new ones at the same time as creating the Shipment, or they can refer
Expand Down Expand Up @@ -385,13 +391,12 @@ func (c *Client) GenerateShipmentFormWithOptionsWithContext(ctx context.Context,
return
}

// GetShipmentEstimatedDeliveryDate retrieves the estimated delivery date of each Rate via SmartRate.
func (c *Client) GetShipmentEstimatedDeliveryDate(shipmentID, plannedShipDate string) (out []*EstimatedDeliveryDate, err error) {
// GetShipmentEstimatedDeliveryDate retrieves the estimated delivery date of each rate for a Shipment via the Delivery Date Estimator API, based on a specific ship date.
func (c *Client) GetShipmentEstimatedDeliveryDate(shipmentID string, plannedShipDate string) (out []*EstimatedDeliveryDate, err error) {
return c.GetShipmentEstimatedDeliveryDateWithContext(context.Background(), shipmentID, plannedShipDate)
}

// GetShipmentEstimatedDeliveryDateWithContext performs the same operation as GetShipmentEstimatedDeliveryDate,
// but allows specifying a context that can interrupt the request.
// GetShipmentEstimatedDeliveryDateWithContext performs the same operation as EstimateDeliveryDateForShipment, but allows specifying a context that can interrupt the request.
func (c *Client) GetShipmentEstimatedDeliveryDateWithContext(ctx context.Context, shipmentID string, plannedShipDate string) (out []*EstimatedDeliveryDate, err error) {
vals := url.Values{"planned_ship_date": []string{plannedShipDate}}
res := struct {
Expand All @@ -400,3 +405,18 @@ func (c *Client) GetShipmentEstimatedDeliveryDateWithContext(ctx context.Context
err = c.do(ctx, http.MethodGet, "shipments/"+shipmentID+"/smartrate/delivery_date", vals, &res)
return
}

// RecommendShipDateForShipment retrieves the recommended ship date of each rate for a Shipment via the Precision Shipping API, based on a specific desired delivery date.
func (c *Client) RecommendShipDateForShipment(shipmentID string, desiredDeliveryDate string) (out []*RecommendShipDateForShipmentResult, err error) {
return c.RecommendShipDateForShipmentWithContext(context.Background(), shipmentID, desiredDeliveryDate)
}

// RecommendShipDateForShipmentWithContext performs the same operation as RecommendShipDateForShipment, but allows specifying a context that can interrupt the request.
func (c *Client) RecommendShipDateForShipmentWithContext(ctx context.Context, shipmentID string, desiredDeliveryDate string) (out []*RecommendShipDateForShipmentResult, err error) {
vals := url.Values{"desired_delivery_date": []string{desiredDeliveryDate}}
res := struct {
Results *[]*RecommendShipDateForShipmentResult `json:"rates,omitempty"`
}{Results: &out}
err = c.do(ctx, http.MethodGet, "shipments/"+shipmentID+"/smartrate/precision_shipping", vals, &res)
return
}
95 changes: 95 additions & 0 deletions smart_rate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package easypost

import (
"context"
)

// TimeInTransitDetailsForDeliveryDate contains the time-in-transit details and estimated delivery date for a specific DeliveryDateForZipPairEstimate.
type TimeInTransitDetailsForDeliveryDate struct {
PlannedShipDate *DateTime `json:"planned_ship_date,omitempty"`
EasyPostEstimatedDeliveryDate *DateTime `json:"easypost_estimated_delivery_date,omitempty"`
DaysInTransit *TimeInTransit `json:"days_in_transit,omitempty"`
}

// DeliveryDateForZipPairEstimate is a single zip-pair-based delivery date estimate for a carrier-service level combination.
type DeliveryDateForZipPairEstimate struct {
Carrier string `json:"carrier,omitempty"`
Service string `json:"service,omitempty"`
EasyPostTimeInTransitData *TimeInTransitDetailsForDeliveryDate `json:"easypost_time_in_transit_data,omitempty"`
}

// EstimateDeliveryDateForZipPairResult is the result of the EstimateDeliveryDateForZipPair method, containing the estimated delivery date of each carrier-service level combination and additional metadata.
type EstimateDeliveryDateForZipPairResult struct {
CarriersWithoutEstimates []string `json:"carriers_without_tint_estimates,omitempty"`
FromZip string `json:"from_zip,omitempty"`
ToZip string `json:"to_zip,omitempty"`
SaturdayDelivery bool `json:"saturday_delivery,omitempty"`
PlannedShipDate *DateTime `json:"planned_ship_date,omitempty"`
Results []*DeliveryDateForZipPairEstimate `json:"results,omitempty"`
}

// EstimateDeliveryDateForZipPairParams are used in the EstimateDeliveryDateForZipPair method.
type EstimateDeliveryDateForZipPairParams struct {
FromZip string `json:"from_zip,omitempty"`
ToZip string `json:"to_zip,omitempty"`
Carriers []string `json:"carriers,omitempty"`
PlannedShipDate string `json:"planned_ship_date,omitempty"`
SaturdayDelivery bool `json:"saturday_delivery,omitempty"`
}

// TimeInTransitDetailsForShipDate contains the time-in-transit details and estimated delivery date for a specific ShipDateForZipPairRecommendation or RecommendShipDateForShipmentResult.
type TimeInTransitDetailsForShipDate struct {
DesiredDeliveryDate *DateTime `json:"desired_delivery_date,omitempty"`
EasyPostRecommendedShipDate *DateTime `json:"ship_on_date,omitempty"`
DeliveryDateConfidence float64 `json:"delivery_date_confidence,omitempty"`
EstimatedTransitDays int `json:"estimated_transit_days,omitempty"`
DaysInTransit *TimeInTransit `json:"days_in_transit,omitempty"`
}

// ShipDateForZipPairRecommendation is a single zip-pair-based ship date recommendation for a carrier-service level combination.
type ShipDateForZipPairRecommendation struct {
Carrier string `json:"carrier,omitempty"`
Service string `json:"service,omitempty"`
EasyPostTimeInTransitData *TimeInTransitDetailsForShipDate `json:"easypost_time_in_transit_data,omitempty"`
}

// RecommendShipDateForZipPairResult is the result of the RecommendShipDateForZipPair method, containing the recommended ship date of each carrier-service level combination and additional metadata.
type RecommendShipDateForZipPairResult struct {
CarriersWithoutEstimates []string `json:"carriers_without_tint_estimates,omitempty"`
FromZip string `json:"from_zip,omitempty"`
ToZip string `json:"to_zip,omitempty"`
SaturdayDelivery bool `json:"saturday_delivery,omitempty"`
DesiredDeliveryDate *DateTime `json:"desired_delivery_date,omitempty"`
Results []*ShipDateForZipPairRecommendation `json:"results,omitempty"`
}

// RecommendShipDateForZipPairParams are used in the RecommendShipDateForZipPair method.
type RecommendShipDateForZipPairParams struct {
FromZip string `json:"from_zip,omitempty"`
ToZip string `json:"to_zip,omitempty"`
Carriers []string `json:"carriers,omitempty"`
DesiredDeliveryDate string `json:"desired_delivery_date,omitempty"`
SaturdayDelivery bool `json:"saturday_delivery,omitempty"`
}

// EstimateDeliveryDateForZipPair retrieves the estimated delivery date of each carrier-service level combination via the Smart Deliver By API, based on a specific ship date and origin-destination postal code pair.
func (c *Client) EstimateDeliveryDateForZipPair(params *EstimateDeliveryDateForZipPairParams) (out *EstimateDeliveryDateForZipPairResult, err error) {
return c.EstimateDeliveryDateForZipPairWithContext(context.Background(), params)
}

// EstimateDeliveryDateForZipPairWithContext performs the same operation as EstimateDeliveryDateForZipPair, but allows specifying a context that can interrupt the request.
func (c *Client) EstimateDeliveryDateForZipPairWithContext(ctx context.Context, params *EstimateDeliveryDateForZipPairParams) (out *EstimateDeliveryDateForZipPairResult, err error) {
err = c.post(ctx, "smartrate/deliver_by", params, &out)
return
}

// RecommendShipDateForZipPair retrieves the recommended ship date of each carrier-service level combination via the Smart Deliver On API, based on a specific desired delivery date and origin-destination postal code pair.
func (c *Client) RecommendShipDateForZipPair(params *RecommendShipDateForZipPairParams) (out *RecommendShipDateForZipPairResult, err error) {
return c.RecommendShipDateForZipPairWithContext(context.Background(), params)
}

// RecommendShipDateForZipPairWithContext performs the same operation as RecommendShipDateForZipPair, but allows specifying a context that can interrupt the request.
func (c *Client) RecommendShipDateForZipPairWithContext(ctx context.Context, params *RecommendShipDateForZipPairParams) (out *RecommendShipDateForZipPairResult, err error) {
err = c.post(ctx, "smartrate/deliver_on", params, &out)
return
}
56 changes: 56 additions & 0 deletions tests/cassettes/TestEstimateDeliveryDateForZipPair.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit c251b4b

Please sign in to comment.