Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions .claude/rules/git-workflow.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Git & Commit Guidelines

## ⛔ `main` is protected — never write to it without explicit permission

NEVER merge, push, force-push, revert, or otherwise modify the `main` (default)
branch — directly or via `gh pr merge` — without the user's **explicit,
per-action** go-ahead. This includes merging community/contributor PRs: their
base must be the active release branch, not `main`.

- All integration happens on the release branch (e.g. `patch/YYYY.M.PATCH`) or a
feature branch. `main` only advances when the user explicitly says to land it.
- "Merge this PR" / "approve this PR" defaults to the **release branch**, not
`main`. If `main` ever seems required, stop and ask first.
- Branch-protection settings are off-limits unless the user explicitly asks you
to change them, and then only with an exact snapshot-and-restore.
- A blanket approval of one action is not standing permission for the next.

## Commit Format
- `type: summary` (e.g., `fix:`, `feat:`, `chore:`, `refactor:`, `docs:`, `test:`)
- `type(scope): summary` for scoped changes (e.g., `fix(smartkargo): ...`)
Expand All @@ -10,6 +25,7 @@
- Keep commits focused and atomic

## Rules
- Never write to `main` without explicit per-action permission (see top of file)
- Never commit without explicit user permission
- Never add AI co-author lines (e.g., `Co-Authored-By: Claude ...`)
- Never add "Generated with Claude Code" or similar AI footers
Expand Down
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,53 @@
# Karrio 2026.1.32

> Patch release. Resolves six pressing operational and security issues reported
> by self-hosted operators, plus reviewed carrier fixes for FedEx and USPS. No
> breaking changes.

## Changes

### Fix

- fix(providers): guard migration `0093` against cascade data-loss — it now
depends on `manager/0079`, so the legacy carrier FK columns are removed before
the `is_system` carrier rows are deleted and the delete can no longer cascade
into tracking/shipment/pickup history (GH #1116).
- fix(settings): scope the MD5 `PASSWORD_HASHERS` override to the test runner, so
production no longer stores MD5 password hashes or locks out PBKDF2 users on
upgrade (GH #1094).
- fix(events): delete the `periodic_data_archiving` backlog in bounded batches to
avoid OOM on the first run after deployment (GH #1125).
- fix(settings): import `workers` before `apm` so Huey binds the configured
`REDIS_HOST` when `OTEL_ENABLED=true` instead of falling back to localhost
(GH #1124).
- fix(manager): make migration `0078_populate_carrier_snapshots` production-safe
— chunked iterator + `bulk_update`, idempotent (GH #1123).
- fix(core): clean up async DB connections after tracing writes to stop the
connection/memory runaway introduced with trace persistence (GH #1119, phase 1).
- fix(usps): update USPS and USPS International server URLs to `apis.usps.com` /
`apis-tem.usps.com` after USPS retired the legacy Web Tools / `api-cat` hosts;
fixes test-mode "Invalid credentials" (GH #1118).
- fix(fedex): populate the full set of shipment `customerReferences`
(INVOICE_NUMBER, CUSTOMER_REFERENCE, DEPARTMENT_NUMBER, P_O_NUMBER,
RMA_ASSOCIATION); fixes the empty REF field on labels (GH #1082).

### Feat

- feat(fedex): make `pickupType` settable via the `fedex_pickup_type` shipping
option (DROPOFF_AT_FEDEX_LOCATION / CONTACT_FEDEX_TO_SCHEDULE /
USE_SCHEDULED_PICKUP); default behaviour unchanged (GH #1105).
- feat(fedex): pickup improvements — map `instruction` to `remarks`, add the
`fedex_pickup_address_type` option, and resolve `package_location` through an
enum (GH #1112).

### Chore

- chore(usps): vendor the official USPS Developer Portal v3 OpenAPI specs for the
`usps` and `usps_international` connectors.
- chore(rules): require explicit permission before writing to `main`.

---

# Karrio 2026.1.31

> Hotfix release. Tracking lookups returned a 500 with
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ npm run build # Turbo build a

## Commit Rules

- **NEVER write to `main` (merge/push/force-push/revert, incl. `gh pr merge`) without explicit per-action permission.** Integrate on the release/feature branch; merge contributor PRs into the release branch, not `main`. See `.claude/rules/git-workflow.md`.
- Format: `type(scope): summary` — never commit without user permission
- Never add `Co-Authored-By` lines
- Run tests before pushing
Expand Down
191 changes: 191 additions & 0 deletions PRDs/FEDEX_SHIPMENT_PICKUP_TYPE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# Plan: FedEx Configurable `fedex_pickup_type` Option

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).

## Context

`pickupType` is hardcoded as `"DROPOFF_AT_FEDEX_LOCATION"` in both:

- `modules/connectors/fedex/karrio/providers/fedex/shipment/create.py` (line 293)
- `modules/connectors/fedex/karrio/providers/fedex/rate.py` (line 214)

Valid FedEx Ship API values (from `ship-api.json`, `rate-api.json`, and the API Reference Guide `#pickuptypes` table):

| Enumeration | Description |
| --------------------------- | ------------------------------------------------------------------ |
| `DROPOFF_AT_FEDEX_LOCATION` | Shipment will be dropped off at a FedEx Location (current default) |
| `CONTACT_FEDEX_TO_SCHEDULE` | FedEx will be contacted to request a pickup |
| `USE_SCHEDULED_PICKUP` | Shipment will be picked up as part of a regular scheduled pickup |

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.

## Design Decisions

- **FedEx-only `ShippingOption`** — no SDK `ShipmentRequest` model changes
- 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.
- `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
- `help` text exposure in the API endpoint is out of scope — separate PR
- Purolator (`PickupType`: `DropOff`/`PreScheduled`) and DHL Poland (`dropOffType`: `REGULAR_PICKUP`) also hardcode this concept — noted as future work, not in scope here
- Default remains `DROPOFF_AT_FEDEX_LOCATION` — zero behaviour change for existing integrations

## Implementation Steps

### Step 1 — `units.py`: add `FedExPickupType` StrEnum

File: `modules/connectors/fedex/karrio/providers/fedex/units.py`

Add before the `ConnectionConfig` class:

```python
class FedExPickupType(lib.StrEnum):
"""How the shipper will tender the package to FedEx (Ship API / Rate API)."""
# Shipper brings the package to a FedEx drop-off location
dropoff_at_fedex_location = "DROPOFF_AT_FEDEX_LOCATION"
# FedEx will be contacted to schedule a one-time pickup
contact_fedex_to_schedule = "CONTACT_FEDEX_TO_SCHEDULE"
# Package will be collected as part of a regular standing pickup schedule
use_scheduled_pickup = "USE_SCHEDULED_PICKUP"
```

### Step 2 — `units.py`: add `fedex_pickup_type` to `ShippingOption`

In the same file, add to the `ShippingOption` enum inside the delivery options group (near `fedex_saturday_delivery`):

```python
fedex_pickup_type = lib.OptionEnum(
"fedex_pickup_type",
str,
help=(
"How the shipper will tender the package to FedEx. "
"Valid values: DROPOFF_AT_FEDEX_LOCATION, CONTACT_FEDEX_TO_SCHEDULE, USE_SCHEDULED_PICKUP. "
"Defaults to DROPOFF_AT_FEDEX_LOCATION."
),
meta=dict(category="DELIVERY_OPTIONS"),
)
```

### Step 3 — `shipment/create.py`: replace hardcoded `pickupType`

File: `modules/connectors/fedex/karrio/providers/fedex/shipment/create.py`, line 293

Replace:

```python
pickupType="DROPOFF_AT_FEDEX_LOCATION",
```

With:

```python
pickupType=(options.fedex_pickup_type.state or "DROPOFF_AT_FEDEX_LOCATION"),
```

### Step 4 — `rate.py`: replace hardcoded `pickupType`

File: `modules/connectors/fedex/karrio/providers/fedex/rate.py`, line 214

Replace:

```python
pickupType="DROPOFF_AT_FEDEX_LOCATION",
```

With:

```python
pickupType=(options.fedex_pickup_type.state or "DROPOFF_AT_FEDEX_LOCATION"),
```

### Step 5 — `i18n.py`: add translation entry

File: `modules/connectors/fedex/karrio/providers/fedex/i18n.py`

Add to `OPTION_NAME_TRANSLATIONS`:

```python
"fedex_pickup_type": _("FedEx Pickup Type"),
```

### Step 6 — `test_shipment.py`: add 2 new test methods + fixture constants

File: `modules/connectors/fedex/tests/fedex/test_shipment.py`

Add two fixture constants (minimal diffs of the existing `ShipmentRequest` fixture with only `pickupType` changed):

```python
ShipmentUseScheduledPickupRequest = {
... # copy of ShipmentRequest with "pickupType": "USE_SCHEDULED_PICKUP"
}

ShipmentContactFedexPickupRequest = {
... # copy of ShipmentRequest with "pickupType": "CONTACT_FEDEX_TO_SCHEDULE"
}
```

Add two new test methods to `TestFedExShipping`:

```python
def test_create_shipment_request_with_use_scheduled_pickup(self):
request = gateway.mapper.create_shipment_request(
models.ShipmentRequest(**{**ShipmentPayload, "options": {"fedex_pickup_type": "USE_SCHEDULED_PICKUP"}})
)
self.assertEqual(request.serialize(), ShipmentUseScheduledPickupRequest)

def test_create_shipment_request_with_contact_fedex_pickup(self):
request = gateway.mapper.create_shipment_request(
models.ShipmentRequest(**{**ShipmentPayload, "options": {"fedex_pickup_type": "CONTACT_FEDEX_TO_SCHEDULE"}})
)
self.assertEqual(request.serialize(), ShipmentContactFedexPickupRequest)
```

### Step 7 — `test_rate.py`: add 1 new test method + fixture constant

File: `modules/connectors/fedex/tests/fedex/test_rate.py`

Add one fixture constant (minimal diff of existing `RateRequest` with only `pickupType` changed):

```python
RateUseScheduledPickupRequest = {
... # copy of RateRequest with "pickupType": "USE_SCHEDULED_PICKUP"
}
```

Add one new test method to `TestFedExRating`:

```python
def test_create_rate_request_with_use_scheduled_pickup(self):
request = gateway.mapper.create_rate_request(
models.RateRequest(**{**RatePayload, "options": {"fedex_pickup_type": "USE_SCHEDULED_PICKUP"}})
)
self.assertEqual(request.serialize(), RateUseScheduledPickupRequest)
```

## Files Changed

| File | Change |
| -------------------------------------------------------------------- | --------------------------------------------------------------- |
| `modules/connectors/fedex/karrio/providers/fedex/units.py` | Add `FedExPickupType` enum + `ShippingOption.fedex_pickup_type` |
| `modules/connectors/fedex/karrio/providers/fedex/shipment/create.py` | Replace hardcoded `pickupType` at line 293 |
| `modules/connectors/fedex/karrio/providers/fedex/rate.py` | Replace hardcoded `pickupType` at line 214 |
| `modules/connectors/fedex/karrio/providers/fedex/i18n.py` | Add `"fedex_pickup_type"` translation |
| `modules/connectors/fedex/tests/fedex/test_shipment.py` | Add 2 tests + 2 fixture constants |
| `modules/connectors/fedex/tests/fedex/test_rate.py` | Add 1 test + 1 fixture constant |

## Verification

```bash
source bin/activate-env
python -m unittest discover -v -f modules/connectors/fedex/tests
```

All 7 existing tests must pass unchanged — confirming `DROPOFF_AT_FEDEX_LOCATION` remains the default when no option is supplied.

The 3 new tests each call `request.serialize()` and directly assert `"pickupType"` in the resulting dict equals the option value passed.

## Future Work (out of scope)

- Expose `OptionEnum.help` through `GET /v1/carriers/fedex/options` API endpoint
- Add equivalent `purolator_pickup_type` option to Purolator connector (`DropOff`/`PreScheduled`)
- Add equivalent `dhl_poland_dropoff_type` option to DHL Poland connector
- Consider a unified `tendering_type` field on `ShipmentRequest` once all three carriers are done
- 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
Loading
Loading