|
| 1 | +# Plan: FedEx Configurable `fedex_pickup_type` Option |
| 2 | + |
| 3 | +FedEx's `pickupType` field (which describes how a shipper tenders a package — drop off, scheduled, or on-call) is hardcoded to `DROPOFF_AT_FEDEX_LOCATION` in both the rate and shipment request builders. This adds it as a standard FedEx `ShippingOption` so API consumers can send any of the three valid Ship API values. Default behaviour is unchanged (backward-compatible). |
| 4 | + |
| 5 | +## Context |
| 6 | + |
| 7 | +`pickupType` is hardcoded as `"DROPOFF_AT_FEDEX_LOCATION"` in both: |
| 8 | + |
| 9 | +- `modules/connectors/fedex/karrio/providers/fedex/shipment/create.py` (line 293) |
| 10 | +- `modules/connectors/fedex/karrio/providers/fedex/rate.py` (line 214) |
| 11 | + |
| 12 | +Valid FedEx Ship API values (from `ship-api.json`, `rate-api.json`, and the API Reference Guide `#pickuptypes` table): |
| 13 | + |
| 14 | +| Enumeration | Description | |
| 15 | +| --------------------------- | ------------------------------------------------------------------ | |
| 16 | +| `DROPOFF_AT_FEDEX_LOCATION` | Shipment will be dropped off at a FedEx Location (current default) | |
| 17 | +| `CONTACT_FEDEX_TO_SCHEDULE` | FedEx will be contacted to request a pickup | |
| 18 | +| `USE_SCHEDULED_PICKUP` | Shipment will be picked up as part of a regular scheduled pickup | |
| 19 | + |
| 20 | +Note: `ON_CALL`, `PACKAGE_RETURN_PROGRAM`, `REGULAR_STOP` are Pickup API values only — they do not belong in the shipment/rate request and should not be included in this option. |
| 21 | + |
| 22 | +## Design Decisions |
| 23 | + |
| 24 | +- **FedEx-only `ShippingOption`** — no SDK `ShipmentRequest` model changes |
| 25 | +- Unified `PickupRequest.pickup_type` (`one_time`/`daily`/`recurring`) is a _different_ concept (it schedules a carrier driver pickup event). The field we are adding answers "how does this shipment get to the carrier?" — set at shipment creation time. |
| 26 | +- `dpd_meta_dropoff_type` is a false cognate (controls label format at drop-off point, not tendering method) — not a reference pattern for this change |
| 27 | +- `help` text exposure in the API endpoint is out of scope — separate PR |
| 28 | +- Purolator (`PickupType`: `DropOff`/`PreScheduled`) and DHL Poland (`dropOffType`: `REGULAR_PICKUP`) also hardcode this concept — noted as future work, not in scope here |
| 29 | +- Default remains `DROPOFF_AT_FEDEX_LOCATION` — zero behaviour change for existing integrations |
| 30 | + |
| 31 | +## Implementation Steps |
| 32 | + |
| 33 | +### Step 1 — `units.py`: add `FedExPickupType` StrEnum |
| 34 | + |
| 35 | +File: `modules/connectors/fedex/karrio/providers/fedex/units.py` |
| 36 | + |
| 37 | +Add before the `ConnectionConfig` class: |
| 38 | + |
| 39 | +```python |
| 40 | +class FedExPickupType(lib.StrEnum): |
| 41 | + """How the shipper will tender the package to FedEx (Ship API / Rate API).""" |
| 42 | + # Shipper brings the package to a FedEx drop-off location |
| 43 | + dropoff_at_fedex_location = "DROPOFF_AT_FEDEX_LOCATION" |
| 44 | + # FedEx will be contacted to schedule a one-time pickup |
| 45 | + contact_fedex_to_schedule = "CONTACT_FEDEX_TO_SCHEDULE" |
| 46 | + # Package will be collected as part of a regular standing pickup schedule |
| 47 | + use_scheduled_pickup = "USE_SCHEDULED_PICKUP" |
| 48 | +``` |
| 49 | + |
| 50 | +### Step 2 — `units.py`: add `fedex_pickup_type` to `ShippingOption` |
| 51 | + |
| 52 | +In the same file, add to the `ShippingOption` enum inside the delivery options group (near `fedex_saturday_delivery`): |
| 53 | + |
| 54 | +```python |
| 55 | +fedex_pickup_type = lib.OptionEnum( |
| 56 | + "fedex_pickup_type", |
| 57 | + str, |
| 58 | + help=( |
| 59 | + "How the shipper will tender the package to FedEx. " |
| 60 | + "Valid values: DROPOFF_AT_FEDEX_LOCATION, CONTACT_FEDEX_TO_SCHEDULE, USE_SCHEDULED_PICKUP. " |
| 61 | + "Defaults to DROPOFF_AT_FEDEX_LOCATION." |
| 62 | + ), |
| 63 | + meta=dict(category="DELIVERY_OPTIONS"), |
| 64 | +) |
| 65 | +``` |
| 66 | + |
| 67 | +### Step 3 — `shipment/create.py`: replace hardcoded `pickupType` |
| 68 | + |
| 69 | +File: `modules/connectors/fedex/karrio/providers/fedex/shipment/create.py`, line 293 |
| 70 | + |
| 71 | +Replace: |
| 72 | + |
| 73 | +```python |
| 74 | +pickupType="DROPOFF_AT_FEDEX_LOCATION", |
| 75 | +``` |
| 76 | + |
| 77 | +With: |
| 78 | + |
| 79 | +```python |
| 80 | +pickupType=(options.fedex_pickup_type.state or "DROPOFF_AT_FEDEX_LOCATION"), |
| 81 | +``` |
| 82 | + |
| 83 | +### Step 4 — `rate.py`: replace hardcoded `pickupType` |
| 84 | + |
| 85 | +File: `modules/connectors/fedex/karrio/providers/fedex/rate.py`, line 214 |
| 86 | + |
| 87 | +Replace: |
| 88 | + |
| 89 | +```python |
| 90 | +pickupType="DROPOFF_AT_FEDEX_LOCATION", |
| 91 | +``` |
| 92 | + |
| 93 | +With: |
| 94 | + |
| 95 | +```python |
| 96 | +pickupType=(options.fedex_pickup_type.state or "DROPOFF_AT_FEDEX_LOCATION"), |
| 97 | +``` |
| 98 | + |
| 99 | +### Step 5 — `i18n.py`: add translation entry |
| 100 | + |
| 101 | +File: `modules/connectors/fedex/karrio/providers/fedex/i18n.py` |
| 102 | + |
| 103 | +Add to `OPTION_NAME_TRANSLATIONS`: |
| 104 | + |
| 105 | +```python |
| 106 | +"fedex_pickup_type": _("FedEx Pickup Type"), |
| 107 | +``` |
| 108 | + |
| 109 | +### Step 6 — `test_shipment.py`: add 2 new test methods + fixture constants |
| 110 | + |
| 111 | +File: `modules/connectors/fedex/tests/fedex/test_shipment.py` |
| 112 | + |
| 113 | +Add two fixture constants (minimal diffs of the existing `ShipmentRequest` fixture with only `pickupType` changed): |
| 114 | + |
| 115 | +```python |
| 116 | +ShipmentUseScheduledPickupRequest = { |
| 117 | + ... # copy of ShipmentRequest with "pickupType": "USE_SCHEDULED_PICKUP" |
| 118 | +} |
| 119 | + |
| 120 | +ShipmentContactFedexPickupRequest = { |
| 121 | + ... # copy of ShipmentRequest with "pickupType": "CONTACT_FEDEX_TO_SCHEDULE" |
| 122 | +} |
| 123 | +``` |
| 124 | + |
| 125 | +Add two new test methods to `TestFedExShipping`: |
| 126 | + |
| 127 | +```python |
| 128 | +def test_create_shipment_request_with_use_scheduled_pickup(self): |
| 129 | + request = gateway.mapper.create_shipment_request( |
| 130 | + models.ShipmentRequest(**{**ShipmentPayload, "options": {"fedex_pickup_type": "USE_SCHEDULED_PICKUP"}}) |
| 131 | + ) |
| 132 | + self.assertEqual(request.serialize(), ShipmentUseScheduledPickupRequest) |
| 133 | + |
| 134 | +def test_create_shipment_request_with_contact_fedex_pickup(self): |
| 135 | + request = gateway.mapper.create_shipment_request( |
| 136 | + models.ShipmentRequest(**{**ShipmentPayload, "options": {"fedex_pickup_type": "CONTACT_FEDEX_TO_SCHEDULE"}}) |
| 137 | + ) |
| 138 | + self.assertEqual(request.serialize(), ShipmentContactFedexPickupRequest) |
| 139 | +``` |
| 140 | + |
| 141 | +### Step 7 — `test_rate.py`: add 1 new test method + fixture constant |
| 142 | + |
| 143 | +File: `modules/connectors/fedex/tests/fedex/test_rate.py` |
| 144 | + |
| 145 | +Add one fixture constant (minimal diff of existing `RateRequest` with only `pickupType` changed): |
| 146 | + |
| 147 | +```python |
| 148 | +RateUseScheduledPickupRequest = { |
| 149 | + ... # copy of RateRequest with "pickupType": "USE_SCHEDULED_PICKUP" |
| 150 | +} |
| 151 | +``` |
| 152 | + |
| 153 | +Add one new test method to `TestFedExRating`: |
| 154 | + |
| 155 | +```python |
| 156 | +def test_create_rate_request_with_use_scheduled_pickup(self): |
| 157 | + request = gateway.mapper.create_rate_request( |
| 158 | + models.RateRequest(**{**RatePayload, "options": {"fedex_pickup_type": "USE_SCHEDULED_PICKUP"}}) |
| 159 | + ) |
| 160 | + self.assertEqual(request.serialize(), RateUseScheduledPickupRequest) |
| 161 | +``` |
| 162 | + |
| 163 | +## Files Changed |
| 164 | + |
| 165 | +| File | Change | |
| 166 | +| -------------------------------------------------------------------- | --------------------------------------------------------------- | |
| 167 | +| `modules/connectors/fedex/karrio/providers/fedex/units.py` | Add `FedExPickupType` enum + `ShippingOption.fedex_pickup_type` | |
| 168 | +| `modules/connectors/fedex/karrio/providers/fedex/shipment/create.py` | Replace hardcoded `pickupType` at line 293 | |
| 169 | +| `modules/connectors/fedex/karrio/providers/fedex/rate.py` | Replace hardcoded `pickupType` at line 214 | |
| 170 | +| `modules/connectors/fedex/karrio/providers/fedex/i18n.py` | Add `"fedex_pickup_type"` translation | |
| 171 | +| `modules/connectors/fedex/tests/fedex/test_shipment.py` | Add 2 tests + 2 fixture constants | |
| 172 | +| `modules/connectors/fedex/tests/fedex/test_rate.py` | Add 1 test + 1 fixture constant | |
| 173 | + |
| 174 | +## Verification |
| 175 | + |
| 176 | +```bash |
| 177 | +source bin/activate-env |
| 178 | +python -m unittest discover -v -f modules/connectors/fedex/tests |
| 179 | +``` |
| 180 | + |
| 181 | +All 7 existing tests must pass unchanged — confirming `DROPOFF_AT_FEDEX_LOCATION` remains the default when no option is supplied. |
| 182 | + |
| 183 | +The 3 new tests each call `request.serialize()` and directly assert `"pickupType"` in the resulting dict equals the option value passed. |
| 184 | + |
| 185 | +## Future Work (out of scope) |
| 186 | + |
| 187 | +- Expose `OptionEnum.help` through `GET /v1/carriers/fedex/options` API endpoint |
| 188 | +- Add equivalent `purolator_pickup_type` option to Purolator connector (`DropOff`/`PreScheduled`) |
| 189 | +- Add equivalent `dhl_poland_dropoff_type` option to DHL Poland connector |
| 190 | +- Consider a unified `tendering_type` field on `ShipmentRequest` once all three carriers are done |
| 191 | +- The 'create_label` core module for the Dashboard only renders boolean "CheckBoxField" options and skips other options so we can't see this option via the Dashboard atm |
0 commit comments