Skip to content

Commit c6f254b

Browse files
committed
docs: ParcelPath/UPS/USPS integration guide (Task 28)
Adds docs/integrations/parcelpath-tms.md — a single-page reference covering all three integration modes (ParcelPath / Direct / Hybrid), setup steps, broker shipper_client_uuid workflow, rating flow, label purchase, tracking (poll + webhook + real-time), batch shipping, Shipsurance insurance, service type enums, and dev environment notes (Docker mount, migration --realpath, Pest runner). This is the end-user-facing integration documentation that summarizes the full Phase 1 + Phase 2 + Phase 3 implementation across all 28 tasks. Written for an operator or developer setting up the integration from scratch with no prior context. ## Final test validation Full Pest suite across all three phases: Tests: 326 passed (681 assertions) Duration: 1.42s Failures: 0 Deprecated: 1 (pre-existing IntegratedVendor.php:14, not ours) All 28 tasks complete. All code committed on feat/parcelpath-phase3.
1 parent ecfd644 commit c6f254b

File tree

1 file changed

+222
-0
lines changed

1 file changed

+222
-0
lines changed
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
# ParcelPath / UPS / USPS — Small Parcel TMS Integration
2+
3+
## Overview
4+
5+
FleetOps supports three coexisting integration modes for UPS and USPS small-parcel shipping:
6+
7+
| Mode | Description | Carrier credentials needed | Best for |
8+
|---|---|---|---|
9+
| **A — ParcelPath** | Single API key → ParcelPath handles UPS + USPS | ParcelPath API key only | New operators, quick start |
10+
| **B — Direct** | TMS calls UPS/USPS APIs with the operator's own credentials | UPS OAuth + account number, USPS OAuth | Enterprise with negotiated rates |
11+
| **C — Hybrid** | Queries ParcelPath + direct accounts simultaneously | Both | Rate comparison, broker optimization |
12+
13+
All three modes share the same ServiceQuote, Order, TrackingNumber, TrackingStatus, and File models. The difference is which bridge class processes the request.
14+
15+
---
16+
17+
## Setup
18+
19+
### Mode A — ParcelPath (recommended default)
20+
21+
1. Navigate to **Fleet-Ops → Management → Integrated Vendors → New**.
22+
2. Select **ParcelPath** in the provider picker.
23+
3. Enter your ParcelPath API key. Toggle **Sandbox** for testing.
24+
4. Configure options:
25+
- **Carrier filter**: UPS + USPS (default), UPS only, or USPS only
26+
- **Label format**: PDF or ZPL (thermal)
27+
- **Insurance default**: No insurance, Auto-insure all, or Ask per shipment
28+
- **Markup**: optional flat (cents) or percentage markup on top of ParcelPath rates
29+
5. Save. You're ready to rate and ship.
30+
31+
### Mode B — Direct UPS
32+
33+
1. Create a UPS developer account at [developer.ups.com](https://developer.ups.com).
34+
2. Create an app with access to Rating, Shipping, and Tracking APIs.
35+
3. In FleetOps: **Integrated Vendors → New → UPS**.
36+
4. Enter `client_id`, `client_secret`, and `account_number`.
37+
5. Toggle **Sandbox** for the UPS CIE test environment.
38+
6. Optional: configure markup, label format (PDF/ZPL), and Shipsurance insurance.
39+
7. Optional: set **Shipper Client** to scope this credential to a specific vendor (for brokers).
40+
41+
### Mode B — Direct USPS
42+
43+
1. Create a USPS developer account at [developer.usps.com](https://developer.usps.com).
44+
2. Enable the Prices, Labels, and Tracking products.
45+
3. In FleetOps: **Integrated Vendors → New → USPS**.
46+
4. Enter `client_id` and `client_secret`. No account number (USPS rates are zip-scoped).
47+
5. Toggle **Sandbox** for the USPS TEM test environment.
48+
6. Note: USPS labels are **PDF only** — no ZPL option.
49+
50+
### Mode C — Hybrid
51+
52+
1. Set up both a ParcelPath record and one or more direct carrier records.
53+
2. When creating an order, pass multiple facilitator IDs:
54+
```
55+
facilitator=integrated_vendor_pp,integrated_vendor_ups_direct
56+
```
57+
3. The controller queries all specified vendors and returns all rates in a single response.
58+
4. The UI renders each rate with a source badge: "via ParcelPath" or "your account".
59+
60+
---
61+
62+
## Broker / Multi-Tenant Configuration
63+
64+
### The shipper_client_uuid workflow
65+
66+
Brokers managing multiple shipper clients (each with their own UPS/USPS accounts) use the **Shipper Client** field on the IntegratedVendor form:
67+
68+
1. Create one IntegratedVendor per carrier per shipper client, setting the **Shipper Client** dropdown to the corresponding Vendor record.
69+
2. Create one catch-all IntegratedVendor per carrier with **Shipper Client** left blank — this handles any order whose customer doesn't have a dedicated credential.
70+
3. When an order's customer is a Vendor, the `IntegratedVendorResolver` automatically resolves the correct credential: exact match first, catch-all fallback if no match, silent skip if neither exists.
71+
72+
**Safety guarantee:** the resolver never routes through a mismatched credential. If an order's customer is Acme Corp and only TechCo has a dedicated UPS record (with no catch-all), UPS is silently dropped from the rate response rather than billing TechCo's account.
73+
74+
### ParcelPath for brokers
75+
76+
ParcelPath eliminates the multi-record problem: one ParcelPath IntegratedVendor covers all shipper clients. ParcelPath handles per-client billing internally. The broker's margin is managed via the markup option on the IntegratedVendor or within ParcelPath's own dashboard.
77+
78+
---
79+
80+
## Rating
81+
82+
| Endpoint | Mode A | Mode B | Mode C |
83+
|---|---|---|---|
84+
| `GET/POST /v1/service-quotes?facilitator=integrated_vendor_xxx&payload=payload_xxx` | ParcelPath API | UPS Rating API / USPS Prices API | All specified vendors queried |
85+
86+
Rates are returned as `ServiceQuote` records with `ServiceQuoteItem` line items. The `meta` field carries carrier-specific data:
87+
88+
**ParcelPath quotes:**
89+
```json
90+
{
91+
"carrier": "UPS",
92+
"service_token": "ups_ground",
93+
"pp_rate_id": "rate_abc",
94+
"estimated_days": 5,
95+
"carrier_amount": 842,
96+
"insurance_available": true,
97+
"insurance_cost": 125
98+
}
99+
```
100+
101+
**Direct UPS quotes:**
102+
```json
103+
{
104+
"carrier": "UPS",
105+
"service_code": "03",
106+
"carrier_amount": 1000,
107+
"markup_amount": 50,
108+
"markup_type": "flat"
109+
}
110+
```
111+
112+
**Direct USPS quotes:**
113+
```json
114+
{
115+
"carrier": "USPS",
116+
"mail_class": "PRIORITY_MAIL",
117+
"carrier_amount": 810,
118+
"markup_amount": 25,
119+
"markup_type": "flat"
120+
}
121+
```
122+
123+
---
124+
125+
## Label Purchase
126+
127+
Selecting a ServiceQuote and confirming the order calls `createOrderFromServiceQuote` on the bridge, which:
128+
129+
1. Calls the carrier's label API (ParcelPath `/v1/labels`, UPS Ship API, USPS Labels API).
130+
2. Decodes the base64 label binary (PDF or ZPL).
131+
3. Writes a `File` record under the `carrier-labels/` folder.
132+
4. Updates `Order.meta.integrated_vendor_order` with tracking number, carrier, and insurance details.
133+
5. The `LabelController::getLabel()` fallback serves the carrier label from Storage before falling back to the internal Blade label.
134+
135+
---
136+
137+
## Tracking
138+
139+
### Polling (primary)
140+
141+
Three scheduled jobs run every 15 minutes:
142+
143+
| Job | Scope | Code mapper |
144+
|---|---|---|
145+
| `PollParcelPathTrackingJob` | Orders with `parcelpath_shipment_id` | Pre-normalized by ParcelPath |
146+
| `PollUPSTrackingJob` | Orders with `shipmentIdentificationNumber` | I→IN_TRANSIT, D→DELIVERED, X→EXCEPTION, P→PICKED_UP, M→MANIFESTED, O→OUT_FOR_DELIVERY, RS→RETURN_TO_SENDER |
147+
| `PollUSPSTrackingJob` | Orders with carrier=USPS | ALERT→EXCEPTION; all others verbatim |
148+
149+
Each job: `TrackingStatus::firstOrCreate` per event (idempotent), terminal transition via `ParcelPath::terminalOrderStatus()` (DELIVERED→completed, RETURN_TO_SENDER→returned).
150+
151+
### Webhooks (supplementary)
152+
153+
`POST /webhooks/parcel/{providerKey}` accepts push events from ParcelPath, UPS, or USPS. Same normalization and dedup key as the poll jobs. Shared-secret auth via `X-Webhook-Secret` header. The `TrackingStatusObserver` handles Order status transitions and SocketCluster broadcast for both poll and webhook paths.
154+
155+
### Real-time updates
156+
157+
When a `TrackingStatus` is created (by any path), the `TrackingStatusObserver` updates the parent Order's status and the existing `SendsWebhooks` trait + `ResourceLifecycleEvent` pipeline broadcasts the event to SocketCluster channels (`company.<uuid>`, `order.<publicId>`). The console and Navigator app receive updates without additional wiring.
158+
159+
---
160+
161+
## Batch Shipping
162+
163+
`POST /v1/batch-shipments/rates` and `POST /v1/batch-shipments/purchase` enable bulk operations:
164+
165+
- **Rates**: submit an array of shipments, each with pickup/dropoff/parcels/facilitator. Returns per-row rate results with `row_id`.
166+
- **Purchase**: submit an array of `service_quote_uuid` selections. Returns per-row tracking numbers + label file UUIDs.
167+
- **Idempotent**: checks PurchaseRate + Order.meta before buying, returns existing label if already purchased.
168+
- **Per-row isolation**: one row failing never aborts the batch.
169+
170+
---
171+
172+
## Insurance (Shipsurance)
173+
174+
**Mode A (ParcelPath):** insurance is managed by ParcelPath's API. The TMS stores the policy details from the label response.
175+
176+
**Mode B (Direct):** operators can enable Shipsurance on the IntegratedVendor:
177+
1. Set `insurance_provider` to `Shipsurance`.
178+
2. Set `insurance_default` to `Auto-insure all` or `Ask per shipment`.
179+
3. Enter the `shipsurance_api_key`.
180+
181+
Premium calculation: declared value rounded up to the next $100 × rate per $100 ($1.00 domestic, $1.50 international).
182+
183+
---
184+
185+
## Service Types
186+
187+
### ParcelPath (13 services)
188+
PP_UPS_GROUND, PP_UPS_GROUND_SAVER, PP_UPS_3DS, PP_UPS_2DA, PP_UPS_2DAM, PP_UPS_1DA, PP_UPS_1DAM, PP_UPS_1DASAVER, PP_USPS_PRIORITY, PP_USPS_EXPRESS, PP_USPS_GROUND_ADV, PP_USPS_FIRST, PP_USPS_MEDIA
189+
190+
### UPS Direct (8 services)
191+
GROUND (03), GROUND_SAVER (93), 3DS (12), 2DA (02), 2DAM (59), 1DA (01), 1DAM (14), 1DASAVER (13)
192+
193+
### USPS Direct (5 services)
194+
PRIORITY (PRIORITY_MAIL), PRIORITY_EXPRESS (PRIORITY_MAIL_EXPRESS), GROUND_ADVANTAGE (USPS_GROUND_ADVANTAGE), FIRST_CLASS (FIRST-CLASS_PACKAGE_SERVICE), MEDIA_MAIL (MEDIA_MAIL)
195+
196+
---
197+
198+
## Dev Environment Notes
199+
200+
### Docker bind mount
201+
202+
Mount only `../fleetops/server` (not the full `../fleetops`) into `application`, `queue`, and `scheduler` services. The full mount drags `server_vendor/` (~58k files) and wedges Docker Desktop.
203+
204+
### Running migrations
205+
206+
```bash
207+
docker compose exec application php artisan migrate \
208+
--path=/fleetbase/api/vendor/fleetbase/fleetops/server/migrations \
209+
--realpath --force
210+
```
211+
212+
`--realpath` is required with absolute paths — without it Laravel silently finds nothing.
213+
214+
### Running tests
215+
216+
```bash
217+
docker run --rm -v ~/fleetbase-project/fleetops:/app -w /app \
218+
fleetbase/fleetbase-api:latest \
219+
sh -c "ln -sf server_vendor vendor && ./server_vendor/bin/pest"
220+
```
221+
222+
The `vendor → server_vendor` symlink is required because Pest hardcodes the `vendor/` path.

0 commit comments

Comments
 (0)